@parity/product-sdk-bulletin 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -88,8 +88,11 @@ async function checkAuthorization(api, address) {
88
88
  }
89
89
  const status = {
90
90
  authorized: true,
91
- remainingTransactions: auth.extent.transactions,
92
- remainingBytes: auth.extent.bytes,
91
+ remainingTransactions: auth.extent.transactions_allowance - auth.extent.transactions,
92
+ // `auth` is `any` (TypedApi<any> upstream — see line 53). TS narrows
93
+ // `any - any` to `number`, so cast each u64 operand to bigint to keep
94
+ // the subtraction in bigint space. Runtime values are bigints from PAPI.
95
+ remainingBytes: auth.extent.bytes_allowance - auth.extent.bytes,
93
96
  expiration: auth.expiration
94
97
  };
95
98
  log.debug("checkAuthorization", {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/authorization.ts","../src/cid.ts","../src/resolve-query.ts","../src/query.ts","../src/verify.ts","../src/client.ts","../src/networks.ts","../src/lazy-signer.ts"],"names":["log","createLogger","CidCodec","CID","chain","inner","bulletinDescriptor"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bulletin error types.\n *\n * Two error families coexist:\n *\n * 1. **Upstream SDK errors** — `BulletinError` and `ErrorCode` from\n * `@parity/bulletin-sdk` cover upload/store/authorization failures\n * surfaced by `AsyncBulletinClient`. Each carries `code`, `retryable`,\n * and `recoveryHint`.\n * 2. **Read-side errors** declared here — host preimage availability /\n * lookup timeouts / interrupts, plus CID format problems, surfaced by\n * our retrieval helpers (`fetchBytes`, `fetchJson`, `verifyOnChain`).\n *\n * Catch upstream errors with `instanceof BulletinError`. Catch our read-side\n * errors with `instanceof ProductBulletinError` (or the specific subclass).\n */\nexport { BulletinError, ErrorCode } from \"@parity/bulletin-sdk\";\n\n/**\n * Base class for read-side errors raised by `@parity/product-sdk-bulletin`.\n *\n * Distinct from upstream `BulletinError` which covers upload/store failures.\n */\nexport class ProductBulletinError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ProductBulletinError\";\n }\n}\n\n/**\n * The host preimage API is unavailable.\n *\n * Thrown when bulletin operations require the host container but it's not\n * available. This typically means the SDK is running outside Polkadot\n * Browser / Desktop. The bulletin SDK is container-only by design — see\n * the README for the rationale.\n */\nexport class BulletinHostUnavailableError extends ProductBulletinError {\n constructor(operation: \"upload\" | \"query\") {\n super(\n `Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`,\n );\n this.name = \"BulletinHostUnavailableError\";\n }\n}\n\n/**\n * The host preimage lookup timed out.\n *\n * The host was unable to retrieve the requested content within the timeout\n * period. The content may still become available later.\n */\nexport class BulletinLookupTimeoutError extends ProductBulletinError {\n /** The CID that was being looked up. */\n readonly cid: string;\n /** The timeout duration in milliseconds. */\n readonly timeoutMs: number;\n\n constructor(cid: string, timeoutMs: number) {\n super(`Host preimage lookup timed out after ${timeoutMs}ms for CID: ${cid}`);\n this.name = \"BulletinLookupTimeoutError\";\n this.cid = cid;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * The host interrupted the preimage lookup.\n *\n * The host terminated the lookup subscription, typically after repeated\n * failures or when the host determines the content is unavailable.\n */\nexport class BulletinLookupInterruptedError extends ProductBulletinError {\n /** The CID that was being looked up. */\n readonly cid: string;\n\n constructor(cid: string) {\n super(`Host preimage lookup was interrupted for CID: ${cid}`);\n this.name = \"BulletinLookupInterruptedError\";\n this.cid = cid;\n }\n}\n\n/**\n * Invalid CID format or version.\n */\nexport class BulletinCidError extends ProductBulletinError {\n /** The invalid CID string, if available. */\n readonly cid?: string;\n\n constructor(message: string, cid?: string) {\n super(message);\n this.name = \"BulletinCidError\";\n this.cid = cid;\n }\n}\n\n/**\n * Failed to check authorization status for an account.\n *\n * Wraps RPC or query errors that occur when checking if an account\n * is authorized to store data on the Bulletin Chain.\n */\nexport class BulletinAuthorizationError extends ProductBulletinError {\n /** The address that was being checked. */\n readonly address: string;\n\n constructor(address: string, cause?: unknown) {\n super(`Failed to check authorization for ${address}`, { cause });\n this.name = \"BulletinAuthorizationError\";\n this.address = address;\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"ProductBulletinError hierarchy\", () => {\n test(\"ProductBulletinError extends Error\", () => {\n const err = new ProductBulletinError(\"test\");\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"ProductBulletinError\");\n });\n\n test(\"BulletinCidError\", () => {\n const err = new BulletinCidError(\"bad\", \"Qmabc\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"Qmabc\");\n });\n\n test(\"BulletinHostUnavailableError\", () => {\n const err = new BulletinHostUnavailableError(\"query\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.message).toContain(\"query\");\n expect(err.message).toContain(\"Host preimage API unavailable\");\n });\n\n test(\"BulletinLookupTimeoutError\", () => {\n const err = new BulletinLookupTimeoutError(\"bafyabc123\", 30000);\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"bafyabc123\");\n expect(err.timeoutMs).toBe(30000);\n expect(err.message).toContain(\"30000ms\");\n });\n\n test(\"BulletinLookupInterruptedError\", () => {\n const err = new BulletinLookupInterruptedError(\"bafyabc123\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"bafyabc123\");\n expect(err.message).toContain(\"interrupted\");\n });\n\n test(\"BulletinAuthorizationError carries cause\", () => {\n const cause = new Error(\"RPC down\");\n const err = new BulletinAuthorizationError(\"5GrwvaEF...\", cause);\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.address).toBe(\"5GrwvaEF...\");\n expect(err.cause).toBe(cause);\n });\n });\n}\n","import { createLogger } from \"@parity/product-sdk-logger\";\nimport { submitAndWatch, type TxStatus, type WaitFor } from \"@parity/product-sdk-tx\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { Enum } from \"polkadot-api\";\n\nimport { BulletinAuthorizationError, ProductBulletinError } from \"./errors.js\";\nimport type { AuthorizationStatus, BulletinApi } from \"./types.js\";\n\nconst log = createLogger(\"bulletin\");\n\nconst NOT_AUTHORIZED: AuthorizationStatus = Object.freeze({\n authorized: false,\n remainingTransactions: 0,\n remainingBytes: 0n,\n expiration: 0,\n});\n\n/**\n * Check whether an account is authorized to store data on the Bulletin Chain.\n *\n * Queries `TransactionStorage.Authorizations` for the given address and returns\n * the raw authorization quota. Use this as a pre-flight check before calling\n * {@link upload} to provide clear UX (\"not authorized\" / \"insufficient quota\")\n * instead of letting the transaction fail mid-execution.\n *\n * The expiration block number is returned as-is — the chain enforces expiration\n * at submission time, so callers can optionally compare against the current\n * block for display purposes.\n *\n * @param api - Typed Bulletin Chain API instance.\n * @param address - SS58-encoded account address to check.\n * @returns Authorization status with remaining quota.\n *\n * @example\n * ```ts\n * import { checkAuthorization } from \"@parity/product-sdk-bulletin\";\n *\n * const auth = await checkAuthorization(api, address);\n * if (!auth.authorized) {\n * console.error(\"Account is not authorized for bulletin storage\");\n * } else if (auth.remainingBytes < BigInt(fileBytes.length)) {\n * console.error(`Insufficient quota: ${auth.remainingBytes} bytes remaining`);\n * }\n * ```\n *\n * @see {@link BulletinClient.checkAuthorization} for the client method equivalent.\n */\nexport async function checkAuthorization(\n api: BulletinApi,\n address: string,\n): Promise<AuthorizationStatus> {\n let auth;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth = await (api as any).query.TransactionStorage.Authorizations.getValue(\n Enum(\"Account\", address),\n );\n } catch (error) {\n log.error(\"checkAuthorization: query failed\", { address, error });\n throw new BulletinAuthorizationError(address, error);\n }\n\n if (!auth) {\n log.debug(\"checkAuthorization: no authorization found\", { address });\n return NOT_AUTHORIZED;\n }\n\n const status: AuthorizationStatus = {\n authorized: true,\n remainingTransactions: auth.extent.transactions,\n remainingBytes: auth.extent.bytes,\n expiration: auth.expiration,\n };\n\n log.debug(\"checkAuthorization\", {\n address,\n remainingTransactions: status.remainingTransactions,\n remainingBytes: status.remainingBytes.toString(),\n expiration: status.expiration,\n });\n\n return status;\n}\n\n/**\n * Options for {@link authorizeAccount}.\n */\nexport interface AuthorizeAccountOptions {\n /**\n * Wrap the extrinsic in `Sudo.sudo(...)` before submission. Default: `false`.\n *\n * Use `true` on networks where granting bulletin storage authorization\n * requires sudo permissions (most production / managed test networks).\n * Use `false` (default) when the account self-authorizes — typical for\n * local development chains.\n */\n viaSudo?: boolean;\n /** When to resolve: `\"best-block\"` (default) or `\"finalized\"`. */\n waitFor?: WaitFor;\n /** Timeout in ms. Default: 300_000 (5 min). */\n timeoutMs?: number;\n /** Lifecycle status callback for UI progress. */\n onStatus?: (status: TxStatus) => void;\n}\n\n/**\n * Grant an account authorization to store data on the Bulletin Chain.\n *\n * Submits a `TransactionStorage.authorize_account` extrinsic, optionally\n * wrapped in `Sudo.sudo(...)` for networks that require sudo to grant\n * authorization. Mirrors the call shape of {@link upload} — top-level\n * function, takes an explicit signer, returns a block hash on success.\n *\n * Pair with {@link checkAuthorization} for a typical \"check, grant if\n * insufficient, then upload\" flow.\n *\n * ## Additive semantics — call once per authorization need\n *\n * `authorize_account` is **additive** within an unexpired authorization window\n * for `AuthorizationScope::Account` (see `pallet-bulletin-transaction-storage`,\n * `fn authorize`). Each successful call **adds** to the existing\n * `transactions_allowance` and `bytes_allowance` rather than overwriting them.\n *\n * Implications for callers:\n *\n * - Calling this function twice with `(100, 1MB)` while the previous\n * authorization is still active leaves the account with quota for `200`\n * transactions and `2MB` — likely unintended.\n * - **This function does NOT use `withRetry`.** Retrying a successful-but-\n * acknowledgment-lost submission would double-grant the quota. Callers\n * needing retry should wrap this function and use {@link checkAuthorization}\n * to verify the post-state before retrying.\n * - To \"reset\" a quota, let the existing authorization expire\n * (`AuthorizationPeriod` blocks). The next call after expiry creates a fresh\n * authorization rather than adding.\n *\n * Note: `AuthorizationScope::Preimage` uses `set` semantics in the same\n * pallet. This helper is for account-scope authorization only.\n *\n * @param api - Typed Bulletin Chain API instance.\n * @param who - SS58-encoded account to authorize.\n * @param transactions - Number of transactions to **add** to the account's allowance.\n * @param bytes - Byte budget to **add** to the account's allowance.\n * @param signer - Signer for the extrinsic. On `viaSudo: true` this must be the sudo key.\n * @param options - Optional `viaSudo` flag plus standard submission controls.\n * @returns Block hash where the extrinsic was included.\n * @throws {ProductBulletinError} If `viaSudo: true` is requested but the chain has no `Sudo` pallet.\n *\n * @example Direct (account self-authorizes — local dev)\n * ```ts\n * import { authorizeAccount } from \"@parity/product-sdk-bulletin\";\n *\n * await authorizeAccount(api, address, 100, 100n * 1024n * 1024n, signer);\n * ```\n *\n * @example Sudo-wrapped (managed test network)\n * ```ts\n * await authorizeAccount(api, userAddress, 100, 1_000_000n, sudoSigner, {\n * viaSudo: true,\n * });\n * ```\n *\n * @see {@link checkAuthorization} for the read counterpart.\n * @see {@link BulletinClient.authorizeAccount} for the client method equivalent.\n */\nexport async function authorizeAccount(\n api: BulletinApi,\n who: string,\n transactions: number,\n bytes: bigint,\n signer: PolkadotSigner,\n options: AuthorizeAccountOptions = {},\n): Promise<{ blockHash: string }> {\n const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;\n\n // Single `as any` cast for the whole function. `BulletinApi` is upstream-\n // typed as `TypedApi<any>` (see types.ts), so member access is loose by\n // design. Same pattern as upload.ts:57 — narrowing it requires retyping\n // BulletinApi against a bundled descriptor (out of scope here).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const apiTx = (api as any).tx;\n\n log.info(\"authorizeAccount: building extrinsic\", {\n who,\n transactions,\n bytes: bytes.toString(),\n viaSudo,\n });\n\n if (viaSudo && !apiTx.Sudo?.sudo) {\n throw new ProductBulletinError(\n \"viaSudo: true requires the Sudo pallet, which is not available on this network. \" +\n \"On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism.\",\n );\n }\n\n const authorizeTx = apiTx.TransactionStorage.authorize_account({\n who,\n transactions,\n bytes,\n });\n\n const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;\n\n // NOTE: Intentionally NOT using `withRetry` here. `authorize_account` is\n // additive (see JSDoc above), so a retry after a successful-but-lost\n // submission would double-grant the quota. Caller-side retry must verify\n // post-state via `checkAuthorization` first.\n const result = await submitAndWatch(txToSubmit, signer, {\n waitFor,\n timeoutMs,\n onStatus,\n });\n\n log.info(\"authorizeAccount: included in block\", {\n who,\n blockHash: result.block.hash,\n });\n\n return { blockHash: result.block.hash };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function createMockApi(authResult: unknown) {\n return {\n query: {\n TransactionStorage: {\n Authorizations: {\n getValue: vi.fn().mockResolvedValue(authResult),\n },\n },\n },\n } as unknown as BulletinApi;\n }\n\n describe(\"checkAuthorization\", () => {\n test(\"returns not authorized when no authorization exists\", async () => {\n const api = createMockApi(undefined);\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(false);\n expect(status.remainingTransactions).toBe(0);\n expect(status.remainingBytes).toBe(0n);\n expect(status.expiration).toBe(0);\n });\n\n test(\"returns authorization with full quota\", async () => {\n const api = createMockApi({\n extent: { transactions: 10, bytes: 1_000_000n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingTransactions).toBe(10);\n expect(status.remainingBytes).toBe(1_000_000n);\n expect(status.expiration).toBe(999);\n });\n\n test(\"returns authorization with zero transactions remaining\", async () => {\n const api = createMockApi({\n extent: { transactions: 0, bytes: 1_000_000n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingTransactions).toBe(0);\n });\n\n test(\"returns authorization with zero bytes remaining\", async () => {\n const api = createMockApi({\n extent: { transactions: 5, bytes: 0n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingBytes).toBe(0n);\n });\n\n test(\"preserves expiration block number\", async () => {\n const api = createMockApi({\n extent: { transactions: 1, bytes: 500n },\n expiration: 12345,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.expiration).toBe(12345);\n });\n\n test(\"throws BulletinAuthorizationError when query fails\", async () => {\n const api = {\n query: {\n TransactionStorage: {\n Authorizations: {\n getValue: vi.fn().mockRejectedValue(new Error(\"RPC connection lost\")),\n },\n },\n },\n } as unknown as BulletinApi;\n\n const err = await checkAuthorization(api, \"5GrwvaEF...\").catch((e: unknown) => e);\n expect(err).toBeInstanceOf(BulletinAuthorizationError);\n const error = err as BulletinAuthorizationError;\n expect(error.address).toBe(\"5GrwvaEF...\");\n expect(error.cause).toBeInstanceOf(Error);\n expect((error.cause as Error).message).toBe(\"RPC connection lost\");\n });\n\n test(\"passes correct Enum key to the query\", async () => {\n const getValue = vi.fn().mockResolvedValue(undefined);\n const api = {\n query: {\n TransactionStorage: {\n Authorizations: { getValue },\n },\n },\n } as unknown as BulletinApi;\n\n await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(getValue).toHaveBeenCalledTimes(1);\n const arg = getValue.mock.calls[0][0];\n expect(arg.type).toBe(\"Account\");\n expect(arg.value).toBe(\"5GrwvaEF...\");\n });\n });\n\n /**\n * Mock factory for `authorizeAccount` tests.\n *\n * Mirrors the mocking style used in `upload.ts` — we don't mock\n * `submitAndWatch` (the SDK helper) directly. Instead we let it call\n * through to a fake `signSubmitAndWatch` on the api, which emits the\n * lifecycle events `submitAndWatch` listens for.\n */\n function createMockApiForAuthorize(blockHash = \"0xblockhash\") {\n const fakeTx = {\n decodedCall: { fakeCall: true } as unknown,\n signSubmitAndWatch: vi.fn().mockReturnValue({\n subscribe: (handlers: { next: (e: unknown) => void }) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xtxhash\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xtxhash\",\n found: true,\n ok: true,\n block: { hash: blockHash, number: 1, index: 0 },\n events: [],\n });\n });\n return { unsubscribe: vi.fn() };\n },\n }),\n };\n\n return {\n api: {\n tx: {\n TransactionStorage: {\n authorize_account: vi.fn().mockReturnValue(fakeTx),\n },\n Sudo: {\n sudo: vi.fn().mockReturnValue(fakeTx),\n },\n },\n },\n fakeTx,\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n describe(\"authorizeAccount\", () => {\n test(\"direct path: calls TransactionStorage.authorize_account with the right params\", async () => {\n const { api } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 100,\n 1_000_000n,\n mockSigner,\n );\n\n expect(api.tx.TransactionStorage.authorize_account).toHaveBeenCalledOnce();\n const arg = api.tx.TransactionStorage.authorize_account.mock.calls[0][0];\n expect(arg.who).toBe(\"5GrwvaEF...\");\n expect(arg.transactions).toBe(100);\n expect(arg.bytes).toBe(1_000_000n);\n });\n\n test(\"direct path: does NOT call Sudo.sudo when viaSudo is false (default)\", async () => {\n const { api } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n );\n\n expect(api.tx.Sudo.sudo).not.toHaveBeenCalled();\n });\n\n test(\"direct path: returns the block hash from submission\", async () => {\n const { api } = createMockApiForAuthorize(\"0xdeadbeef\");\n\n const result = await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n );\n\n expect(result).toEqual({ blockHash: \"0xdeadbeef\" });\n });\n\n test(\"sudo path: wraps the authorize_account call inside Sudo.sudo\", async () => {\n const { api, fakeTx } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n );\n\n expect(api.tx.Sudo.sudo).toHaveBeenCalledOnce();\n const sudoArg = api.tx.Sudo.sudo.mock.calls[0][0];\n expect(sudoArg.call).toBe(fakeTx.decodedCall);\n });\n\n test(\"sudo path: still returns the block hash from the sudo extrinsic\", async () => {\n const { api } = createMockApiForAuthorize(\"0xsudoblock\");\n\n const result = await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n );\n\n expect(result).toEqual({ blockHash: \"0xsudoblock\" });\n });\n\n test(\"throws ProductBulletinError when viaSudo is true but the chain lacks a Sudo pallet\", async () => {\n const apiWithoutSudo = {\n tx: {\n TransactionStorage: {\n authorize_account: vi.fn().mockReturnValue({\n decodedCall: {} as unknown,\n signSubmitAndWatch: vi.fn(),\n }),\n },\n // Sudo intentionally absent — represents production Polkadot/Kusama\n },\n };\n\n const err = await authorizeAccount(\n apiWithoutSudo as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n ).catch((e: unknown) => e);\n\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect((err as Error).message).toMatch(/Sudo pallet/i);\n // Verify we did NOT proceed to submit anything\n expect(apiWithoutSudo.tx.TransactionStorage.authorize_account).not.toHaveBeenCalled();\n });\n\n // Note: submission-failure propagation is tested upstream in\n // @parity/product-sdk-tx's own suite. Re-testing it here would just\n // re-test submitAndWatch's error path through a brittle mock, which\n // depends on internal event-shape details outside this file's contract.\n });\n}\n","/**\n * CID helpers for converting between on-chain hex hashes and CIDs.\n *\n * Upstream `@parity/bulletin-sdk` exposes `calculateCid` (data → CID),\n * `parseCid` (string → CID), `cidFromBytes` (full-encoded → CID), and\n * `cidToBytes` (CID → full-encoded). The helpers here add a thin layer\n * for the `0x`-prefixed hex shape that on-chain `TransactionInfo` uses,\n * so callers don't need to do the digest plumbing themselves.\n *\n * Both helpers default to the chain default (blake2b-256, raw codec).\n * Pass `HashAlgorithm` and `CidCodec` for other configurations\n * (sha2-256, dag-pb, etc.).\n */\nimport { CID } from \"multiformats/cid\";\nimport * as Digest from \"multiformats/hashes/digest\";\n\nimport { BulletinCidError } from \"./errors.js\";\n\n/**\n * Hash algorithms supported by the Bulletin Chain.\n *\n * Values are multihash codes from the multicodec table.\n */\nexport const HashAlgorithm = {\n /** BLAKE2b-256 — chain default. */\n Blake2b256: 0xb220,\n /** SHA2-256. */\n Sha2_256: 0x12,\n /** Keccak-256 — Ethereum compatibility. */\n Keccak256: 0x1b,\n} as const;\nexport type HashAlgorithm = (typeof HashAlgorithm)[keyof typeof HashAlgorithm];\n\n/**\n * CID codecs supported by the Bulletin Chain.\n */\nexport const CidCodec = {\n /** Raw binary — default for single-chunk data. */\n Raw: 0x55,\n /** DAG-PB — used for multi-chunk manifests / IPFS UnixFS. */\n DagPb: 0x70,\n /** DAG-CBOR — alternative DAG encoding. */\n DagCbor: 0x71,\n} as const;\nexport type CidCodec = (typeof CidCodec)[keyof typeof CidCodec];\n\nconst SUPPORTED_HASH_CODES = new Set<number>(Object.values(HashAlgorithm));\nconst SUPPORTED_CODEC_CODES = new Set<number>(Object.values(CidCodec));\nconst EXPECTED_HEX_LENGTH = 66; // \"0x\" + 64 hex chars (32-byte digest)\n\n/**\n * Reconstruct a CIDv1 from a `0x`-prefixed 32-byte hex hash.\n *\n * Useful when reading on-chain `TransactionInfo.content_hash` and you need\n * the CID to look up content via an IPFS gateway.\n *\n * @param hexHash - 66-char `0x`-prefixed hex of a 32-byte digest.\n * @param hashCode - Multihash code (default: blake2b-256).\n * @param codec - Multicodec code (default: raw).\n */\nexport function hashToCid(\n hexHash: `0x${string}`,\n hashCode: HashAlgorithm = HashAlgorithm.Blake2b256,\n codec: CidCodec = CidCodec.Raw,\n): string {\n if (hexHash.length !== EXPECTED_HEX_LENGTH) {\n throw new BulletinCidError(\n `Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), ` +\n `got ${hexHash.length} chars`,\n );\n }\n if (!/^0x[0-9a-fA-F]{64}$/.test(hexHash)) {\n throw new BulletinCidError(\n `Invalid hash format: expected 0x-prefixed 32-byte hex string, got: ${hexHash}`,\n );\n }\n if (!SUPPORTED_HASH_CODES.has(hashCode)) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${hashCode.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n );\n }\n if (!SUPPORTED_CODEC_CODES.has(codec)) {\n throw new BulletinCidError(\n `Unsupported CID codec 0x${codec.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n );\n }\n const digest = hexToBytes(hexHash);\n return CID.createV1(codec, Digest.create(hashCode, digest)).toString();\n}\n\n/**\n * Extract the 32-byte content hash digest from a CIDv1 and return it as a\n * `0x`-prefixed hex string.\n *\n * Useful for matching a CID against on-chain `TransactionInfo.content_hash`.\n */\nexport function cidToPreimageKey(cid: string): `0x${string}` {\n let parsed;\n try {\n parsed = CID.parse(cid);\n } catch {\n throw new BulletinCidError(`Invalid CID: ${cid}`, cid);\n }\n if (parsed.version !== 1) {\n throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);\n }\n if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n cid,\n );\n }\n return `0x${bytesToHex(parsed.multihash.digest)}` as `0x${string}`;\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const out = new Uint8Array(32);\n for (let i = 0; i < 32; i++) {\n out[i] = Number.parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);\n }\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let s = \"\";\n for (let i = 0; i < bytes.length; i++) {\n s += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return s;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"hashToCid\", () => {\n const sampleHex = `0x${\"ab\".repeat(32)}` as `0x${string}`;\n\n test(\"produces valid base32-lower CIDv1 (default: blake2b-256, raw)\", () => {\n const cid = hashToCid(sampleHex);\n expect(cid).toMatch(/^b[a-z2-7]+$/);\n const parsed = CID.parse(cid);\n expect(parsed.version).toBe(1);\n expect(parsed.code).toBe(CidCodec.Raw);\n expect(parsed.multihash.code).toBe(HashAlgorithm.Blake2b256);\n });\n\n test(\"supports sha2-256\", () => {\n const cid = hashToCid(sampleHex, HashAlgorithm.Sha2_256);\n expect(CID.parse(cid).multihash.code).toBe(HashAlgorithm.Sha2_256);\n });\n\n test(\"supports dag-pb codec\", () => {\n const cid = hashToCid(sampleHex, HashAlgorithm.Blake2b256, CidCodec.DagPb);\n expect(CID.parse(cid).code).toBe(CidCodec.DagPb);\n });\n\n test(\"throws on short hex\", () => {\n expect(() => hashToCid(\"0xabcd\" as `0x${string}`)).toThrow(BulletinCidError);\n });\n\n test(\"throws on long hex\", () => {\n const tooLong = `0x${\"aa\".repeat(33)}` as `0x${string}`;\n expect(() => hashToCid(tooLong)).toThrow(BulletinCidError);\n });\n\n test(\"throws on non-hex characters\", () => {\n const bad = `0x${\"zz\".repeat(32)}` as `0x${string}`;\n expect(() => hashToCid(bad)).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported hash algorithm\", () => {\n expect(() => hashToCid(sampleHex, 0x99 as HashAlgorithm)).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported codec\", () => {\n expect(() => hashToCid(sampleHex, HashAlgorithm.Blake2b256, 0x99 as CidCodec)).toThrow(\n BulletinCidError,\n );\n });\n });\n\n describe(\"cidToPreimageKey\", () => {\n test(\"round-trip with hashToCid\", () => {\n const hex = `0x${\"cd\".repeat(32)}` as `0x${string}`;\n const cid = hashToCid(hex);\n expect(cidToPreimageKey(cid)).toBe(hex);\n });\n\n test(\"round-trip with sha2-256\", () => {\n const hex = `0x${\"ef\".repeat(32)}` as `0x${string}`;\n const cid = hashToCid(hex, HashAlgorithm.Sha2_256);\n expect(cidToPreimageKey(cid)).toBe(hex);\n });\n\n test(\"throws on invalid CID string\", () => {\n expect(() => cidToPreimageKey(\"not-a-cid\")).toThrow(BulletinCidError);\n });\n\n test(\"throws on CIDv0 input\", () => {\n const hash = new Uint8Array(32).fill(0xab);\n const cidV0 = CID.create(0, 0x70, Digest.create(HashAlgorithm.Sha2_256, hash));\n expect(() => cidToPreimageKey(cidV0.toString())).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported hash algorithm\", () => {\n const hash = new Uint8Array(32).fill(0xab);\n const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(0x99, hash));\n expect(() => cidToPreimageKey(cidV1.toString())).toThrow(BulletinCidError);\n });\n });\n}\n","import { calculateCid } from \"@parity/bulletin-sdk\";\nimport { getPreimageManager, type PreimageManager } from \"@parity/product-sdk-host\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { cidToPreimageKey } from \"./cid.js\";\nimport {\n BulletinHostUnavailableError,\n BulletinLookupInterruptedError,\n BulletinLookupTimeoutError,\n} from \"./errors.js\";\n\nconst log = createLogger(\"bulletin\");\n\nconst DEFAULT_LOOKUP_TIMEOUT_MS = 30_000;\n\n/**\n * Query strategy for the Bulletin Chain.\n *\n * The host manages the lookup via its preimage subscription API,\n * which includes local caching and managed IPFS polling.\n */\nexport interface QueryStrategy {\n kind: \"host-lookup\";\n lookup: (cid: string, timeoutMs?: number) => Promise<Uint8Array>;\n}\n\n/**\n * Determine the query strategy for the Bulletin Chain.\n *\n * Uses the host preimage lookup API which caches results and manages\n * IPFS polling automatically.\n *\n * @returns The resolved query strategy.\n * @throws {BulletinHostUnavailableError} If the host preimage manager is unavailable.\n */\nexport async function resolveQueryStrategy(): Promise<QueryStrategy> {\n const preimageManager = await getPreimageManager();\n if (preimageManager) {\n log.info(\"using host preimage lookup for bulletin queries\");\n return {\n kind: \"host-lookup\",\n lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs),\n };\n }\n\n throw new BulletinHostUnavailableError(\"query\");\n}\n\n/**\n * Wrap `preimageManager.lookup` (subscription-based) into a one-shot Promise.\n *\n * Converts the CID to a hex preimage key, subscribes, and resolves on the\n * first non-null callback. Rejects on timeout or if the host interrupts the\n * subscription (e.g. after repeated failures). Always unsubscribes on settlement.\n *\n * @param manager - The product-sdk preimage manager.\n * @param cid - CIDv1 string to look up.\n * @param timeoutMs - Maximum wait time. Default: 30_000ms.\n * @returns The raw bytes of the preimage.\n */\nexport function lookupViaHost(\n manager: PreimageManager,\n cid: string,\n timeoutMs: number = DEFAULT_LOOKUP_TIMEOUT_MS,\n): Promise<Uint8Array> {\n const key = cidToPreimageKey(cid);\n\n return new Promise<Uint8Array>((resolve, reject) => {\n const cleanup = () => {\n cancelInterrupt();\n sub.unsubscribe();\n };\n\n const settle = (fn: () => void) => {\n if (timer === null) return;\n clearTimeout(timer);\n timer = null;\n cleanup();\n fn();\n };\n\n let timer: ReturnType<typeof setTimeout> | null = setTimeout(() => {\n settle(() => {\n reject(new BulletinLookupTimeoutError(cid, timeoutMs));\n });\n }, timeoutMs);\n\n const sub = manager.lookup(key, (preimage) => {\n if (preimage !== null) {\n settle(() => resolve(preimage));\n }\n // null means \"not found yet\" — host will keep polling\n });\n\n const cancelInterrupt = sub.onInterrupt(() => {\n settle(() => {\n reject(new BulletinLookupInterruptedError(cid));\n });\n });\n });\n}\n\nif (import.meta.vitest) {\n const { beforeAll, describe, test, expect, vi } = import.meta.vitest;\n\n // Note: resolveQueryStrategy tests require e2e testing as they\n // depend on the host container environment.\n\n describe(\"lookupViaHost\", () => {\n function createMockManager(\n behavior: \"resolve\" | \"null-then-resolve\" | \"hang\" | \"interrupt\",\n ) {\n const unsubscribe = vi.fn();\n const cancelInterrupt = vi.fn();\n let interruptCb: VoidFunction | undefined;\n\n const lookup = vi.fn((_key: string, callback: (p: Uint8Array | null) => void) => {\n const data = new Uint8Array([10, 20, 30]);\n queueMicrotask(() => {\n if (behavior === \"resolve\") {\n callback(data);\n } else if (behavior === \"null-then-resolve\") {\n callback(null);\n queueMicrotask(() => callback(data));\n } else if (behavior === \"interrupt\") {\n interruptCb?.();\n }\n // \"hang\" does nothing\n });\n return {\n unsubscribe,\n onInterrupt: (cb: VoidFunction) => {\n interruptCb = cb;\n return cancelInterrupt;\n },\n };\n });\n\n return { lookup, unsubscribe, cancelInterrupt, submit: vi.fn() };\n }\n\n // calculateCid is async (Web Crypto), so populate lazily.\n let testCid: string;\n beforeAll(async () => {\n const cid = await calculateCid(new TextEncoder().encode(\"test\"));\n testCid = cid.toString();\n });\n\n test(\"resolves on first non-null callback\", async () => {\n const manager = createMockManager(\"resolve\");\n const result = await lookupViaHost(manager, testCid);\n expect(result).toEqual(new Uint8Array([10, 20, 30]));\n });\n\n test(\"ignores null callbacks and resolves on subsequent data\", async () => {\n const manager = createMockManager(\"null-then-resolve\");\n const result = await lookupViaHost(manager, testCid);\n expect(result).toEqual(new Uint8Array([10, 20, 30]));\n });\n\n test(\"rejects with BulletinLookupTimeoutError on timeout\", async () => {\n const { BulletinLookupTimeoutError } = await import(\"./errors.js\");\n const manager = createMockManager(\"hang\");\n const err = await lookupViaHost(manager, testCid, 50).catch((e) => e);\n expect(err).toBeInstanceOf(BulletinLookupTimeoutError);\n expect(err.cid).toBe(testCid);\n expect(err.timeoutMs).toBe(50);\n });\n\n test(\"rejects with BulletinLookupInterruptedError on interrupt\", async () => {\n const { BulletinLookupInterruptedError } = await import(\"./errors.js\");\n const manager = createMockManager(\"interrupt\");\n const err = await lookupViaHost(manager, testCid).catch((e) => e);\n expect(err).toBeInstanceOf(BulletinLookupInterruptedError);\n expect(err.cid).toBe(testCid);\n });\n\n test(\"calls unsubscribe and cancelInterrupt on resolution\", async () => {\n const manager = createMockManager(\"resolve\");\n await lookupViaHost(manager, testCid);\n expect(manager.unsubscribe).toHaveBeenCalledOnce();\n expect(manager.cancelInterrupt).toHaveBeenCalledOnce();\n });\n\n test(\"calls unsubscribe on interrupt\", async () => {\n const manager = createMockManager(\"interrupt\");\n await lookupViaHost(manager, testCid).catch(() => {});\n expect(manager.unsubscribe).toHaveBeenCalledOnce();\n });\n\n test(\"passes correct hex key to manager\", async () => {\n const expectedKey = cidToPreimageKey(testCid);\n const manager = createMockManager(\"resolve\");\n await lookupViaHost(manager, testCid);\n expect(manager.lookup).toHaveBeenCalledWith(expectedKey, expect.any(Function));\n });\n });\n}\n","import { CidCodec, parseCid, UnixFsDagBuilder } from \"@parity/bulletin-sdk\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { QueryStrategy } from \"./resolve-query.js\";\nimport { resolveQueryStrategy } from \"./resolve-query.js\";\nimport type { QueryOptions } from \"./types.js\";\n\nconst log = createLogger(\"bulletin\");\n\n/**\n * Fetch raw bytes for a CID via the host's preimage lookup.\n *\n * Container-only by design: the bulletin SDK does not retrieve content\n * through public IPFS gateways. Inside a Polkadot Browser / Desktop\n * container, the host's `preimageManager` provides a cached, polling-\n * managed lookup that returns bytes when the underlying IPFS network\n * makes them available. Outside a container, this throws\n * {@link BulletinHostUnavailableError}.\n *\n * The bulletin chain stores transaction *metadata* on-chain\n * (`chunk_root`, `content_hash`, `size`, `cid_codec`, `hashing`) — the\n * content bytes themselves live in IPFS and are surfaced through the\n * host's preimage subscription, never via direct gateway fetch.\n *\n * To prove that a CID was stored on-chain (without fetching the bytes),\n * use `verifyOnChain` from `verify.ts`.\n *\n * @param cid - CIDv1 string to fetch.\n * @param options - Query options (`lookupTimeoutMs` for host).\n * @throws {BulletinHostUnavailableError} If running outside a container.\n */\nexport async function queryBytes(cid: string, options?: QueryOptions): Promise<Uint8Array> {\n const strategy = await resolveQueryStrategy();\n return executeQuery(strategy, cid, options);\n}\n\n/**\n * Fetch and parse JSON for a CID via the host's preimage lookup.\n *\n * Convenience wrapper over {@link queryBytes}.\n */\nexport async function queryJson<T>(cid: string, options?: QueryOptions): Promise<T> {\n const bytes = await queryBytes(cid, options);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Execute a query using a pre-resolved strategy.\n *\n * Exposed so `BulletinClient` can resolve the strategy once at\n * construction time and reuse it across calls without re-detecting\n * the host environment on every fetch.\n *\n * **Reassembly is automatic.** If `cid` carries the DAG-PB codec\n * (`0x70`) — meaning the upload was chunked and a UnixFS manifest was\n * created — this function recursively fetches each chunk via `strategy\n * .lookup` and returns the concatenated bytes. Pass `noReassemble: true`\n * to get the raw manifest bytes instead.\n *\n * For raw-codec CIDs (`0x55`, single-chunk content), the bytes returned\n * by the host are returned directly — no parsing overhead.\n */\nexport async function executeQuery(\n strategy: QueryStrategy,\n cid: string,\n options?: QueryOptions,\n): Promise<Uint8Array> {\n log.info(\"query: host preimage lookup\", { cid });\n const bytes = await strategy.lookup(cid, options?.lookupTimeoutMs);\n\n // Skip reassembly when the caller explicitly asks for raw bytes, or\n // when the CID's codec says this is a single-block payload (raw,\n // 0x55) — most uploads land here, so the parseCid + Promise.all\n // overhead is worth gating on codec rather than always paying it.\n if (options?.noReassemble) return bytes;\n const parsed = parseCid(cid);\n if (parsed.code !== CidCodec.DagPb) return bytes;\n\n log.info(\"query: reassembling DAG-PB manifest\", { cid });\n const builder = new UnixFsDagBuilder();\n const { chunkCids } = await builder.parse(bytes);\n\n // Fetch chunks in parallel — the host's preimageManager caches and\n // dedupes lookups, and order is preserved by Promise.all's input\n // ordering, which matches the DAG-PB Links order from parse().\n const chunks = await Promise.all(\n chunkCids.map((c) => strategy.lookup(c.toString(), options?.lookupTimeoutMs)),\n );\n\n let total = 0;\n for (const chunk of chunks) total += chunk.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nif (import.meta.vitest) {\n const { beforeAll, describe, test, expect, vi } = import.meta.vitest;\n const { calculateCid } = await import(\"@parity/bulletin-sdk\");\n\n describe(\"executeQuery\", () => {\n const testData = new Uint8Array([1, 2, 3]);\n\n test(\"delegates to the strategy's lookup function\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n // Use a real raw-codec CID so the codec check passes through\n // without triggering reassembly.\n const rawCid = (await calculateCid(testData)).toString();\n const result = await executeQuery(strategy, rawCid);\n expect(result).toBe(testData);\n expect(lookup).toHaveBeenCalledWith(rawCid, undefined);\n });\n\n test(\"forwards lookupTimeoutMs to the strategy\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const rawCid = (await calculateCid(testData)).toString();\n await executeQuery(strategy, rawCid, { lookupTimeoutMs: 5000 });\n expect(lookup).toHaveBeenCalledWith(rawCid, 5000);\n });\n\n test(\"returns raw bytes directly for raw-codec CIDs (no reassembly)\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const rawCid = (await calculateCid(testData)).toString();\n const result = await executeQuery(strategy, rawCid);\n expect(result).toBe(testData);\n // Single lookup, no recursion.\n expect(lookup).toHaveBeenCalledTimes(1);\n });\n\n test(\"noReassemble: true short-circuits even for DAG-PB CIDs\", async () => {\n // Manufacture a DAG-PB CID; we don't need the bytes to actually\n // be a valid manifest because we're skipping the parse step.\n const fakeManifestBytes = new Uint8Array([10, 20, 30]);\n const dagPbCid = (await calculateCid(fakeManifestBytes, /* dag-pb */ 0x70)).toString();\n const lookup = vi.fn().mockResolvedValue(fakeManifestBytes);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, dagPbCid, { noReassemble: true });\n expect(result).toBe(fakeManifestBytes);\n expect(lookup).toHaveBeenCalledTimes(1);\n });\n\n describe(\"DAG-PB reassembly\", () => {\n // Build a real manifest: two raw chunks, dag-pb root.\n const chunkA = new Uint8Array([0xaa, 0xaa, 0xaa]);\n const chunkB = new Uint8Array([0xbb, 0xbb]);\n let chunkACid: string;\n let chunkBCid: string;\n let manifestBytes: Uint8Array;\n let manifestCid: string;\n\n beforeAll(async () => {\n const { UnixFsDagBuilder: Builder } = await import(\"@parity/bulletin-sdk\");\n chunkACid = (await calculateCid(chunkA)).toString();\n chunkBCid = (await calculateCid(chunkB)).toString();\n const cidA = await calculateCid(chunkA);\n const cidB = await calculateCid(chunkB);\n const manifest = await new Builder().build([\n { data: chunkA, cid: cidA, index: 0, totalChunks: 2 },\n { data: chunkB, cid: cidB, index: 1, totalChunks: 2 },\n ]);\n manifestBytes = manifest.dagBytes;\n manifestCid = manifest.rootCid.toString();\n });\n\n test(\"recursively fetches chunks and concatenates\", async () => {\n const lookup = vi.fn(async (cid: string) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(`unexpected lookup: ${cid}`);\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, manifestCid);\n expect(result).toEqual(new Uint8Array([0xaa, 0xaa, 0xaa, 0xbb, 0xbb]));\n // 1 manifest + 2 chunks = 3 lookups.\n expect(lookup).toHaveBeenCalledTimes(3);\n });\n\n test(\"forwards lookupTimeoutMs to every chunk lookup\", async () => {\n const lookup = vi.fn(async (cid: string, _timeoutMs?: number) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(\"boom\");\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n await executeQuery(strategy, manifestCid, { lookupTimeoutMs: 7777 });\n for (const call of lookup.mock.calls) {\n expect(call[1]).toBe(7777);\n }\n });\n\n test(\"preserves chunk order from the manifest\", async () => {\n const lookup = vi.fn(async (cid: string) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(\"boom\");\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, manifestCid);\n // Order matters: chunkA bytes must come before chunkB bytes\n // even though both are fetched in parallel.\n expect(Array.from(result.slice(0, 3))).toEqual([0xaa, 0xaa, 0xaa]);\n expect(Array.from(result.slice(3))).toEqual([0xbb, 0xbb]);\n });\n });\n });\n}\n","/**\n * Chain-storage verification for stored CIDs.\n *\n * The bulletin chain doesn't store content bytes on-chain — `TransactionStorage\n * .Transactions[block]` holds metadata only (`{ chunk_root, content_hash,\n * hashing, cid_codec, size, block_chunks }`), and the bytes themselves live\n * in IPFS. So \"read by CID from chain\" isn't possible. What *is* possible is\n * proving that a given CID was stored: parse the CID's digest + multihash\n * code, then look it up in `TransactionStorage.Transactions` and confirm a\n * matching `content_hash` + `hashing`.\n *\n * Common use case: just-after-upload UX — `await client.store(data).send()`\n * gives you back `{ cid, blockNumber, extrinsicIndex? }`, and a follow-up\n * `verifyOnChain(api, cid, { block: blockNumber })` confirms the metadata\n * landed where expected.\n */\nimport { CID } from \"multiformats/cid\";\n\nimport { HashAlgorithm } from \"./cid.js\";\nimport { BulletinCidError } from \"./errors.js\";\nimport type { BulletinApi } from \"./types.js\";\n\n/**\n * Match a multihash code in a CID against the chain's `hashing` enum value.\n */\nconst HASH_CODE_TO_ENUM_TYPE: Record<number, \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\"> = {\n [HashAlgorithm.Blake2b256]: \"Blake2b256\",\n [HashAlgorithm.Sha2_256]: \"Sha2_256\",\n [HashAlgorithm.Keccak256]: \"Keccak256\",\n};\n\n/** A single matched entry from `TransactionStorage.Transactions`. */\nexport interface ChainStoredEntry {\n /** Block number where the transaction was included. */\n block: number;\n /** Index of the entry within the block's transactions array. */\n index: number;\n /** Size of the stored data in bytes (from chain metadata). */\n size: number;\n /** Number of chunks (1 for unchunked data, >1 for chunked + manifest). */\n blockChunks: number;\n}\n\n/**\n * Verification options for {@link verifyOnChain}.\n */\nexport interface VerifyOnChainOptions {\n /**\n * Block number to look up. Pass the `blockNumber` returned from a prior\n * `store(...).send()` for an O(1) lookup.\n *\n * If omitted, throws — full-chain scans are not supported because\n * `RetentionPeriod` can be many days of blocks. Use `getEntries()` on\n * `api.query.TransactionStorage.Transactions` directly if you need that.\n */\n block: number;\n /**\n * Optional: index within the block. When provided, narrows verification\n * to that exact slot. Useful when re-checking a known `(block, index)`\n * tuple from an earlier receipt.\n */\n index?: number;\n}\n\n/**\n * Verify that a CID is recorded in the bulletin chain's `Transactions`\n * storage at the given block.\n *\n * Returns the matched entry (with block + index) when the CID's content\n * hash and hashing algorithm both match a `Transactions[block]` entry.\n * Returns `null` when no match is found at that block.\n *\n * @param api - Typed bulletin API.\n * @param cid - CIDv1 string to look up.\n * @param options - Verification target (block number, optional index).\n *\n * @example\n * ```ts\n * const receipt = await client.store(data).send();\n * if (receipt.blockNumber !== undefined) {\n * const entry = await verifyOnChain(client.api, receipt.cid!.toString(), {\n * block: receipt.blockNumber,\n * index: receipt.extrinsicIndex,\n * });\n * if (!entry) console.warn(\"CID not found in expected block — chain reorg?\");\n * }\n * ```\n */\nexport async function verifyOnChain(\n api: BulletinApi,\n cid: string,\n options: VerifyOnChainOptions,\n): Promise<ChainStoredEntry | null> {\n const parsed = parseCidForVerify(cid);\n\n const queryFn = (api as unknown as TransactionsQueryApi).query?.TransactionStorage?.Transactions\n ?.getValue;\n if (!queryFn) {\n throw new Error(\n \"Bulletin API does not expose query.TransactionStorage.Transactions — \" +\n \"the typed API may be incomplete or the runtime version doesn't match the descriptor.\",\n );\n }\n\n const entries = await queryFn(options.block);\n if (!entries || entries.length === 0) return null;\n\n // When an explicit index is provided, check that slot directly — no\n // reason to walk the full array just to skip everything else.\n if (options.index !== undefined) {\n const entry = entries[options.index];\n if (entry && matchesEntry(entry, parsed)) {\n return {\n block: options.block,\n index: options.index,\n size: entry.size,\n blockChunks: entry.block_chunks,\n };\n }\n return null;\n }\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!;\n if (matchesEntry(entry, parsed)) {\n return {\n block: options.block,\n index: i,\n size: entry.size,\n blockChunks: entry.block_chunks,\n };\n }\n }\n\n return null;\n}\n\ninterface ParsedCid {\n digest: Uint8Array;\n hashType: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\";\n}\n\n/**\n * Hand-rolled mirror of `TransactionStorage.Transactions[block][n]` — the\n * shape PAPI returns at runtime when you call `query.TransactionStorage\n * .Transactions.getValue(block)`. Defined here (rather than derived from\n * `BulletinTypedApi`) because the typed API surfaces these values through\n * `Anonymize<I…>` codec aliases that aren't ergonomic to inline.\n *\n * **If the bulletin runtime changes the entry shape, update this here too.**\n * Source of truth: `TransactionInfo` in\n * `packages/descriptors/chains/bulletin/generated/dist/common-types.d.ts`\n * (look for `chunk_root: FixedSizeBinary<32>` to anchor it). When the\n * descriptor regenerates and the fields shift, this interface, the\n * `cid_codec`/`hashing` matching in `matchesEntry`, and the\n * `HASH_CODE_TO_ENUM_TYPE` map above all need to be re-validated together.\n *\n * `Uint8Array | { asBytes(): Uint8Array }` covers both the raw and Binary-\n * wrapped shapes the codec can return depending on configuration.\n */\ninterface ChainEntry {\n chunk_root: { asBytes(): Uint8Array } | Uint8Array;\n content_hash: { asBytes(): Uint8Array } | Uint8Array;\n hashing: { type: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\" };\n cid_codec: bigint;\n size: number;\n block_chunks: number;\n}\n\ninterface TransactionsQueryApi {\n query?: {\n TransactionStorage?: {\n Transactions?: {\n getValue: (block: number) => Promise<ChainEntry[] | undefined>;\n };\n };\n };\n}\n\nfunction parseCidForVerify(cid: string): ParsedCid {\n let parsed;\n try {\n parsed = CID.parse(cid);\n } catch {\n throw new BulletinCidError(`Invalid CID: ${cid}`, cid);\n }\n if (parsed.version !== 1) {\n throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);\n }\n const hashType = HASH_CODE_TO_ENUM_TYPE[parsed.multihash.code];\n if (!hashType) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}`,\n cid,\n );\n }\n return { digest: parsed.multihash.digest, hashType };\n}\n\nfunction matchesEntry(entry: ChainEntry, target: ParsedCid): boolean {\n if (entry.hashing.type !== target.hashType) return false;\n const onChainBytes =\n entry.content_hash instanceof Uint8Array\n ? entry.content_hash\n : entry.content_hash.asBytes();\n if (onChainBytes.length !== target.digest.length) return false;\n for (let i = 0; i < onChainBytes.length; i++) {\n if (onChainBytes[i] !== target.digest[i]) return false;\n }\n return true;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function makeMockApi(getValue: (block: number) => Promise<ChainEntry[] | undefined>) {\n return {\n query: {\n TransactionStorage: {\n Transactions: { getValue },\n },\n },\n } as unknown as BulletinApi;\n }\n\n function makeEntry(\n digest: Uint8Array,\n hashType: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\" = \"Blake2b256\",\n size = 100,\n blockChunks = 1,\n ): ChainEntry {\n return {\n chunk_root: digest,\n content_hash: digest,\n hashing: { type: hashType },\n cid_codec: 0x55n,\n size,\n block_chunks: blockChunks,\n };\n }\n\n // Build a real CIDv1 (blake2b-256, raw) we can verify against\n async function makeCidWithDigest(digest: Uint8Array, hashCode = 0xb220): Promise<string> {\n const Digest = await import(\"multiformats/hashes/digest\");\n return CID.createV1(0x55, Digest.create(hashCode, digest)).toString();\n }\n\n describe(\"verifyOnChain\", () => {\n test(\"returns entry when CID matches at given block\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(digest)]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toEqual({ block: 100, index: 0, size: 100, blockChunks: 1 });\n });\n\n test(\"returns null when block has no entries\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const api = makeMockApi(vi.fn().mockResolvedValue(undefined));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"returns null when block has entries but none match\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const otherDigest = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(otherDigest)]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"returns null when hashing algorithm differs\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n // CID uses blake2b-256, chain entry says sha2-256 with same digest bytes\n const cid = await makeCidWithDigest(digest, 0xb220);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(digest, \"Sha2_256\")]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"finds match at correct index when multiple entries exist\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result?.index).toBe(2);\n });\n\n test(\"respects explicit index option\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n // Target is at index 2, but caller says index 0 — should not match\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100, index: 0 });\n expect(result).toBeNull();\n });\n\n test(\"returns the entry when explicit index matches\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100, index: 2 });\n expect(result?.index).toBe(2);\n });\n\n test(\"throws BulletinCidError on invalid CID\", async () => {\n const api = makeMockApi(vi.fn());\n await expect(verifyOnChain(api, \"not-a-cid\", { block: 1 })).rejects.toThrow(\n BulletinCidError,\n );\n });\n\n test(\"throws when api lacks the expected query path\", async () => {\n const api = {} as BulletinApi;\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n await expect(verifyOnChain(api, cid, { block: 1 })).rejects.toThrow(\n /does not expose query/,\n );\n });\n\n test(\"handles content_hash as a Binary-like wrapper\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const wrapper = { asBytes: () => digest };\n const entry: ChainEntry = {\n chunk_root: wrapper,\n content_hash: wrapper,\n hashing: { type: \"Blake2b256\" },\n cid_codec: 0x55n,\n size: 50,\n block_chunks: 1,\n };\n const api = makeMockApi(vi.fn().mockResolvedValue([entry]));\n const result = await verifyOnChain(api, cid, { block: 1 });\n expect(result).toEqual({ block: 1, index: 0, size: 50, blockChunks: 1 });\n });\n\n test(\"passes the block number to the storage call\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const getValue = vi.fn().mockResolvedValue([makeEntry(digest)]);\n const api = makeMockApi(getValue);\n await verifyOnChain(api, cid, { block: 42 });\n expect(getValue).toHaveBeenCalledWith(42);\n });\n });\n}\n","import {\n AsyncBulletinClient,\n type AuthCallBuilder,\n type BulletinTypedApi,\n type CallBuilder,\n type ClientConfig,\n type StoreBuilder,\n type SubmitFn,\n} from \"@parity/bulletin-sdk\";\nimport { createChainClient, getChainAPI } from \"@parity/product-sdk-chain-client\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport type { PolkadotClient, PolkadotSigner } from \"polkadot-api\";\n\nimport { checkAuthorization } from \"./authorization.js\";\nimport type { BulletinChain, BulletinEnvironment } from \"./networks.js\";\nimport { executeQuery } from \"./query.js\";\nimport { resolveQueryStrategy, type QueryStrategy } from \"./resolve-query.js\";\nimport type { AuthorizationStatus, BulletinApi, QueryOptions } from \"./types.js\";\nimport { verifyOnChain, type ChainStoredEntry, type VerifyOnChainOptions } from \"./verify.js\";\n\nconst log = createLogger(\"bulletin\");\n\n/**\n * Options for {@link BulletinClient.create}.\n *\n * One of two construction shapes is supported:\n *\n * - **Environment shorthand** — pass an `environment` string keyed by\n * {@link BulletinChain}. Wires up the chain-client automatically.\n * - **Explicit network** — pass `genesisHash` and `descriptor` directly\n * (e.g., spread from a {@link BulletinChain} entry, or supply custom\n * values for a private chain).\n */\nexport type CreateBulletinClientOptions =\n | (CreateBulletinClientCommon & { environment: BulletinEnvironment })\n | (CreateBulletinClientCommon & {\n genesisHash: `0x${string}`;\n descriptor: (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"];\n });\n\ninterface CreateBulletinClientCommon {\n /** Signer for transaction submission. Required — every store needs a signer. */\n signer: PolkadotSigner;\n /** Optional config forwarded to {@link AsyncBulletinClient}. */\n config?: Partial<ClientConfig>;\n}\n\n/**\n * Ergonomic entry point for Bulletin Chain operations.\n *\n * Wraps {@link AsyncBulletinClient} from `@parity/bulletin-sdk` (which handles\n * chunking, DAG-PB manifests, CID calculation, and progress events) and adds:\n *\n * - **Network presets** via {@link BulletinClient.create} and {@link BulletinChain}.\n * - **Read helpers** ({@link fetchBytes}, {@link fetchJson}) routed through\n * the host's preimage subscription — upstream is upload-only and the SDK\n * is container-only by design (no public-gateway fetches).\n * - **Pre-flight authorization check** ({@link checkAuthorization}) for\n * friendlier UX before submitting a store.\n *\n * For uploads, mirror upstream's fluent builders:\n *\n * ```ts\n * const client = await BulletinClient.create({ environment: \"paseo\", signer });\n * const result = await client.store(data).send();\n * ```\n *\n * For chunked uploads with progress:\n *\n * ```ts\n * const result = await client\n * .store(largeFile)\n * .withChunkSize(1 << 20)\n * .withCallback((evt) => console.log(evt))\n * .send();\n * ```\n */\nexport class BulletinClient {\n /** Underlying upstream client — exposed for power users. */\n readonly inner: AsyncBulletinClient;\n /** Typed Bulletin Chain API. */\n readonly api: BulletinApi;\n\n /** Lazy-resolved host-preimage query strategy, cached for the client lifetime. */\n private queryStrategyPromise: Promise<QueryStrategy> | null = null;\n\n /** Constructed via {@link create} or {@link from}. */\n private constructor(inner: AsyncBulletinClient, api: BulletinApi) {\n this.inner = inner;\n this.api = api;\n }\n\n /** Resolve and cache the host query strategy on first use. */\n private resolveQuery(): Promise<QueryStrategy> {\n if (!this.queryStrategyPromise) {\n this.queryStrategyPromise = resolveQueryStrategy();\n }\n return this.queryStrategyPromise;\n }\n\n /**\n * Create a client from an environment shorthand or an explicit network.\n *\n * Environment form uses our `getChainAPI(env)` to resolve the typed API.\n * Explicit form skips the environment lookup and lets you pass any\n * genesis/descriptor combo.\n *\n * @example\n * ```ts\n * // Shorthand\n * const client = await BulletinClient.create({ environment: \"paseo\", signer });\n *\n * // Explicit (custom network)\n * const client = await BulletinClient.create({\n * ...BulletinChain.paseo,\n * signer,\n * config: { defaultChunkSize: 1 << 20 },\n * });\n * ```\n */\n static async create(options: CreateBulletinClientOptions): Promise<BulletinClient> {\n if (\"environment\" in options) {\n const chain = await getChainAPI(options.environment);\n const inner = new AsyncBulletinClient(\n chain.bulletin as BulletinTypedApi,\n options.signer,\n chain.raw.bulletin.submit as SubmitFn,\n options.config,\n () => chain.destroy(),\n );\n log.info(\"BulletinClient created (environment shorthand)\", {\n environment: options.environment,\n });\n return new BulletinClient(inner, chain.bulletin);\n }\n\n // Explicit form — caller owns the descriptor choice. We still need a\n // PolkadotClient to feed AsyncBulletinClient. Going through\n // chain-client keeps connection management consistent across the SDK.\n const { genesisHash, descriptor, signer, config } = options;\n // Catch the obvious foot-gun where caller mixes a genesis from one\n // network with a descriptor from another — the connection would\n // succeed but typed calls would silently target the wrong chain.\n // The descriptor's own `.genesis` field is the on-chain truth; the\n // user-supplied `genesisHash` is informational today (createChainClient\n // doesn't use it because host routes connections) but kept on the\n // option shape for future RPC-direct paths.\n if (descriptor.genesis && genesisHash.toLowerCase() !== descriptor.genesis.toLowerCase()) {\n throw new Error(\n `BulletinClient.create: genesisHash (${genesisHash}) does not match descriptor.genesis (${descriptor.genesis}). These must refer to the same network — check that you're pairing the right descriptor with the right genesis hash.`,\n );\n }\n const chain = await createChainClient({\n chains: { bulletin: descriptor },\n rpcs: { bulletin: [] },\n });\n const inner = new AsyncBulletinClient(\n chain.bulletin as BulletinTypedApi,\n signer,\n chain.raw.bulletin.submit as SubmitFn,\n config,\n () => chain.destroy(),\n );\n log.info(\"BulletinClient created (explicit network)\");\n return new BulletinClient(inner, chain.bulletin);\n }\n\n /**\n * Construct from a pre-built `AsyncBulletinClient` and PAPI typed API.\n *\n * Use this when you already own the connection lifecycle (BYOD setups,\n * tests). The caller is responsible for calling `papiClient.destroy()`\n * — this client's {@link destroy} only tears down the upstream's\n * `onDestroy` hook.\n */\n static from(inner: AsyncBulletinClient, api: BulletinApi): BulletinClient {\n return new BulletinClient(inner, api);\n }\n\n // ─── Upload + authorization (forwarded to upstream) ────────────────\n\n /** Build a store transaction. See upstream `StoreBuilder` for chained options. */\n store(data: Uint8Array): StoreBuilder {\n return this.inner.store(data);\n }\n\n /** Authorize an account to store data on the chain (sudo required on most networks). */\n authorizeAccount(who: string, transactions: number, bytes: bigint): AuthCallBuilder {\n return this.inner.authorizeAccount(who, transactions, bytes);\n }\n\n /** Authorize content storage by hash (anyone can store; no fees). */\n authorizePreimage(contentHash: Uint8Array, maxSize: bigint): AuthCallBuilder {\n return this.inner.authorizePreimage(contentHash, maxSize);\n }\n\n /** Renew a stored transaction by block + index. */\n renew(block: number, index: number): CallBuilder {\n return this.inner.renew(block, index);\n }\n\n /** Estimate the authorization (transactions + bytes) needed for `dataSize`. */\n estimateAuthorization(dataSize: number): { transactions: number; bytes: number } {\n return this.inner.estimateAuthorization(dataSize);\n }\n\n // ─── Read side (our own helpers) ───────────────────────────────────\n\n /**\n * Fetch raw bytes for a CID via the host's preimage lookup.\n *\n * Container-only — outside a Polkadot Browser / Desktop host this\n * throws {@link BulletinHostUnavailableError}. The chain stores\n * content metadata (`content_hash`, size, codec) but the bytes\n * themselves are surfaced through the host's preimage subscription.\n *\n * Use {@link verifyOnChain} if you only need to confirm a CID was\n * recorded on-chain (no byte fetch).\n */\n async fetchBytes(cid: string, options?: QueryOptions): Promise<Uint8Array> {\n const strategy = await this.resolveQuery();\n return executeQuery(strategy, cid, options);\n }\n\n /** Fetch and parse JSON for a CID. */\n async fetchJson<T>(cid: string, options?: QueryOptions): Promise<T> {\n const bytes = await this.fetchBytes(cid, options);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n }\n\n /** Pre-flight: check whether `address` can store on the bulletin chain. */\n async checkAuthorization(address: string): Promise<AuthorizationStatus> {\n return checkAuthorization(this.api, address);\n }\n\n /**\n * Verify that a CID was recorded on-chain at the given block.\n *\n * Common pattern: pass `blockNumber` (and optionally `extrinsicIndex`)\n * from a `store(...).send()` receipt to confirm the upload landed.\n * See {@link verifyOnChain} for details.\n */\n async verifyOnChain(\n cid: string,\n options: VerifyOnChainOptions,\n ): Promise<ChainStoredEntry | null> {\n return verifyOnChain(this.api, cid, options);\n }\n\n /** Tear down the underlying connection. */\n async destroy(): Promise<void> {\n await this.inner.destroy();\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n describe(\"BulletinClient.from\", () => {\n test(\"constructs with given inner and api\", () => {\n const inner = {\n destroy: vi.fn().mockResolvedValue(undefined),\n } as unknown as AsyncBulletinClient;\n const api = {} as BulletinApi;\n const client = BulletinClient.from(inner, api);\n expect(client.inner).toBe(inner);\n expect(client.api).toBe(api);\n });\n\n test(\"destroy delegates to upstream\", async () => {\n const destroy = vi.fn().mockResolvedValue(undefined);\n const inner = { destroy } as unknown as AsyncBulletinClient;\n const client = BulletinClient.from(inner, {} as BulletinApi);\n await client.destroy();\n expect(destroy).toHaveBeenCalledOnce();\n });\n\n test(\"store delegates to inner\", () => {\n const builder = {} as StoreBuilder;\n const inner = {\n store: vi.fn().mockReturnValue(builder),\n } as unknown as AsyncBulletinClient;\n const client = BulletinClient.from(inner, {} as BulletinApi);\n const data = new Uint8Array([1, 2, 3]);\n expect(client.store(data)).toBe(builder);\n expect(inner.store).toHaveBeenCalledWith(data);\n });\n });\n\n describe(\"BulletinClient.create (BYOD genesis assertion)\", () => {\n // Stand-in descriptor with a known genesis. The full PAPI descriptor\n // type is a `ChainDefinition` with a deep type-level shape; the cast\n // below is fine for the assertion test because we never actually\n // reach createChainClient.\n const stubDescriptor = (\n genesis: `0x${string}`,\n ): (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"] =>\n ({ genesis }) as unknown as (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"];\n\n const realPaseo =\n \"0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea\" as `0x${string}`;\n\n test(\"throws when genesisHash and descriptor.genesis disagree\", async () => {\n await expect(\n BulletinClient.create({\n genesisHash:\n \"0x0000000000000000000000000000000000000000000000000000000000000001\",\n descriptor: stubDescriptor(realPaseo),\n signer: {} as PolkadotSigner,\n }),\n ).rejects.toThrow(/does not match descriptor\\.genesis/i);\n });\n });\n}\n","/**\n * Known Bulletin Chain networks.\n *\n * Pairs each environment with the genesis hash and the PAPI descriptor needed\n * to construct an `AsyncBulletinClient`. Re-uses the descriptor exported by\n * `@parity/product-sdk-descriptors/bulletin` — the bulletin descriptor is the\n * same across all environments today, so the difference between entries is\n * the genesis hash (and, downstream, the chain RPC URL).\n */\nimport { bulletin as bulletinDescriptor } from \"@parity/product-sdk-descriptors/bulletin\";\n\nexport interface BulletinNetwork {\n /** Genesis hash of the bulletin chain on this environment. */\n genesisHash: `0x${string}`;\n /** PAPI descriptor for typed API access. */\n descriptor: typeof bulletinDescriptor;\n}\n\n/**\n * Bulletin Chain network presets.\n *\n * Use these with {@link BulletinClient.create} when you want to be explicit\n * about the network rather than passing an environment string. Reads go\n * through the host's preimage subscription (container-only); no gateway\n * URL is configured per network.\n */\nexport const BulletinChain = {\n paseo: {\n genesisHash: \"0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea\",\n descriptor: bulletinDescriptor,\n },\n} as const satisfies Record<string, BulletinNetwork>;\n\n/** Network keys with built-in presets in {@link BulletinChain}. */\nexport type BulletinEnvironment = keyof typeof BulletinChain;\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"BulletinChain\", () => {\n test(\"paseo has a valid genesis hash\", () => {\n expect(BulletinChain.paseo.genesisHash).toMatch(/^0x[a-f0-9]{64}$/);\n });\n\n test(\"paseo descriptor has matching genesis\", () => {\n expect(BulletinChain.paseo.descriptor.genesis).toBe(BulletinChain.paseo.genesisHash);\n });\n });\n}\n","import type { PolkadotSigner } from \"polkadot-api\";\n\n/**\n * Build a `PolkadotSigner` whose underlying signer is resolved on every call.\n *\n * `AsyncBulletinClient` takes a fixed `PolkadotSigner` at construction, but\n * apps often build the bulletin client before any account is selected. This\n * wrapper defers signer resolution: each call to `signTx` / `signBytes`\n * invokes `getSigner()` and forwards to the result. If the getter returns\n * `null`, calls throw with a clear message.\n *\n * The `publicKey` field is *also* resolved lazily — accessing it before a\n * signer is available throws. This means callers that read `publicKey`\n * eagerly will fail fast with the same error rather than seeing a stale\n * key from a previously-selected account.\n *\n * Account changes between calls are picked up automatically: each sign\n * resolves the current signer.\n */\nexport function createLazySigner(\n getSigner: () => PolkadotSigner | null,\n onMissing = \"No signer available — connect a wallet and select an account first.\",\n): PolkadotSigner {\n const resolve = (): PolkadotSigner => {\n const inner = getSigner();\n if (!inner) throw new Error(onMissing);\n return inner;\n };\n\n // `async` on the methods is deliberate: it converts a \"no signer\" throw\n // from `resolve()` into a rejected Promise. PolkadotSigner.signTx /\n // signBytes are typed as returning Promises, and consumers expect a\n // rejection rather than a synchronous escape on the failure path.\n const lazy: PolkadotSigner = {\n get publicKey() {\n return resolve().publicKey;\n },\n signTx: async (...args: Parameters<PolkadotSigner[\"signTx\"]>) => resolve().signTx(...args),\n signBytes: async (...args: Parameters<PolkadotSigner[\"signBytes\"]>) =>\n resolve().signBytes(...args),\n };\n return lazy;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function makeMockSigner(label: string): PolkadotSigner {\n return {\n publicKey: new TextEncoder().encode(label),\n signTx: vi.fn().mockResolvedValue(new Uint8Array([1])),\n signBytes: vi.fn().mockResolvedValue(new Uint8Array([2])),\n };\n }\n\n describe(\"createLazySigner\", () => {\n test(\"publicKey throws when getter returns null\", () => {\n const lazy = createLazySigner(() => null);\n expect(() => lazy.publicKey).toThrow(\"No signer available\");\n });\n\n test(\"publicKey resolves through getter when signer is available\", () => {\n const inner = makeMockSigner(\"alice\");\n const lazy = createLazySigner(() => inner);\n expect(lazy.publicKey).toBe(inner.publicKey);\n });\n\n test(\"signTx throws when getter returns null\", async () => {\n const lazy = createLazySigner(() => null);\n await expect(lazy.signTx(new Uint8Array(), {}, new Uint8Array(), 0)).rejects.toThrow(\n \"No signer available\",\n );\n });\n\n test(\"signTx forwards to current signer\", async () => {\n const inner = makeMockSigner(\"alice\");\n const lazy = createLazySigner(() => inner);\n const callData = new Uint8Array([0xaa, 0xbb]);\n const signedExtensions = {};\n const metadata = new Uint8Array([0xcc]);\n const atBlock = 42;\n const result = await lazy.signTx(callData, signedExtensions, metadata, atBlock);\n expect(inner.signTx).toHaveBeenCalledWith(\n callData,\n signedExtensions,\n metadata,\n atBlock,\n );\n expect(result).toEqual(new Uint8Array([1]));\n });\n\n test(\"signBytes forwards to current signer\", async () => {\n const inner = makeMockSigner(\"bob\");\n const lazy = createLazySigner(() => inner);\n const result = await lazy.signBytes(new Uint8Array([9]));\n expect(inner.signBytes).toHaveBeenCalledWith(new Uint8Array([9]));\n expect(result).toEqual(new Uint8Array([2]));\n });\n\n test(\"picks up account changes between calls\", () => {\n let active: PolkadotSigner | null = makeMockSigner(\"first\");\n const lazy = createLazySigner(() => active);\n expect(lazy.publicKey).toEqual(new TextEncoder().encode(\"first\"));\n active = makeMockSigner(\"second\");\n expect(lazy.publicKey).toEqual(new TextEncoder().encode(\"second\"));\n });\n\n test(\"custom error message\", () => {\n const lazy = createLazySigner(() => null, \"select an account first\");\n expect(() => lazy.publicKey).toThrow(\"select an account first\");\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/authorization.ts","../src/cid.ts","../src/resolve-query.ts","../src/query.ts","../src/verify.ts","../src/client.ts","../src/networks.ts","../src/lazy-signer.ts"],"names":["log","createLogger","CidCodec","CID","chain","inner","bulletinDescriptor"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bulletin error types.\n *\n * Two error families coexist:\n *\n * 1. **Upstream SDK errors** — `BulletinError` and `ErrorCode` from\n * `@parity/bulletin-sdk` cover upload/store/authorization failures\n * surfaced by `AsyncBulletinClient`. Each carries `code`, `retryable`,\n * and `recoveryHint`.\n * 2. **Read-side errors** declared here — host preimage availability /\n * lookup timeouts / interrupts, plus CID format problems, surfaced by\n * our retrieval helpers (`fetchBytes`, `fetchJson`, `verifyOnChain`).\n *\n * Catch upstream errors with `instanceof BulletinError`. Catch our read-side\n * errors with `instanceof ProductBulletinError` (or the specific subclass).\n */\nexport { BulletinError, ErrorCode } from \"@parity/bulletin-sdk\";\n\n/**\n * Base class for read-side errors raised by `@parity/product-sdk-bulletin`.\n *\n * Distinct from upstream `BulletinError` which covers upload/store failures.\n */\nexport class ProductBulletinError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ProductBulletinError\";\n }\n}\n\n/**\n * The host preimage API is unavailable.\n *\n * Thrown when bulletin operations require the host container but it's not\n * available. This typically means the SDK is running outside Polkadot\n * Browser / Desktop. The bulletin SDK is container-only by design — see\n * the README for the rationale.\n */\nexport class BulletinHostUnavailableError extends ProductBulletinError {\n constructor(operation: \"upload\" | \"query\") {\n super(\n `Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`,\n );\n this.name = \"BulletinHostUnavailableError\";\n }\n}\n\n/**\n * The host preimage lookup timed out.\n *\n * The host was unable to retrieve the requested content within the timeout\n * period. The content may still become available later.\n */\nexport class BulletinLookupTimeoutError extends ProductBulletinError {\n /** The CID that was being looked up. */\n readonly cid: string;\n /** The timeout duration in milliseconds. */\n readonly timeoutMs: number;\n\n constructor(cid: string, timeoutMs: number) {\n super(`Host preimage lookup timed out after ${timeoutMs}ms for CID: ${cid}`);\n this.name = \"BulletinLookupTimeoutError\";\n this.cid = cid;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * The host interrupted the preimage lookup.\n *\n * The host terminated the lookup subscription, typically after repeated\n * failures or when the host determines the content is unavailable.\n */\nexport class BulletinLookupInterruptedError extends ProductBulletinError {\n /** The CID that was being looked up. */\n readonly cid: string;\n\n constructor(cid: string) {\n super(`Host preimage lookup was interrupted for CID: ${cid}`);\n this.name = \"BulletinLookupInterruptedError\";\n this.cid = cid;\n }\n}\n\n/**\n * Invalid CID format or version.\n */\nexport class BulletinCidError extends ProductBulletinError {\n /** The invalid CID string, if available. */\n readonly cid?: string;\n\n constructor(message: string, cid?: string) {\n super(message);\n this.name = \"BulletinCidError\";\n this.cid = cid;\n }\n}\n\n/**\n * Failed to check authorization status for an account.\n *\n * Wraps RPC or query errors that occur when checking if an account\n * is authorized to store data on the Bulletin Chain.\n */\nexport class BulletinAuthorizationError extends ProductBulletinError {\n /** The address that was being checked. */\n readonly address: string;\n\n constructor(address: string, cause?: unknown) {\n super(`Failed to check authorization for ${address}`, { cause });\n this.name = \"BulletinAuthorizationError\";\n this.address = address;\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"ProductBulletinError hierarchy\", () => {\n test(\"ProductBulletinError extends Error\", () => {\n const err = new ProductBulletinError(\"test\");\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"ProductBulletinError\");\n });\n\n test(\"BulletinCidError\", () => {\n const err = new BulletinCidError(\"bad\", \"Qmabc\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"Qmabc\");\n });\n\n test(\"BulletinHostUnavailableError\", () => {\n const err = new BulletinHostUnavailableError(\"query\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.message).toContain(\"query\");\n expect(err.message).toContain(\"Host preimage API unavailable\");\n });\n\n test(\"BulletinLookupTimeoutError\", () => {\n const err = new BulletinLookupTimeoutError(\"bafyabc123\", 30000);\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"bafyabc123\");\n expect(err.timeoutMs).toBe(30000);\n expect(err.message).toContain(\"30000ms\");\n });\n\n test(\"BulletinLookupInterruptedError\", () => {\n const err = new BulletinLookupInterruptedError(\"bafyabc123\");\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.cid).toBe(\"bafyabc123\");\n expect(err.message).toContain(\"interrupted\");\n });\n\n test(\"BulletinAuthorizationError carries cause\", () => {\n const cause = new Error(\"RPC down\");\n const err = new BulletinAuthorizationError(\"5GrwvaEF...\", cause);\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect(err.address).toBe(\"5GrwvaEF...\");\n expect(err.cause).toBe(cause);\n });\n });\n}\n","import { createLogger } from \"@parity/product-sdk-logger\";\nimport { submitAndWatch, type TxStatus, type WaitFor } from \"@parity/product-sdk-tx\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { Enum } from \"polkadot-api\";\n\nimport { BulletinAuthorizationError, ProductBulletinError } from \"./errors.js\";\nimport type { AuthorizationStatus, BulletinApi } from \"./types.js\";\n\nconst log = createLogger(\"bulletin\");\n\nconst NOT_AUTHORIZED: AuthorizationStatus = Object.freeze({\n authorized: false,\n remainingTransactions: 0,\n remainingBytes: 0n,\n expiration: 0,\n});\n\n/**\n * Check whether an account is authorized to store data on the Bulletin Chain.\n *\n * Queries `TransactionStorage.Authorizations` for the given address and returns\n * the raw authorization quota. Use this as a pre-flight check before calling\n * {@link upload} to provide clear UX (\"not authorized\" / \"insufficient quota\")\n * instead of letting the transaction fail mid-execution.\n *\n * The expiration block number is returned as-is — the chain enforces expiration\n * at submission time, so callers can optionally compare against the current\n * block for display purposes.\n *\n * @param api - Typed Bulletin Chain API instance.\n * @param address - SS58-encoded account address to check.\n * @returns Authorization status with remaining quota.\n *\n * @example\n * ```ts\n * import { checkAuthorization } from \"@parity/product-sdk-bulletin\";\n *\n * const auth = await checkAuthorization(api, address);\n * if (!auth.authorized) {\n * console.error(\"Account is not authorized for bulletin storage\");\n * } else if (auth.remainingBytes < BigInt(fileBytes.length)) {\n * console.error(`Insufficient quota: ${auth.remainingBytes} bytes remaining`);\n * }\n * ```\n *\n * @see {@link BulletinClient.checkAuthorization} for the client method equivalent.\n */\nexport async function checkAuthorization(\n api: BulletinApi,\n address: string,\n): Promise<AuthorizationStatus> {\n let auth;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth = await (api as any).query.TransactionStorage.Authorizations.getValue(\n Enum(\"Account\", address),\n );\n } catch (error) {\n log.error(\"checkAuthorization: query failed\", { address, error });\n throw new BulletinAuthorizationError(address, error);\n }\n\n if (!auth) {\n log.debug(\"checkAuthorization: no authorization found\", { address });\n return NOT_AUTHORIZED;\n }\n\n // After polkadot-bulletin-chain PR #448 (e543696, 2026-04-30), AuthorizationExtent's\n // `transactions` and `bytes` fields are *consumed counters*; the granted allowance moved\n // to `transactions_allowance` / `bytes_allowance`. Compute remaining = allowance − consumed\n // so the public `remainingTransactions` / `remainingBytes` contract stays semantically\n // honest.\n const status: AuthorizationStatus = {\n authorized: true,\n remainingTransactions: auth.extent.transactions_allowance - auth.extent.transactions,\n // `auth` is `any` (TypedApi<any> upstream — see line 53). TS narrows\n // `any - any` to `number`, so cast each u64 operand to bigint to keep\n // the subtraction in bigint space. Runtime values are bigints from PAPI.\n remainingBytes: (auth.extent.bytes_allowance as bigint) - (auth.extent.bytes as bigint),\n expiration: auth.expiration,\n };\n\n log.debug(\"checkAuthorization\", {\n address,\n remainingTransactions: status.remainingTransactions,\n remainingBytes: status.remainingBytes.toString(),\n expiration: status.expiration,\n });\n\n return status;\n}\n\n/**\n * Options for {@link authorizeAccount}.\n */\nexport interface AuthorizeAccountOptions {\n /**\n * Wrap the extrinsic in `Sudo.sudo(...)` before submission. Default: `false`.\n *\n * Use `true` on networks where granting bulletin storage authorization\n * requires sudo permissions (most production / managed test networks).\n * Use `false` (default) when the account self-authorizes — typical for\n * local development chains.\n */\n viaSudo?: boolean;\n /** When to resolve: `\"best-block\"` (default) or `\"finalized\"`. */\n waitFor?: WaitFor;\n /** Timeout in ms. Default: 300_000 (5 min). */\n timeoutMs?: number;\n /** Lifecycle status callback for UI progress. */\n onStatus?: (status: TxStatus) => void;\n}\n\n/**\n * Grant an account authorization to store data on the Bulletin Chain.\n *\n * Submits a `TransactionStorage.authorize_account` extrinsic, optionally\n * wrapped in `Sudo.sudo(...)` for networks that require sudo to grant\n * authorization. Mirrors the call shape of {@link upload} — top-level\n * function, takes an explicit signer, returns a block hash on success.\n *\n * Pair with {@link checkAuthorization} for a typical \"check, grant if\n * insufficient, then upload\" flow.\n *\n * ## Additive semantics — call once per authorization need\n *\n * `authorize_account` is **additive** within an unexpired authorization window\n * for `AuthorizationScope::Account` (see `pallet-bulletin-transaction-storage`,\n * `fn authorize`). Each successful call **adds** to the existing\n * `transactions_allowance` and `bytes_allowance` rather than overwriting them.\n *\n * Implications for callers:\n *\n * - Calling this function twice with `(100, 1MB)` while the previous\n * authorization is still active leaves the account with quota for `200`\n * transactions and `2MB` — likely unintended.\n * - **This function does NOT use `withRetry`.** Retrying a successful-but-\n * acknowledgment-lost submission would double-grant the quota. Callers\n * needing retry should wrap this function and use {@link checkAuthorization}\n * to verify the post-state before retrying.\n * - To \"reset\" a quota, let the existing authorization expire\n * (`AuthorizationPeriod` blocks). The next call after expiry creates a fresh\n * authorization rather than adding.\n *\n * Note: `AuthorizationScope::Preimage` uses `set` semantics in the same\n * pallet. This helper is for account-scope authorization only.\n *\n * @param api - Typed Bulletin Chain API instance.\n * @param who - SS58-encoded account to authorize.\n * @param transactions - Number of transactions to **add** to the account's allowance.\n * @param bytes - Byte budget to **add** to the account's allowance.\n * @param signer - Signer for the extrinsic. On `viaSudo: true` this must be the sudo key.\n * @param options - Optional `viaSudo` flag plus standard submission controls.\n * @returns Block hash where the extrinsic was included.\n * @throws {ProductBulletinError} If `viaSudo: true` is requested but the chain has no `Sudo` pallet.\n *\n * @example Direct (account self-authorizes — local dev)\n * ```ts\n * import { authorizeAccount } from \"@parity/product-sdk-bulletin\";\n *\n * await authorizeAccount(api, address, 100, 100n * 1024n * 1024n, signer);\n * ```\n *\n * @example Sudo-wrapped (managed test network)\n * ```ts\n * await authorizeAccount(api, userAddress, 100, 1_000_000n, sudoSigner, {\n * viaSudo: true,\n * });\n * ```\n *\n * @see {@link checkAuthorization} for the read counterpart.\n * @see {@link BulletinClient.authorizeAccount} for the client method equivalent.\n */\nexport async function authorizeAccount(\n api: BulletinApi,\n who: string,\n transactions: number,\n bytes: bigint,\n signer: PolkadotSigner,\n options: AuthorizeAccountOptions = {},\n): Promise<{ blockHash: string }> {\n const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;\n\n // Single `as any` cast for the whole function. `BulletinApi` is upstream-\n // typed as `TypedApi<any>` (see types.ts), so member access is loose by\n // design. Same pattern as upload.ts:57 — narrowing it requires retyping\n // BulletinApi against a bundled descriptor (out of scope here).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const apiTx = (api as any).tx;\n\n log.info(\"authorizeAccount: building extrinsic\", {\n who,\n transactions,\n bytes: bytes.toString(),\n viaSudo,\n });\n\n if (viaSudo && !apiTx.Sudo?.sudo) {\n throw new ProductBulletinError(\n \"viaSudo: true requires the Sudo pallet, which is not available on this network. \" +\n \"On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism.\",\n );\n }\n\n const authorizeTx = apiTx.TransactionStorage.authorize_account({\n who,\n transactions,\n bytes,\n });\n\n const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;\n\n // NOTE: Intentionally NOT using `withRetry` here. `authorize_account` is\n // additive (see JSDoc above), so a retry after a successful-but-lost\n // submission would double-grant the quota. Caller-side retry must verify\n // post-state via `checkAuthorization` first.\n const result = await submitAndWatch(txToSubmit, signer, {\n waitFor,\n timeoutMs,\n onStatus,\n });\n\n log.info(\"authorizeAccount: included in block\", {\n who,\n blockHash: result.block.hash,\n });\n\n return { blockHash: result.block.hash };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function createMockApi(authResult: unknown) {\n return {\n query: {\n TransactionStorage: {\n Authorizations: {\n getValue: vi.fn().mockResolvedValue(authResult),\n },\n },\n },\n } as unknown as BulletinApi;\n }\n\n describe(\"checkAuthorization\", () => {\n test(\"returns not authorized when no authorization exists\", async () => {\n const api = createMockApi(undefined);\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(false);\n expect(status.remainingTransactions).toBe(0);\n expect(status.remainingBytes).toBe(0n);\n expect(status.expiration).toBe(0);\n });\n\n test(\"returns authorization with full quota (fresh authorize, nothing consumed)\", async () => {\n const api = createMockApi({\n extent: {\n transactions: 0,\n transactions_allowance: 10,\n bytes: 0n,\n bytes_permanent: 0n,\n bytes_allowance: 1_000_000n,\n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingTransactions).toBe(10);\n expect(status.remainingBytes).toBe(1_000_000n);\n expect(status.expiration).toBe(999);\n });\n\n test(\"returns authorization with zero transactions remaining (fully consumed)\", async () => {\n const api = createMockApi({\n extent: {\n transactions: 5,\n transactions_allowance: 5,\n bytes: 0n,\n bytes_permanent: 0n,\n bytes_allowance: 1_000_000n,\n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingTransactions).toBe(0);\n });\n\n test(\"returns authorization with zero bytes remaining (fully consumed)\", async () => {\n const api = createMockApi({\n extent: {\n transactions: 0,\n transactions_allowance: 5,\n bytes: 1_000n,\n bytes_permanent: 0n,\n bytes_allowance: 1_000n,\n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingBytes).toBe(0n);\n });\n\n test(\"returns remaining = allowance − consumed when partially used\", async () => {\n const api = createMockApi({\n extent: {\n transactions: 3,\n transactions_allowance: 10,\n bytes: 250_000n,\n bytes_permanent: 0n,\n bytes_allowance: 1_000_000n,\n },\n expiration: 999,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.authorized).toBe(true);\n expect(status.remainingTransactions).toBe(7); // 10 − 3\n expect(status.remainingBytes).toBe(750_000n); // 1M − 250K\n });\n\n test(\"preserves expiration block number\", async () => {\n const api = createMockApi({\n extent: {\n transactions: 0,\n transactions_allowance: 1,\n bytes: 0n,\n bytes_permanent: 0n,\n bytes_allowance: 500n,\n },\n expiration: 12345,\n });\n const status = await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(status.expiration).toBe(12345);\n });\n\n test(\"throws BulletinAuthorizationError when query fails\", async () => {\n const api = {\n query: {\n TransactionStorage: {\n Authorizations: {\n getValue: vi.fn().mockRejectedValue(new Error(\"RPC connection lost\")),\n },\n },\n },\n } as unknown as BulletinApi;\n\n const err = await checkAuthorization(api, \"5GrwvaEF...\").catch((e: unknown) => e);\n expect(err).toBeInstanceOf(BulletinAuthorizationError);\n const error = err as BulletinAuthorizationError;\n expect(error.address).toBe(\"5GrwvaEF...\");\n expect(error.cause).toBeInstanceOf(Error);\n expect((error.cause as Error).message).toBe(\"RPC connection lost\");\n });\n\n test(\"passes correct Enum key to the query\", async () => {\n const getValue = vi.fn().mockResolvedValue(undefined);\n const api = {\n query: {\n TransactionStorage: {\n Authorizations: { getValue },\n },\n },\n } as unknown as BulletinApi;\n\n await checkAuthorization(api, \"5GrwvaEF...\");\n\n expect(getValue).toHaveBeenCalledTimes(1);\n const arg = getValue.mock.calls[0][0];\n expect(arg.type).toBe(\"Account\");\n expect(arg.value).toBe(\"5GrwvaEF...\");\n });\n });\n\n /**\n * Mock factory for `authorizeAccount` tests.\n *\n * Mirrors the mocking style used in `upload.ts` — we don't mock\n * `submitAndWatch` (the SDK helper) directly. Instead we let it call\n * through to a fake `signSubmitAndWatch` on the api, which emits the\n * lifecycle events `submitAndWatch` listens for.\n */\n function createMockApiForAuthorize(blockHash = \"0xblockhash\") {\n const fakeTx = {\n decodedCall: { fakeCall: true } as unknown,\n signSubmitAndWatch: vi.fn().mockReturnValue({\n subscribe: (handlers: { next: (e: unknown) => void }) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xtxhash\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xtxhash\",\n found: true,\n ok: true,\n block: { hash: blockHash, number: 1, index: 0 },\n events: [],\n });\n });\n return { unsubscribe: vi.fn() };\n },\n }),\n };\n\n return {\n api: {\n tx: {\n TransactionStorage: {\n authorize_account: vi.fn().mockReturnValue(fakeTx),\n },\n Sudo: {\n sudo: vi.fn().mockReturnValue(fakeTx),\n },\n },\n },\n fakeTx,\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n describe(\"authorizeAccount\", () => {\n test(\"direct path: calls TransactionStorage.authorize_account with the right params\", async () => {\n const { api } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 100,\n 1_000_000n,\n mockSigner,\n );\n\n expect(api.tx.TransactionStorage.authorize_account).toHaveBeenCalledOnce();\n const arg = api.tx.TransactionStorage.authorize_account.mock.calls[0][0];\n expect(arg.who).toBe(\"5GrwvaEF...\");\n expect(arg.transactions).toBe(100);\n expect(arg.bytes).toBe(1_000_000n);\n });\n\n test(\"direct path: does NOT call Sudo.sudo when viaSudo is false (default)\", async () => {\n const { api } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n );\n\n expect(api.tx.Sudo.sudo).not.toHaveBeenCalled();\n });\n\n test(\"direct path: returns the block hash from submission\", async () => {\n const { api } = createMockApiForAuthorize(\"0xdeadbeef\");\n\n const result = await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n );\n\n expect(result).toEqual({ blockHash: \"0xdeadbeef\" });\n });\n\n test(\"sudo path: wraps the authorize_account call inside Sudo.sudo\", async () => {\n const { api, fakeTx } = createMockApiForAuthorize();\n\n await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n );\n\n expect(api.tx.Sudo.sudo).toHaveBeenCalledOnce();\n const sudoArg = api.tx.Sudo.sudo.mock.calls[0][0];\n expect(sudoArg.call).toBe(fakeTx.decodedCall);\n });\n\n test(\"sudo path: still returns the block hash from the sudo extrinsic\", async () => {\n const { api } = createMockApiForAuthorize(\"0xsudoblock\");\n\n const result = await authorizeAccount(\n api as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n );\n\n expect(result).toEqual({ blockHash: \"0xsudoblock\" });\n });\n\n test(\"throws ProductBulletinError when viaSudo is true but the chain lacks a Sudo pallet\", async () => {\n const apiWithoutSudo = {\n tx: {\n TransactionStorage: {\n authorize_account: vi.fn().mockReturnValue({\n decodedCall: {} as unknown,\n signSubmitAndWatch: vi.fn(),\n }),\n },\n // Sudo intentionally absent — represents production Polkadot/Kusama\n },\n };\n\n const err = await authorizeAccount(\n apiWithoutSudo as unknown as BulletinApi,\n \"5GrwvaEF...\",\n 10,\n 100n,\n mockSigner,\n { viaSudo: true },\n ).catch((e: unknown) => e);\n\n expect(err).toBeInstanceOf(ProductBulletinError);\n expect((err as Error).message).toMatch(/Sudo pallet/i);\n // Verify we did NOT proceed to submit anything\n expect(apiWithoutSudo.tx.TransactionStorage.authorize_account).not.toHaveBeenCalled();\n });\n\n // Note: submission-failure propagation is tested upstream in\n // @parity/product-sdk-tx's own suite. Re-testing it here would just\n // re-test submitAndWatch's error path through a brittle mock, which\n // depends on internal event-shape details outside this file's contract.\n });\n}\n","/**\n * CID helpers for converting between on-chain hex hashes and CIDs.\n *\n * Upstream `@parity/bulletin-sdk` exposes `calculateCid` (data → CID),\n * `parseCid` (string → CID), `cidFromBytes` (full-encoded → CID), and\n * `cidToBytes` (CID → full-encoded). The helpers here add a thin layer\n * for the `0x`-prefixed hex shape that on-chain `TransactionInfo` uses,\n * so callers don't need to do the digest plumbing themselves.\n *\n * Both helpers default to the chain default (blake2b-256, raw codec).\n * Pass `HashAlgorithm` and `CidCodec` for other configurations\n * (sha2-256, dag-pb, etc.).\n */\nimport { CID } from \"multiformats/cid\";\nimport * as Digest from \"multiformats/hashes/digest\";\n\nimport { BulletinCidError } from \"./errors.js\";\n\n/**\n * Hash algorithms supported by the Bulletin Chain.\n *\n * Values are multihash codes from the multicodec table.\n */\nexport const HashAlgorithm = {\n /** BLAKE2b-256 — chain default. */\n Blake2b256: 0xb220,\n /** SHA2-256. */\n Sha2_256: 0x12,\n /** Keccak-256 — Ethereum compatibility. */\n Keccak256: 0x1b,\n} as const;\nexport type HashAlgorithm = (typeof HashAlgorithm)[keyof typeof HashAlgorithm];\n\n/**\n * CID codecs supported by the Bulletin Chain.\n */\nexport const CidCodec = {\n /** Raw binary — default for single-chunk data. */\n Raw: 0x55,\n /** DAG-PB — used for multi-chunk manifests / IPFS UnixFS. */\n DagPb: 0x70,\n /** DAG-CBOR — alternative DAG encoding. */\n DagCbor: 0x71,\n} as const;\nexport type CidCodec = (typeof CidCodec)[keyof typeof CidCodec];\n\nconst SUPPORTED_HASH_CODES = new Set<number>(Object.values(HashAlgorithm));\nconst SUPPORTED_CODEC_CODES = new Set<number>(Object.values(CidCodec));\nconst EXPECTED_HEX_LENGTH = 66; // \"0x\" + 64 hex chars (32-byte digest)\n\n/**\n * Reconstruct a CIDv1 from a `0x`-prefixed 32-byte hex hash.\n *\n * Useful when reading on-chain `TransactionInfo.content_hash` and you need\n * the CID to look up content via an IPFS gateway.\n *\n * @param hexHash - 66-char `0x`-prefixed hex of a 32-byte digest.\n * @param hashCode - Multihash code (default: blake2b-256).\n * @param codec - Multicodec code (default: raw).\n */\nexport function hashToCid(\n hexHash: `0x${string}`,\n hashCode: HashAlgorithm = HashAlgorithm.Blake2b256,\n codec: CidCodec = CidCodec.Raw,\n): string {\n if (hexHash.length !== EXPECTED_HEX_LENGTH) {\n throw new BulletinCidError(\n `Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), ` +\n `got ${hexHash.length} chars`,\n );\n }\n if (!/^0x[0-9a-fA-F]{64}$/.test(hexHash)) {\n throw new BulletinCidError(\n `Invalid hash format: expected 0x-prefixed 32-byte hex string, got: ${hexHash}`,\n );\n }\n if (!SUPPORTED_HASH_CODES.has(hashCode)) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${hashCode.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n );\n }\n if (!SUPPORTED_CODEC_CODES.has(codec)) {\n throw new BulletinCidError(\n `Unsupported CID codec 0x${codec.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n );\n }\n const digest = hexToBytes(hexHash);\n return CID.createV1(codec, Digest.create(hashCode, digest)).toString();\n}\n\n/**\n * Extract the 32-byte content hash digest from a CIDv1 and return it as a\n * `0x`-prefixed hex string.\n *\n * Useful for matching a CID against on-chain `TransactionInfo.content_hash`.\n */\nexport function cidToPreimageKey(cid: string): `0x${string}` {\n let parsed;\n try {\n parsed = CID.parse(cid);\n } catch {\n throw new BulletinCidError(`Invalid CID: ${cid}`, cid);\n }\n if (parsed.version !== 1) {\n throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);\n }\n if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; ` +\n `expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(\", \")}`,\n cid,\n );\n }\n return `0x${bytesToHex(parsed.multihash.digest)}` as `0x${string}`;\n}\n\nfunction hexToBytes(hex: `0x${string}`): Uint8Array {\n const out = new Uint8Array(32);\n for (let i = 0; i < 32; i++) {\n out[i] = Number.parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);\n }\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let s = \"\";\n for (let i = 0; i < bytes.length; i++) {\n s += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return s;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"hashToCid\", () => {\n const sampleHex = `0x${\"ab\".repeat(32)}` as `0x${string}`;\n\n test(\"produces valid base32-lower CIDv1 (default: blake2b-256, raw)\", () => {\n const cid = hashToCid(sampleHex);\n expect(cid).toMatch(/^b[a-z2-7]+$/);\n const parsed = CID.parse(cid);\n expect(parsed.version).toBe(1);\n expect(parsed.code).toBe(CidCodec.Raw);\n expect(parsed.multihash.code).toBe(HashAlgorithm.Blake2b256);\n });\n\n test(\"supports sha2-256\", () => {\n const cid = hashToCid(sampleHex, HashAlgorithm.Sha2_256);\n expect(CID.parse(cid).multihash.code).toBe(HashAlgorithm.Sha2_256);\n });\n\n test(\"supports dag-pb codec\", () => {\n const cid = hashToCid(sampleHex, HashAlgorithm.Blake2b256, CidCodec.DagPb);\n expect(CID.parse(cid).code).toBe(CidCodec.DagPb);\n });\n\n test(\"throws on short hex\", () => {\n expect(() => hashToCid(\"0xabcd\" as `0x${string}`)).toThrow(BulletinCidError);\n });\n\n test(\"throws on long hex\", () => {\n const tooLong = `0x${\"aa\".repeat(33)}` as `0x${string}`;\n expect(() => hashToCid(tooLong)).toThrow(BulletinCidError);\n });\n\n test(\"throws on non-hex characters\", () => {\n const bad = `0x${\"zz\".repeat(32)}` as `0x${string}`;\n expect(() => hashToCid(bad)).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported hash algorithm\", () => {\n expect(() => hashToCid(sampleHex, 0x99 as HashAlgorithm)).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported codec\", () => {\n expect(() => hashToCid(sampleHex, HashAlgorithm.Blake2b256, 0x99 as CidCodec)).toThrow(\n BulletinCidError,\n );\n });\n });\n\n describe(\"cidToPreimageKey\", () => {\n test(\"round-trip with hashToCid\", () => {\n const hex = `0x${\"cd\".repeat(32)}` as `0x${string}`;\n const cid = hashToCid(hex);\n expect(cidToPreimageKey(cid)).toBe(hex);\n });\n\n test(\"round-trip with sha2-256\", () => {\n const hex = `0x${\"ef\".repeat(32)}` as `0x${string}`;\n const cid = hashToCid(hex, HashAlgorithm.Sha2_256);\n expect(cidToPreimageKey(cid)).toBe(hex);\n });\n\n test(\"throws on invalid CID string\", () => {\n expect(() => cidToPreimageKey(\"not-a-cid\")).toThrow(BulletinCidError);\n });\n\n test(\"throws on CIDv0 input\", () => {\n const hash = new Uint8Array(32).fill(0xab);\n const cidV0 = CID.create(0, 0x70, Digest.create(HashAlgorithm.Sha2_256, hash));\n expect(() => cidToPreimageKey(cidV0.toString())).toThrow(BulletinCidError);\n });\n\n test(\"throws on unsupported hash algorithm\", () => {\n const hash = new Uint8Array(32).fill(0xab);\n const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(0x99, hash));\n expect(() => cidToPreimageKey(cidV1.toString())).toThrow(BulletinCidError);\n });\n });\n}\n","import { calculateCid } from \"@parity/bulletin-sdk\";\nimport { getPreimageManager, type PreimageManager } from \"@parity/product-sdk-host\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { cidToPreimageKey } from \"./cid.js\";\nimport {\n BulletinHostUnavailableError,\n BulletinLookupInterruptedError,\n BulletinLookupTimeoutError,\n} from \"./errors.js\";\n\nconst log = createLogger(\"bulletin\");\n\nconst DEFAULT_LOOKUP_TIMEOUT_MS = 30_000;\n\n/**\n * Query strategy for the Bulletin Chain.\n *\n * The host manages the lookup via its preimage subscription API,\n * which includes local caching and managed IPFS polling.\n */\nexport interface QueryStrategy {\n kind: \"host-lookup\";\n lookup: (cid: string, timeoutMs?: number) => Promise<Uint8Array>;\n}\n\n/**\n * Determine the query strategy for the Bulletin Chain.\n *\n * Uses the host preimage lookup API which caches results and manages\n * IPFS polling automatically.\n *\n * @returns The resolved query strategy.\n * @throws {BulletinHostUnavailableError} If the host preimage manager is unavailable.\n */\nexport async function resolveQueryStrategy(): Promise<QueryStrategy> {\n const preimageManager = await getPreimageManager();\n if (preimageManager) {\n log.info(\"using host preimage lookup for bulletin queries\");\n return {\n kind: \"host-lookup\",\n lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs),\n };\n }\n\n throw new BulletinHostUnavailableError(\"query\");\n}\n\n/**\n * Wrap `preimageManager.lookup` (subscription-based) into a one-shot Promise.\n *\n * Converts the CID to a hex preimage key, subscribes, and resolves on the\n * first non-null callback. Rejects on timeout or if the host interrupts the\n * subscription (e.g. after repeated failures). Always unsubscribes on settlement.\n *\n * @param manager - The product-sdk preimage manager.\n * @param cid - CIDv1 string to look up.\n * @param timeoutMs - Maximum wait time. Default: 30_000ms.\n * @returns The raw bytes of the preimage.\n */\nexport function lookupViaHost(\n manager: PreimageManager,\n cid: string,\n timeoutMs: number = DEFAULT_LOOKUP_TIMEOUT_MS,\n): Promise<Uint8Array> {\n const key = cidToPreimageKey(cid);\n\n return new Promise<Uint8Array>((resolve, reject) => {\n const cleanup = () => {\n cancelInterrupt();\n sub.unsubscribe();\n };\n\n const settle = (fn: () => void) => {\n if (timer === null) return;\n clearTimeout(timer);\n timer = null;\n cleanup();\n fn();\n };\n\n let timer: ReturnType<typeof setTimeout> | null = setTimeout(() => {\n settle(() => {\n reject(new BulletinLookupTimeoutError(cid, timeoutMs));\n });\n }, timeoutMs);\n\n const sub = manager.lookup(key, (preimage) => {\n if (preimage !== null) {\n settle(() => resolve(preimage));\n }\n // null means \"not found yet\" — host will keep polling\n });\n\n const cancelInterrupt = sub.onInterrupt(() => {\n settle(() => {\n reject(new BulletinLookupInterruptedError(cid));\n });\n });\n });\n}\n\nif (import.meta.vitest) {\n const { beforeAll, describe, test, expect, vi } = import.meta.vitest;\n\n // Note: resolveQueryStrategy tests require e2e testing as they\n // depend on the host container environment.\n\n describe(\"lookupViaHost\", () => {\n function createMockManager(\n behavior: \"resolve\" | \"null-then-resolve\" | \"hang\" | \"interrupt\",\n ) {\n const unsubscribe = vi.fn();\n const cancelInterrupt = vi.fn();\n let interruptCb: VoidFunction | undefined;\n\n const lookup = vi.fn((_key: string, callback: (p: Uint8Array | null) => void) => {\n const data = new Uint8Array([10, 20, 30]);\n queueMicrotask(() => {\n if (behavior === \"resolve\") {\n callback(data);\n } else if (behavior === \"null-then-resolve\") {\n callback(null);\n queueMicrotask(() => callback(data));\n } else if (behavior === \"interrupt\") {\n interruptCb?.();\n }\n // \"hang\" does nothing\n });\n return {\n unsubscribe,\n onInterrupt: (cb: VoidFunction) => {\n interruptCb = cb;\n return cancelInterrupt;\n },\n };\n });\n\n return { lookup, unsubscribe, cancelInterrupt, submit: vi.fn() };\n }\n\n // calculateCid is async (Web Crypto), so populate lazily.\n let testCid: string;\n beforeAll(async () => {\n const cid = await calculateCid(new TextEncoder().encode(\"test\"));\n testCid = cid.toString();\n });\n\n test(\"resolves on first non-null callback\", async () => {\n const manager = createMockManager(\"resolve\");\n const result = await lookupViaHost(manager, testCid);\n expect(result).toEqual(new Uint8Array([10, 20, 30]));\n });\n\n test(\"ignores null callbacks and resolves on subsequent data\", async () => {\n const manager = createMockManager(\"null-then-resolve\");\n const result = await lookupViaHost(manager, testCid);\n expect(result).toEqual(new Uint8Array([10, 20, 30]));\n });\n\n test(\"rejects with BulletinLookupTimeoutError on timeout\", async () => {\n const { BulletinLookupTimeoutError } = await import(\"./errors.js\");\n const manager = createMockManager(\"hang\");\n const err = await lookupViaHost(manager, testCid, 50).catch((e) => e);\n expect(err).toBeInstanceOf(BulletinLookupTimeoutError);\n expect(err.cid).toBe(testCid);\n expect(err.timeoutMs).toBe(50);\n });\n\n test(\"rejects with BulletinLookupInterruptedError on interrupt\", async () => {\n const { BulletinLookupInterruptedError } = await import(\"./errors.js\");\n const manager = createMockManager(\"interrupt\");\n const err = await lookupViaHost(manager, testCid).catch((e) => e);\n expect(err).toBeInstanceOf(BulletinLookupInterruptedError);\n expect(err.cid).toBe(testCid);\n });\n\n test(\"calls unsubscribe and cancelInterrupt on resolution\", async () => {\n const manager = createMockManager(\"resolve\");\n await lookupViaHost(manager, testCid);\n expect(manager.unsubscribe).toHaveBeenCalledOnce();\n expect(manager.cancelInterrupt).toHaveBeenCalledOnce();\n });\n\n test(\"calls unsubscribe on interrupt\", async () => {\n const manager = createMockManager(\"interrupt\");\n await lookupViaHost(manager, testCid).catch(() => {});\n expect(manager.unsubscribe).toHaveBeenCalledOnce();\n });\n\n test(\"passes correct hex key to manager\", async () => {\n const expectedKey = cidToPreimageKey(testCid);\n const manager = createMockManager(\"resolve\");\n await lookupViaHost(manager, testCid);\n expect(manager.lookup).toHaveBeenCalledWith(expectedKey, expect.any(Function));\n });\n });\n}\n","import { CidCodec, parseCid, UnixFsDagBuilder } from \"@parity/bulletin-sdk\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { QueryStrategy } from \"./resolve-query.js\";\nimport { resolveQueryStrategy } from \"./resolve-query.js\";\nimport type { QueryOptions } from \"./types.js\";\n\nconst log = createLogger(\"bulletin\");\n\n/**\n * Fetch raw bytes for a CID via the host's preimage lookup.\n *\n * Container-only by design: the bulletin SDK does not retrieve content\n * through public IPFS gateways. Inside a Polkadot Browser / Desktop\n * container, the host's `preimageManager` provides a cached, polling-\n * managed lookup that returns bytes when the underlying IPFS network\n * makes them available. Outside a container, this throws\n * {@link BulletinHostUnavailableError}.\n *\n * The bulletin chain stores transaction *metadata* on-chain\n * (`chunk_root`, `content_hash`, `size`, `cid_codec`, `hashing`) — the\n * content bytes themselves live in IPFS and are surfaced through the\n * host's preimage subscription, never via direct gateway fetch.\n *\n * To prove that a CID was stored on-chain (without fetching the bytes),\n * use `verifyOnChain` from `verify.ts`.\n *\n * @param cid - CIDv1 string to fetch.\n * @param options - Query options (`lookupTimeoutMs` for host).\n * @throws {BulletinHostUnavailableError} If running outside a container.\n */\nexport async function queryBytes(cid: string, options?: QueryOptions): Promise<Uint8Array> {\n const strategy = await resolveQueryStrategy();\n return executeQuery(strategy, cid, options);\n}\n\n/**\n * Fetch and parse JSON for a CID via the host's preimage lookup.\n *\n * Convenience wrapper over {@link queryBytes}.\n */\nexport async function queryJson<T>(cid: string, options?: QueryOptions): Promise<T> {\n const bytes = await queryBytes(cid, options);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Execute a query using a pre-resolved strategy.\n *\n * Exposed so `BulletinClient` can resolve the strategy once at\n * construction time and reuse it across calls without re-detecting\n * the host environment on every fetch.\n *\n * **Reassembly is automatic.** If `cid` carries the DAG-PB codec\n * (`0x70`) — meaning the upload was chunked and a UnixFS manifest was\n * created — this function recursively fetches each chunk via `strategy\n * .lookup` and returns the concatenated bytes. Pass `noReassemble: true`\n * to get the raw manifest bytes instead.\n *\n * For raw-codec CIDs (`0x55`, single-chunk content), the bytes returned\n * by the host are returned directly — no parsing overhead.\n */\nexport async function executeQuery(\n strategy: QueryStrategy,\n cid: string,\n options?: QueryOptions,\n): Promise<Uint8Array> {\n log.info(\"query: host preimage lookup\", { cid });\n const bytes = await strategy.lookup(cid, options?.lookupTimeoutMs);\n\n // Skip reassembly when the caller explicitly asks for raw bytes, or\n // when the CID's codec says this is a single-block payload (raw,\n // 0x55) — most uploads land here, so the parseCid + Promise.all\n // overhead is worth gating on codec rather than always paying it.\n if (options?.noReassemble) return bytes;\n const parsed = parseCid(cid);\n if (parsed.code !== CidCodec.DagPb) return bytes;\n\n log.info(\"query: reassembling DAG-PB manifest\", { cid });\n const builder = new UnixFsDagBuilder();\n const { chunkCids } = await builder.parse(bytes);\n\n // Fetch chunks in parallel — the host's preimageManager caches and\n // dedupes lookups, and order is preserved by Promise.all's input\n // ordering, which matches the DAG-PB Links order from parse().\n const chunks = await Promise.all(\n chunkCids.map((c) => strategy.lookup(c.toString(), options?.lookupTimeoutMs)),\n );\n\n let total = 0;\n for (const chunk of chunks) total += chunk.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nif (import.meta.vitest) {\n const { beforeAll, describe, test, expect, vi } = import.meta.vitest;\n const { calculateCid } = await import(\"@parity/bulletin-sdk\");\n\n describe(\"executeQuery\", () => {\n const testData = new Uint8Array([1, 2, 3]);\n\n test(\"delegates to the strategy's lookup function\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n // Use a real raw-codec CID so the codec check passes through\n // without triggering reassembly.\n const rawCid = (await calculateCid(testData)).toString();\n const result = await executeQuery(strategy, rawCid);\n expect(result).toBe(testData);\n expect(lookup).toHaveBeenCalledWith(rawCid, undefined);\n });\n\n test(\"forwards lookupTimeoutMs to the strategy\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const rawCid = (await calculateCid(testData)).toString();\n await executeQuery(strategy, rawCid, { lookupTimeoutMs: 5000 });\n expect(lookup).toHaveBeenCalledWith(rawCid, 5000);\n });\n\n test(\"returns raw bytes directly for raw-codec CIDs (no reassembly)\", async () => {\n const lookup = vi.fn().mockResolvedValue(testData);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const rawCid = (await calculateCid(testData)).toString();\n const result = await executeQuery(strategy, rawCid);\n expect(result).toBe(testData);\n // Single lookup, no recursion.\n expect(lookup).toHaveBeenCalledTimes(1);\n });\n\n test(\"noReassemble: true short-circuits even for DAG-PB CIDs\", async () => {\n // Manufacture a DAG-PB CID; we don't need the bytes to actually\n // be a valid manifest because we're skipping the parse step.\n const fakeManifestBytes = new Uint8Array([10, 20, 30]);\n const dagPbCid = (await calculateCid(fakeManifestBytes, /* dag-pb */ 0x70)).toString();\n const lookup = vi.fn().mockResolvedValue(fakeManifestBytes);\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, dagPbCid, { noReassemble: true });\n expect(result).toBe(fakeManifestBytes);\n expect(lookup).toHaveBeenCalledTimes(1);\n });\n\n describe(\"DAG-PB reassembly\", () => {\n // Build a real manifest: two raw chunks, dag-pb root.\n const chunkA = new Uint8Array([0xaa, 0xaa, 0xaa]);\n const chunkB = new Uint8Array([0xbb, 0xbb]);\n let chunkACid: string;\n let chunkBCid: string;\n let manifestBytes: Uint8Array;\n let manifestCid: string;\n\n beforeAll(async () => {\n const { UnixFsDagBuilder: Builder } = await import(\"@parity/bulletin-sdk\");\n chunkACid = (await calculateCid(chunkA)).toString();\n chunkBCid = (await calculateCid(chunkB)).toString();\n const cidA = await calculateCid(chunkA);\n const cidB = await calculateCid(chunkB);\n const manifest = await new Builder().build([\n { data: chunkA, cid: cidA, index: 0, totalChunks: 2 },\n { data: chunkB, cid: cidB, index: 1, totalChunks: 2 },\n ]);\n manifestBytes = manifest.dagBytes;\n manifestCid = manifest.rootCid.toString();\n });\n\n test(\"recursively fetches chunks and concatenates\", async () => {\n const lookup = vi.fn(async (cid: string) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(`unexpected lookup: ${cid}`);\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, manifestCid);\n expect(result).toEqual(new Uint8Array([0xaa, 0xaa, 0xaa, 0xbb, 0xbb]));\n // 1 manifest + 2 chunks = 3 lookups.\n expect(lookup).toHaveBeenCalledTimes(3);\n });\n\n test(\"forwards lookupTimeoutMs to every chunk lookup\", async () => {\n const lookup = vi.fn(async (cid: string, _timeoutMs?: number) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(\"boom\");\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n await executeQuery(strategy, manifestCid, { lookupTimeoutMs: 7777 });\n for (const call of lookup.mock.calls) {\n expect(call[1]).toBe(7777);\n }\n });\n\n test(\"preserves chunk order from the manifest\", async () => {\n const lookup = vi.fn(async (cid: string) => {\n if (cid === manifestCid) return manifestBytes;\n if (cid === chunkACid) return chunkA;\n if (cid === chunkBCid) return chunkB;\n throw new Error(\"boom\");\n });\n const strategy: QueryStrategy = { kind: \"host-lookup\", lookup };\n const result = await executeQuery(strategy, manifestCid);\n // Order matters: chunkA bytes must come before chunkB bytes\n // even though both are fetched in parallel.\n expect(Array.from(result.slice(0, 3))).toEqual([0xaa, 0xaa, 0xaa]);\n expect(Array.from(result.slice(3))).toEqual([0xbb, 0xbb]);\n });\n });\n });\n}\n","/**\n * Chain-storage verification for stored CIDs.\n *\n * The bulletin chain doesn't store content bytes on-chain — `TransactionStorage\n * .Transactions[block]` holds metadata only (`{ chunk_root, content_hash,\n * hashing, cid_codec, size, block_chunks }`), and the bytes themselves live\n * in IPFS. So \"read by CID from chain\" isn't possible. What *is* possible is\n * proving that a given CID was stored: parse the CID's digest + multihash\n * code, then look it up in `TransactionStorage.Transactions` and confirm a\n * matching `content_hash` + `hashing`.\n *\n * Common use case: just-after-upload UX — `await client.store(data).send()`\n * gives you back `{ cid, blockNumber, extrinsicIndex? }`, and a follow-up\n * `verifyOnChain(api, cid, { block: blockNumber })` confirms the metadata\n * landed where expected.\n */\nimport { CID } from \"multiformats/cid\";\n\nimport { HashAlgorithm } from \"./cid.js\";\nimport { BulletinCidError } from \"./errors.js\";\nimport type { BulletinApi } from \"./types.js\";\n\n/**\n * Match a multihash code in a CID against the chain's `hashing` enum value.\n */\nconst HASH_CODE_TO_ENUM_TYPE: Record<number, \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\"> = {\n [HashAlgorithm.Blake2b256]: \"Blake2b256\",\n [HashAlgorithm.Sha2_256]: \"Sha2_256\",\n [HashAlgorithm.Keccak256]: \"Keccak256\",\n};\n\n/** A single matched entry from `TransactionStorage.Transactions`. */\nexport interface ChainStoredEntry {\n /** Block number where the transaction was included. */\n block: number;\n /** Index of the entry within the block's transactions array. */\n index: number;\n /** Size of the stored data in bytes (from chain metadata). */\n size: number;\n /** Number of chunks (1 for unchunked data, >1 for chunked + manifest). */\n blockChunks: number;\n}\n\n/**\n * Verification options for {@link verifyOnChain}.\n */\nexport interface VerifyOnChainOptions {\n /**\n * Block number to look up. Pass the `blockNumber` returned from a prior\n * `store(...).send()` for an O(1) lookup.\n *\n * If omitted, throws — full-chain scans are not supported because\n * `RetentionPeriod` can be many days of blocks. Use `getEntries()` on\n * `api.query.TransactionStorage.Transactions` directly if you need that.\n */\n block: number;\n /**\n * Optional: index within the block. When provided, narrows verification\n * to that exact slot. Useful when re-checking a known `(block, index)`\n * tuple from an earlier receipt.\n */\n index?: number;\n}\n\n/**\n * Verify that a CID is recorded in the bulletin chain's `Transactions`\n * storage at the given block.\n *\n * Returns the matched entry (with block + index) when the CID's content\n * hash and hashing algorithm both match a `Transactions[block]` entry.\n * Returns `null` when no match is found at that block.\n *\n * @param api - Typed bulletin API.\n * @param cid - CIDv1 string to look up.\n * @param options - Verification target (block number, optional index).\n *\n * @example\n * ```ts\n * const receipt = await client.store(data).send();\n * if (receipt.blockNumber !== undefined) {\n * const entry = await verifyOnChain(client.api, receipt.cid!.toString(), {\n * block: receipt.blockNumber,\n * index: receipt.extrinsicIndex,\n * });\n * if (!entry) console.warn(\"CID not found in expected block — chain reorg?\");\n * }\n * ```\n */\nexport async function verifyOnChain(\n api: BulletinApi,\n cid: string,\n options: VerifyOnChainOptions,\n): Promise<ChainStoredEntry | null> {\n const parsed = parseCidForVerify(cid);\n\n const queryFn = (api as unknown as TransactionsQueryApi).query?.TransactionStorage?.Transactions\n ?.getValue;\n if (!queryFn) {\n throw new Error(\n \"Bulletin API does not expose query.TransactionStorage.Transactions — \" +\n \"the typed API may be incomplete or the runtime version doesn't match the descriptor.\",\n );\n }\n\n const entries = await queryFn(options.block);\n if (!entries || entries.length === 0) return null;\n\n // When an explicit index is provided, check that slot directly — no\n // reason to walk the full array just to skip everything else.\n if (options.index !== undefined) {\n const entry = entries[options.index];\n if (entry && matchesEntry(entry, parsed)) {\n return {\n block: options.block,\n index: options.index,\n size: entry.size,\n blockChunks: entry.block_chunks,\n };\n }\n return null;\n }\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!;\n if (matchesEntry(entry, parsed)) {\n return {\n block: options.block,\n index: i,\n size: entry.size,\n blockChunks: entry.block_chunks,\n };\n }\n }\n\n return null;\n}\n\ninterface ParsedCid {\n digest: Uint8Array;\n hashType: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\";\n}\n\n/**\n * Hand-rolled mirror of `TransactionStorage.Transactions[block][n]` — the\n * shape PAPI returns at runtime when you call `query.TransactionStorage\n * .Transactions.getValue(block)`. Defined here (rather than derived from\n * `BulletinTypedApi`) because the typed API surfaces these values through\n * `Anonymize<I…>` codec aliases that aren't ergonomic to inline.\n *\n * **If the bulletin runtime changes the entry shape, update this here too.**\n * Source of truth: `TransactionInfo` in\n * `packages/descriptors/chains/bulletin/generated/dist/common-types.d.ts`\n * (look for `chunk_root: FixedSizeBinary<32>` to anchor it). When the\n * descriptor regenerates and the fields shift, this interface, the\n * `cid_codec`/`hashing` matching in `matchesEntry`, and the\n * `HASH_CODE_TO_ENUM_TYPE` map above all need to be re-validated together.\n *\n * `Uint8Array | { asBytes(): Uint8Array }` covers both the raw and Binary-\n * wrapped shapes the codec can return depending on configuration.\n */\ninterface ChainEntry {\n chunk_root: { asBytes(): Uint8Array } | Uint8Array;\n content_hash: { asBytes(): Uint8Array } | Uint8Array;\n hashing: { type: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\" };\n cid_codec: bigint;\n size: number;\n block_chunks: number;\n}\n\ninterface TransactionsQueryApi {\n query?: {\n TransactionStorage?: {\n Transactions?: {\n getValue: (block: number) => Promise<ChainEntry[] | undefined>;\n };\n };\n };\n}\n\nfunction parseCidForVerify(cid: string): ParsedCid {\n let parsed;\n try {\n parsed = CID.parse(cid);\n } catch {\n throw new BulletinCidError(`Invalid CID: ${cid}`, cid);\n }\n if (parsed.version !== 1) {\n throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);\n }\n const hashType = HASH_CODE_TO_ENUM_TYPE[parsed.multihash.code];\n if (!hashType) {\n throw new BulletinCidError(\n `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}`,\n cid,\n );\n }\n return { digest: parsed.multihash.digest, hashType };\n}\n\nfunction matchesEntry(entry: ChainEntry, target: ParsedCid): boolean {\n if (entry.hashing.type !== target.hashType) return false;\n const onChainBytes =\n entry.content_hash instanceof Uint8Array\n ? entry.content_hash\n : entry.content_hash.asBytes();\n if (onChainBytes.length !== target.digest.length) return false;\n for (let i = 0; i < onChainBytes.length; i++) {\n if (onChainBytes[i] !== target.digest[i]) return false;\n }\n return true;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function makeMockApi(getValue: (block: number) => Promise<ChainEntry[] | undefined>) {\n return {\n query: {\n TransactionStorage: {\n Transactions: { getValue },\n },\n },\n } as unknown as BulletinApi;\n }\n\n function makeEntry(\n digest: Uint8Array,\n hashType: \"Blake2b256\" | \"Sha2_256\" | \"Keccak256\" = \"Blake2b256\",\n size = 100,\n blockChunks = 1,\n ): ChainEntry {\n return {\n chunk_root: digest,\n content_hash: digest,\n hashing: { type: hashType },\n cid_codec: 0x55n,\n size,\n block_chunks: blockChunks,\n };\n }\n\n // Build a real CIDv1 (blake2b-256, raw) we can verify against\n async function makeCidWithDigest(digest: Uint8Array, hashCode = 0xb220): Promise<string> {\n const Digest = await import(\"multiformats/hashes/digest\");\n return CID.createV1(0x55, Digest.create(hashCode, digest)).toString();\n }\n\n describe(\"verifyOnChain\", () => {\n test(\"returns entry when CID matches at given block\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(digest)]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toEqual({ block: 100, index: 0, size: 100, blockChunks: 1 });\n });\n\n test(\"returns null when block has no entries\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const api = makeMockApi(vi.fn().mockResolvedValue(undefined));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"returns null when block has entries but none match\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const otherDigest = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(otherDigest)]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"returns null when hashing algorithm differs\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n // CID uses blake2b-256, chain entry says sha2-256 with same digest bytes\n const cid = await makeCidWithDigest(digest, 0xb220);\n const api = makeMockApi(vi.fn().mockResolvedValue([makeEntry(digest, \"Sha2_256\")]));\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result).toBeNull();\n });\n\n test(\"finds match at correct index when multiple entries exist\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100 });\n expect(result?.index).toBe(2);\n });\n\n test(\"respects explicit index option\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n // Target is at index 2, but caller says index 0 — should not match\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100, index: 0 });\n expect(result).toBeNull();\n });\n\n test(\"returns the entry when explicit index matches\", async () => {\n const targetDigest = new Uint8Array(32).fill(0xab);\n const filler = new Uint8Array(32).fill(0xcd);\n const cid = await makeCidWithDigest(targetDigest);\n const api = makeMockApi(\n vi\n .fn()\n .mockResolvedValue([\n makeEntry(filler),\n makeEntry(filler),\n makeEntry(targetDigest),\n ]),\n );\n\n const result = await verifyOnChain(api, cid, { block: 100, index: 2 });\n expect(result?.index).toBe(2);\n });\n\n test(\"throws BulletinCidError on invalid CID\", async () => {\n const api = makeMockApi(vi.fn());\n await expect(verifyOnChain(api, \"not-a-cid\", { block: 1 })).rejects.toThrow(\n BulletinCidError,\n );\n });\n\n test(\"throws when api lacks the expected query path\", async () => {\n const api = {} as BulletinApi;\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n await expect(verifyOnChain(api, cid, { block: 1 })).rejects.toThrow(\n /does not expose query/,\n );\n });\n\n test(\"handles content_hash as a Binary-like wrapper\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const wrapper = { asBytes: () => digest };\n const entry: ChainEntry = {\n chunk_root: wrapper,\n content_hash: wrapper,\n hashing: { type: \"Blake2b256\" },\n cid_codec: 0x55n,\n size: 50,\n block_chunks: 1,\n };\n const api = makeMockApi(vi.fn().mockResolvedValue([entry]));\n const result = await verifyOnChain(api, cid, { block: 1 });\n expect(result).toEqual({ block: 1, index: 0, size: 50, blockChunks: 1 });\n });\n\n test(\"passes the block number to the storage call\", async () => {\n const digest = new Uint8Array(32).fill(0xab);\n const cid = await makeCidWithDigest(digest);\n const getValue = vi.fn().mockResolvedValue([makeEntry(digest)]);\n const api = makeMockApi(getValue);\n await verifyOnChain(api, cid, { block: 42 });\n expect(getValue).toHaveBeenCalledWith(42);\n });\n });\n}\n","import {\n AsyncBulletinClient,\n type AuthCallBuilder,\n type BulletinTypedApi,\n type CallBuilder,\n type ClientConfig,\n type StoreBuilder,\n type SubmitFn,\n} from \"@parity/bulletin-sdk\";\nimport { createChainClient, getChainAPI } from \"@parity/product-sdk-chain-client\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport type { PolkadotClient, PolkadotSigner } from \"polkadot-api\";\n\nimport { checkAuthorization } from \"./authorization.js\";\nimport type { BulletinChain, BulletinEnvironment } from \"./networks.js\";\nimport { executeQuery } from \"./query.js\";\nimport { resolveQueryStrategy, type QueryStrategy } from \"./resolve-query.js\";\nimport type { AuthorizationStatus, BulletinApi, QueryOptions } from \"./types.js\";\nimport { verifyOnChain, type ChainStoredEntry, type VerifyOnChainOptions } from \"./verify.js\";\n\nconst log = createLogger(\"bulletin\");\n\n/**\n * Options for {@link BulletinClient.create}.\n *\n * One of two construction shapes is supported:\n *\n * - **Environment shorthand** — pass an `environment` string keyed by\n * {@link BulletinChain}. Wires up the chain-client automatically.\n * - **Explicit network** — pass `genesisHash` and `descriptor` directly\n * (e.g., spread from a {@link BulletinChain} entry, or supply custom\n * values for a private chain).\n */\nexport type CreateBulletinClientOptions =\n | (CreateBulletinClientCommon & { environment: BulletinEnvironment })\n | (CreateBulletinClientCommon & {\n genesisHash: `0x${string}`;\n descriptor: (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"];\n });\n\ninterface CreateBulletinClientCommon {\n /** Signer for transaction submission. Required — every store needs a signer. */\n signer: PolkadotSigner;\n /** Optional config forwarded to {@link AsyncBulletinClient}. */\n config?: Partial<ClientConfig>;\n}\n\n/**\n * Ergonomic entry point for Bulletin Chain operations.\n *\n * Wraps {@link AsyncBulletinClient} from `@parity/bulletin-sdk` (which handles\n * chunking, DAG-PB manifests, CID calculation, and progress events) and adds:\n *\n * - **Network presets** via {@link BulletinClient.create} and {@link BulletinChain}.\n * - **Read helpers** ({@link fetchBytes}, {@link fetchJson}) routed through\n * the host's preimage subscription — upstream is upload-only and the SDK\n * is container-only by design (no public-gateway fetches).\n * - **Pre-flight authorization check** ({@link checkAuthorization}) for\n * friendlier UX before submitting a store.\n *\n * For uploads, mirror upstream's fluent builders:\n *\n * ```ts\n * const client = await BulletinClient.create({ environment: \"paseo\", signer });\n * const result = await client.store(data).send();\n * ```\n *\n * For chunked uploads with progress:\n *\n * ```ts\n * const result = await client\n * .store(largeFile)\n * .withChunkSize(1 << 20)\n * .withCallback((evt) => console.log(evt))\n * .send();\n * ```\n */\nexport class BulletinClient {\n /** Underlying upstream client — exposed for power users. */\n readonly inner: AsyncBulletinClient;\n /** Typed Bulletin Chain API. */\n readonly api: BulletinApi;\n\n /** Lazy-resolved host-preimage query strategy, cached for the client lifetime. */\n private queryStrategyPromise: Promise<QueryStrategy> | null = null;\n\n /** Constructed via {@link create} or {@link from}. */\n private constructor(inner: AsyncBulletinClient, api: BulletinApi) {\n this.inner = inner;\n this.api = api;\n }\n\n /** Resolve and cache the host query strategy on first use. */\n private resolveQuery(): Promise<QueryStrategy> {\n if (!this.queryStrategyPromise) {\n this.queryStrategyPromise = resolveQueryStrategy();\n }\n return this.queryStrategyPromise;\n }\n\n /**\n * Create a client from an environment shorthand or an explicit network.\n *\n * Environment form uses our `getChainAPI(env)` to resolve the typed API.\n * Explicit form skips the environment lookup and lets you pass any\n * genesis/descriptor combo.\n *\n * @example\n * ```ts\n * // Shorthand\n * const client = await BulletinClient.create({ environment: \"paseo\", signer });\n *\n * // Explicit (custom network)\n * const client = await BulletinClient.create({\n * ...BulletinChain.paseo,\n * signer,\n * config: { defaultChunkSize: 1 << 20 },\n * });\n * ```\n */\n static async create(options: CreateBulletinClientOptions): Promise<BulletinClient> {\n if (\"environment\" in options) {\n const chain = await getChainAPI(options.environment);\n const inner = new AsyncBulletinClient(\n chain.bulletin as BulletinTypedApi,\n options.signer,\n chain.raw.bulletin.submit as SubmitFn,\n options.config,\n () => chain.destroy(),\n );\n log.info(\"BulletinClient created (environment shorthand)\", {\n environment: options.environment,\n });\n return new BulletinClient(inner, chain.bulletin);\n }\n\n // Explicit form — caller owns the descriptor choice. We still need a\n // PolkadotClient to feed AsyncBulletinClient. Going through\n // chain-client keeps connection management consistent across the SDK.\n const { genesisHash, descriptor, signer, config } = options;\n // Catch the obvious foot-gun where caller mixes a genesis from one\n // network with a descriptor from another — the connection would\n // succeed but typed calls would silently target the wrong chain.\n // The descriptor's own `.genesis` field is the on-chain truth; the\n // user-supplied `genesisHash` is informational today (createChainClient\n // doesn't use it because host routes connections) but kept on the\n // option shape for future RPC-direct paths.\n if (descriptor.genesis && genesisHash.toLowerCase() !== descriptor.genesis.toLowerCase()) {\n throw new Error(\n `BulletinClient.create: genesisHash (${genesisHash}) does not match descriptor.genesis (${descriptor.genesis}). These must refer to the same network — check that you're pairing the right descriptor with the right genesis hash.`,\n );\n }\n const chain = await createChainClient({\n chains: { bulletin: descriptor },\n rpcs: { bulletin: [] },\n });\n const inner = new AsyncBulletinClient(\n chain.bulletin as BulletinTypedApi,\n signer,\n chain.raw.bulletin.submit as SubmitFn,\n config,\n () => chain.destroy(),\n );\n log.info(\"BulletinClient created (explicit network)\");\n return new BulletinClient(inner, chain.bulletin);\n }\n\n /**\n * Construct from a pre-built `AsyncBulletinClient` and PAPI typed API.\n *\n * Use this when you already own the connection lifecycle (BYOD setups,\n * tests). The caller is responsible for calling `papiClient.destroy()`\n * — this client's {@link destroy} only tears down the upstream's\n * `onDestroy` hook.\n */\n static from(inner: AsyncBulletinClient, api: BulletinApi): BulletinClient {\n return new BulletinClient(inner, api);\n }\n\n // ─── Upload + authorization (forwarded to upstream) ────────────────\n\n /** Build a store transaction. See upstream `StoreBuilder` for chained options. */\n store(data: Uint8Array): StoreBuilder {\n return this.inner.store(data);\n }\n\n /** Authorize an account to store data on the chain (sudo required on most networks). */\n authorizeAccount(who: string, transactions: number, bytes: bigint): AuthCallBuilder {\n return this.inner.authorizeAccount(who, transactions, bytes);\n }\n\n /** Authorize content storage by hash (anyone can store; no fees). */\n authorizePreimage(contentHash: Uint8Array, maxSize: bigint): AuthCallBuilder {\n return this.inner.authorizePreimage(contentHash, maxSize);\n }\n\n /** Renew a stored transaction by block + index. */\n renew(block: number, index: number): CallBuilder {\n return this.inner.renew(block, index);\n }\n\n /** Estimate the authorization (transactions + bytes) needed for `dataSize`. */\n estimateAuthorization(dataSize: number): { transactions: number; bytes: number } {\n return this.inner.estimateAuthorization(dataSize);\n }\n\n // ─── Read side (our own helpers) ───────────────────────────────────\n\n /**\n * Fetch raw bytes for a CID via the host's preimage lookup.\n *\n * Container-only — outside a Polkadot Browser / Desktop host this\n * throws {@link BulletinHostUnavailableError}. The chain stores\n * content metadata (`content_hash`, size, codec) but the bytes\n * themselves are surfaced through the host's preimage subscription.\n *\n * Use {@link verifyOnChain} if you only need to confirm a CID was\n * recorded on-chain (no byte fetch).\n */\n async fetchBytes(cid: string, options?: QueryOptions): Promise<Uint8Array> {\n const strategy = await this.resolveQuery();\n return executeQuery(strategy, cid, options);\n }\n\n /** Fetch and parse JSON for a CID. */\n async fetchJson<T>(cid: string, options?: QueryOptions): Promise<T> {\n const bytes = await this.fetchBytes(cid, options);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n }\n\n /** Pre-flight: check whether `address` can store on the bulletin chain. */\n async checkAuthorization(address: string): Promise<AuthorizationStatus> {\n return checkAuthorization(this.api, address);\n }\n\n /**\n * Verify that a CID was recorded on-chain at the given block.\n *\n * Common pattern: pass `blockNumber` (and optionally `extrinsicIndex`)\n * from a `store(...).send()` receipt to confirm the upload landed.\n * See {@link verifyOnChain} for details.\n */\n async verifyOnChain(\n cid: string,\n options: VerifyOnChainOptions,\n ): Promise<ChainStoredEntry | null> {\n return verifyOnChain(this.api, cid, options);\n }\n\n /** Tear down the underlying connection. */\n async destroy(): Promise<void> {\n await this.inner.destroy();\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n describe(\"BulletinClient.from\", () => {\n test(\"constructs with given inner and api\", () => {\n const inner = {\n destroy: vi.fn().mockResolvedValue(undefined),\n } as unknown as AsyncBulletinClient;\n const api = {} as BulletinApi;\n const client = BulletinClient.from(inner, api);\n expect(client.inner).toBe(inner);\n expect(client.api).toBe(api);\n });\n\n test(\"destroy delegates to upstream\", async () => {\n const destroy = vi.fn().mockResolvedValue(undefined);\n const inner = { destroy } as unknown as AsyncBulletinClient;\n const client = BulletinClient.from(inner, {} as BulletinApi);\n await client.destroy();\n expect(destroy).toHaveBeenCalledOnce();\n });\n\n test(\"store delegates to inner\", () => {\n const builder = {} as StoreBuilder;\n const inner = {\n store: vi.fn().mockReturnValue(builder),\n } as unknown as AsyncBulletinClient;\n const client = BulletinClient.from(inner, {} as BulletinApi);\n const data = new Uint8Array([1, 2, 3]);\n expect(client.store(data)).toBe(builder);\n expect(inner.store).toHaveBeenCalledWith(data);\n });\n });\n\n describe(\"BulletinClient.create (BYOD genesis assertion)\", () => {\n // Stand-in descriptor with a known genesis. The full PAPI descriptor\n // type is a `ChainDefinition` with a deep type-level shape; the cast\n // below is fine for the assertion test because we never actually\n // reach createChainClient.\n const stubDescriptor = (\n genesis: `0x${string}`,\n ): (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"] =>\n ({ genesis }) as unknown as (typeof BulletinChain)[BulletinEnvironment][\"descriptor\"];\n\n const realPaseo =\n \"0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea\" as `0x${string}`;\n\n test(\"throws when genesisHash and descriptor.genesis disagree\", async () => {\n await expect(\n BulletinClient.create({\n genesisHash:\n \"0x0000000000000000000000000000000000000000000000000000000000000001\",\n descriptor: stubDescriptor(realPaseo),\n signer: {} as PolkadotSigner,\n }),\n ).rejects.toThrow(/does not match descriptor\\.genesis/i);\n });\n });\n}\n","/**\n * Known Bulletin Chain networks.\n *\n * Pairs each environment with the genesis hash and the PAPI descriptor needed\n * to construct an `AsyncBulletinClient`. Re-uses the descriptor exported by\n * `@parity/product-sdk-descriptors/bulletin` — the bulletin descriptor is the\n * same across all environments today, so the difference between entries is\n * the genesis hash (and, downstream, the chain RPC URL).\n */\nimport { bulletin as bulletinDescriptor } from \"@parity/product-sdk-descriptors/bulletin\";\n\nexport interface BulletinNetwork {\n /** Genesis hash of the bulletin chain on this environment. */\n genesisHash: `0x${string}`;\n /** PAPI descriptor for typed API access. */\n descriptor: typeof bulletinDescriptor;\n}\n\n/**\n * Bulletin Chain network presets.\n *\n * Use these with {@link BulletinClient.create} when you want to be explicit\n * about the network rather than passing an environment string. Reads go\n * through the host's preimage subscription (container-only); no gateway\n * URL is configured per network.\n */\nexport const BulletinChain = {\n paseo: {\n genesisHash: \"0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea\",\n descriptor: bulletinDescriptor,\n },\n} as const satisfies Record<string, BulletinNetwork>;\n\n/** Network keys with built-in presets in {@link BulletinChain}. */\nexport type BulletinEnvironment = keyof typeof BulletinChain;\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"BulletinChain\", () => {\n test(\"paseo has a valid genesis hash\", () => {\n expect(BulletinChain.paseo.genesisHash).toMatch(/^0x[a-f0-9]{64}$/);\n });\n\n test(\"paseo descriptor has matching genesis\", () => {\n expect(BulletinChain.paseo.descriptor.genesis).toBe(BulletinChain.paseo.genesisHash);\n });\n });\n}\n","import type { PolkadotSigner } from \"polkadot-api\";\n\n/**\n * Build a `PolkadotSigner` whose underlying signer is resolved on every call.\n *\n * `AsyncBulletinClient` takes a fixed `PolkadotSigner` at construction, but\n * apps often build the bulletin client before any account is selected. This\n * wrapper defers signer resolution: each call to `signTx` / `signBytes`\n * invokes `getSigner()` and forwards to the result. If the getter returns\n * `null`, calls throw with a clear message.\n *\n * The `publicKey` field is *also* resolved lazily — accessing it before a\n * signer is available throws. This means callers that read `publicKey`\n * eagerly will fail fast with the same error rather than seeing a stale\n * key from a previously-selected account.\n *\n * Account changes between calls are picked up automatically: each sign\n * resolves the current signer.\n */\nexport function createLazySigner(\n getSigner: () => PolkadotSigner | null,\n onMissing = \"No signer available — connect a wallet and select an account first.\",\n): PolkadotSigner {\n const resolve = (): PolkadotSigner => {\n const inner = getSigner();\n if (!inner) throw new Error(onMissing);\n return inner;\n };\n\n // `async` on the methods is deliberate: it converts a \"no signer\" throw\n // from `resolve()` into a rejected Promise. PolkadotSigner.signTx /\n // signBytes are typed as returning Promises, and consumers expect a\n // rejection rather than a synchronous escape on the failure path.\n const lazy: PolkadotSigner = {\n get publicKey() {\n return resolve().publicKey;\n },\n signTx: async (...args: Parameters<PolkadotSigner[\"signTx\"]>) => resolve().signTx(...args),\n signBytes: async (...args: Parameters<PolkadotSigner[\"signBytes\"]>) =>\n resolve().signBytes(...args),\n };\n return lazy;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n function makeMockSigner(label: string): PolkadotSigner {\n return {\n publicKey: new TextEncoder().encode(label),\n signTx: vi.fn().mockResolvedValue(new Uint8Array([1])),\n signBytes: vi.fn().mockResolvedValue(new Uint8Array([2])),\n };\n }\n\n describe(\"createLazySigner\", () => {\n test(\"publicKey throws when getter returns null\", () => {\n const lazy = createLazySigner(() => null);\n expect(() => lazy.publicKey).toThrow(\"No signer available\");\n });\n\n test(\"publicKey resolves through getter when signer is available\", () => {\n const inner = makeMockSigner(\"alice\");\n const lazy = createLazySigner(() => inner);\n expect(lazy.publicKey).toBe(inner.publicKey);\n });\n\n test(\"signTx throws when getter returns null\", async () => {\n const lazy = createLazySigner(() => null);\n await expect(lazy.signTx(new Uint8Array(), {}, new Uint8Array(), 0)).rejects.toThrow(\n \"No signer available\",\n );\n });\n\n test(\"signTx forwards to current signer\", async () => {\n const inner = makeMockSigner(\"alice\");\n const lazy = createLazySigner(() => inner);\n const callData = new Uint8Array([0xaa, 0xbb]);\n const signedExtensions = {};\n const metadata = new Uint8Array([0xcc]);\n const atBlock = 42;\n const result = await lazy.signTx(callData, signedExtensions, metadata, atBlock);\n expect(inner.signTx).toHaveBeenCalledWith(\n callData,\n signedExtensions,\n metadata,\n atBlock,\n );\n expect(result).toEqual(new Uint8Array([1]));\n });\n\n test(\"signBytes forwards to current signer\", async () => {\n const inner = makeMockSigner(\"bob\");\n const lazy = createLazySigner(() => inner);\n const result = await lazy.signBytes(new Uint8Array([9]));\n expect(inner.signBytes).toHaveBeenCalledWith(new Uint8Array([9]));\n expect(result).toEqual(new Uint8Array([2]));\n });\n\n test(\"picks up account changes between calls\", () => {\n let active: PolkadotSigner | null = makeMockSigner(\"first\");\n const lazy = createLazySigner(() => active);\n expect(lazy.publicKey).toEqual(new TextEncoder().encode(\"first\"));\n active = makeMockSigner(\"second\");\n expect(lazy.publicKey).toEqual(new TextEncoder().encode(\"second\"));\n });\n\n test(\"custom error message\", () => {\n const lazy = createLazySigner(() => null, \"select an account first\");\n expect(() => lazy.publicKey).toThrow(\"select an account first\");\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity/product-sdk-bulletin",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "TypeScript SDK for uploading and retrieving data on the Polkadot Bulletin Chain",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -21,8 +21,8 @@
21
21
  "@parity/bulletin-sdk": "^0.3.0",
22
22
  "multiformats": "^13.3.0",
23
23
  "polkadot-api": "^2.1.2",
24
- "@parity/product-sdk-chain-client": "0.2.0",
25
- "@parity/product-sdk-descriptors": "0.2.0",
24
+ "@parity/product-sdk-chain-client": "0.2.1",
25
+ "@parity/product-sdk-descriptors": "0.2.1",
26
26
  "@parity/product-sdk-host": "0.2.0",
27
27
  "@parity/product-sdk-logger": "0.1.1",
28
28
  "@parity/product-sdk-tx": "0.2.0"
@@ -65,10 +65,18 @@ export async function checkAuthorization(
65
65
  return NOT_AUTHORIZED;
66
66
  }
67
67
 
68
+ // After polkadot-bulletin-chain PR #448 (e543696, 2026-04-30), AuthorizationExtent's
69
+ // `transactions` and `bytes` fields are *consumed counters*; the granted allowance moved
70
+ // to `transactions_allowance` / `bytes_allowance`. Compute remaining = allowance − consumed
71
+ // so the public `remainingTransactions` / `remainingBytes` contract stays semantically
72
+ // honest.
68
73
  const status: AuthorizationStatus = {
69
74
  authorized: true,
70
- remainingTransactions: auth.extent.transactions,
71
- remainingBytes: auth.extent.bytes,
75
+ remainingTransactions: auth.extent.transactions_allowance - auth.extent.transactions,
76
+ // `auth` is `any` (TypedApi<any> upstream — see line 53). TS narrows
77
+ // `any - any` to `number`, so cast each u64 operand to bigint to keep
78
+ // the subtraction in bigint space. Runtime values are bigints from PAPI.
79
+ remainingBytes: (auth.extent.bytes_allowance as bigint) - (auth.extent.bytes as bigint),
72
80
  expiration: auth.expiration,
73
81
  };
74
82
 
@@ -246,9 +254,15 @@ if (import.meta.vitest) {
246
254
  expect(status.expiration).toBe(0);
247
255
  });
248
256
 
249
- test("returns authorization with full quota", async () => {
257
+ test("returns authorization with full quota (fresh authorize, nothing consumed)", async () => {
250
258
  const api = createMockApi({
251
- extent: { transactions: 10, bytes: 1_000_000n },
259
+ extent: {
260
+ transactions: 0,
261
+ transactions_allowance: 10,
262
+ bytes: 0n,
263
+ bytes_permanent: 0n,
264
+ bytes_allowance: 1_000_000n,
265
+ },
252
266
  expiration: 999,
253
267
  });
254
268
  const status = await checkAuthorization(api, "5GrwvaEF...");
@@ -259,9 +273,15 @@ if (import.meta.vitest) {
259
273
  expect(status.expiration).toBe(999);
260
274
  });
261
275
 
262
- test("returns authorization with zero transactions remaining", async () => {
276
+ test("returns authorization with zero transactions remaining (fully consumed)", async () => {
263
277
  const api = createMockApi({
264
- extent: { transactions: 0, bytes: 1_000_000n },
278
+ extent: {
279
+ transactions: 5,
280
+ transactions_allowance: 5,
281
+ bytes: 0n,
282
+ bytes_permanent: 0n,
283
+ bytes_allowance: 1_000_000n,
284
+ },
265
285
  expiration: 999,
266
286
  });
267
287
  const status = await checkAuthorization(api, "5GrwvaEF...");
@@ -270,9 +290,15 @@ if (import.meta.vitest) {
270
290
  expect(status.remainingTransactions).toBe(0);
271
291
  });
272
292
 
273
- test("returns authorization with zero bytes remaining", async () => {
293
+ test("returns authorization with zero bytes remaining (fully consumed)", async () => {
274
294
  const api = createMockApi({
275
- extent: { transactions: 5, bytes: 0n },
295
+ extent: {
296
+ transactions: 0,
297
+ transactions_allowance: 5,
298
+ bytes: 1_000n,
299
+ bytes_permanent: 0n,
300
+ bytes_allowance: 1_000n,
301
+ },
276
302
  expiration: 999,
277
303
  });
278
304
  const status = await checkAuthorization(api, "5GrwvaEF...");
@@ -281,9 +307,33 @@ if (import.meta.vitest) {
281
307
  expect(status.remainingBytes).toBe(0n);
282
308
  });
283
309
 
310
+ test("returns remaining = allowance − consumed when partially used", async () => {
311
+ const api = createMockApi({
312
+ extent: {
313
+ transactions: 3,
314
+ transactions_allowance: 10,
315
+ bytes: 250_000n,
316
+ bytes_permanent: 0n,
317
+ bytes_allowance: 1_000_000n,
318
+ },
319
+ expiration: 999,
320
+ });
321
+ const status = await checkAuthorization(api, "5GrwvaEF...");
322
+
323
+ expect(status.authorized).toBe(true);
324
+ expect(status.remainingTransactions).toBe(7); // 10 − 3
325
+ expect(status.remainingBytes).toBe(750_000n); // 1M − 250K
326
+ });
327
+
284
328
  test("preserves expiration block number", async () => {
285
329
  const api = createMockApi({
286
- extent: { transactions: 1, bytes: 500n },
330
+ extent: {
331
+ transactions: 0,
332
+ transactions_allowance: 1,
333
+ bytes: 0n,
334
+ bytes_permanent: 0n,
335
+ bytes_allowance: 500n,
336
+ },
287
337
  expiration: 12345,
288
338
  });
289
339
  const status = await checkAuthorization(api, "5GrwvaEF...");