@parity/product-sdk-contracts 0.2.2 → 0.5.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @parity/product-sdk-contracts
2
2
 
3
- Typed contract interactions on Polkadot Asset Hub. Resolve deployed contracts from a `cdm.json` manifest, get fully-typed handles for `query`, `tx`, and batched `prepare` calls — all backed by the ink SDK.
3
+ Typed contract interactions on Polkadot Asset Hub. Resolve deployed contracts from a `cdm.json` manifest (or directly from `cargo pvm-contract build` artefacts), get fully-typed handles for `query`, `tx`, and batched `prepare` calls — backed by `pallet-revive` via PAPI typed APIs, with viem providing the Solidity ABI codec.
4
4
 
5
5
  ## Install
6
6
 
@@ -10,7 +10,7 @@ pnpm add @parity/product-sdk-contracts
10
10
 
11
11
  ## Quick start (with cdm.json)
12
12
 
13
- The `cdm.json` flow is the primary path. A `cdm.json` manifest in your project root pins each contract to an address + ABI per target chain; `ContractManager.fromClient(cdm, api)` resolves them at runtime.
13
+ The `cdm.json` flow is the primary path. A `cdm.json` manifest in your project root pins each contract to an address + ABI per target chain; `ContractManager.fromClient(cdm, client, descriptor)` resolves them at runtime.
14
14
 
15
15
  ```ts
16
16
  import { createChainClient } from "@parity/product-sdk-chain-client";
@@ -21,13 +21,13 @@ import cdmJson from "./cdm.json";
21
21
 
22
22
  const client = await createChainClient({
23
23
  chains: { assetHub: paseo_asset_hub },
24
- rpcs: { assetHub: ["wss://asset-hub-paseo-rpc.n.dwellir.com"] },
24
+ rpcs: { assetHub: ["wss://paseo-asset-hub-next-rpc.polkadot.io"] },
25
25
  });
26
26
 
27
27
  const signerManager = new SignerManager();
28
28
  await signerManager.connect();
29
29
 
30
- const manager = await ContractManager.fromClient(cdmJson, client.raw.assetHub, {
30
+ const manager = ContractManager.fromClient(cdmJson, client.raw.assetHub, paseo_asset_hub, {
31
31
  signerManager,
32
32
  });
33
33
 
@@ -43,14 +43,15 @@ await registry.publish.tx("my-app00", "ipfs://...", 0);
43
43
 
44
44
  ## `ContractManager` API
45
45
 
46
- ### `ContractManager.fromClient(cdmJson, client, options?)`
46
+ ### `ContractManager.fromClient(cdmJson, client, descriptor, options?)`
47
47
 
48
- Async factory. Lazy-imports `@polkadot-api/sdk-ink` (~4 MB) only when called, so the install footprint stays small for callers who never resolve contracts.
48
+ Synchronous factory. Builds a `ContractRuntime` internally that wires the typed API (for extrinsics + storage) and the unsafe API (for the `ReviveApi.call` dry-run, sidesteps compat-token drift when the descriptor lags a runtime upgrade) on the same `PolkadotClient`.
49
49
 
50
50
  | Param | Type | Notes |
51
51
  | --- | --- | --- |
52
52
  | `cdmJson` | `CdmJson` | Imported `cdm.json` |
53
53
  | `client` | `PolkadotClient` | E.g. `client.raw.assetHub` |
54
+ | `descriptor` | `ChainDefinition` | E.g. `paseo_asset_hub` from `@parity/product-sdk-descriptors` |
54
55
  | `options.signerManager?` | `SignerManager` | Resolves signer + origin from the logged-in account |
55
56
  | `options.defaultOrigin?` | `SS58String` | Static fallback origin for queries |
56
57
  | `options.defaultSigner?` | `PolkadotSigner` | Static fallback signer for txs |
@@ -95,6 +96,23 @@ Order, highest wins:
95
96
 
96
97
  Throws `ContractSignerMissingError` from `.tx()` if no signer is available. `.query()` and `.prepare()` never need a signer.
97
98
 
99
+ ## Account mapping (`pallet-revive` prerequisite)
100
+
101
+ `pallet-revive` requires every signing SS58 account to have a registered `OriginalAccount` mapping to its derived H160 before `Revive.call` extrinsics from it are accepted. Call `ensureContractAccountMapped(runtime, address, signer)` once at app boot per signing account — it's idempotent (reads `Revive.OriginalAccount` first, short-circuits with `null` if already mapped) so the worst case is one extra storage read on every boot.
102
+
103
+ ```ts
104
+ import { ensureContractAccountMapped } from "@parity/product-sdk-contracts";
105
+
106
+ await ensureContractAccountMapped(manager.getRuntime(), address, signer);
107
+ // safe to call manager.getContract(...).<method>.tx({ signer }) from here
108
+ ```
109
+
110
+ Without this, every fresh-account `.tx()` fails the pre-flight dry-run with `AccountNotMapped` before signing.
111
+
112
+ ## Dry-run preflight
113
+
114
+ Every `.tx()` runs a `ReviveApi.call` dry-run first to size `weight_limit` / `storage_deposit_limit` and to fail fast on revert, OOG, or `AccountNotMapped` before any signing happens. Throws `ContractDryRunFailedError` (with the chain's `dispatchError`) when the dry-run reports failure — caller pays no gas on a tx the chain already rejected. Pass both `gasLimit` and `storageDepositLimit` overrides on `TxOptions` to skip the dry-run entirely.
115
+
98
116
  ## Batching with `.prepare()`
99
117
 
100
118
  Use `.prepare()` to build `BatchableCall` handles consumable by `batchSubmitAndWatch` from `@parity/product-sdk-tx`. Combine multiple contract calls — or contract calls mixed with other Asset Hub transactions — into a single atomic `Utility.batch_all` extrinsic.
@@ -118,9 +136,11 @@ If you don't have a `cdm.json` (or want to test against a contract not yet in yo
118
136
 
119
137
  ```ts
120
138
  import { createContractFromClient } from "@parity/product-sdk-contracts";
139
+ import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
121
140
 
122
- const counter = await createContractFromClient(
141
+ const counter = createContractFromClient(
123
142
  client.raw.assetHub,
143
+ paseo_asset_hub,
124
144
  "0xC472...",
125
145
  counterAbi,
126
146
  { signerManager },
@@ -130,7 +150,9 @@ const { value } = await counter.getCount.query();
130
150
  await counter.increment.tx();
131
151
  ```
132
152
 
133
- `createContract` is the same but takes a pre-created `InkSdk` if you want to control when `@polkadot-api/sdk-ink` loads.
153
+ `createContract` is the same but takes a pre-built `ContractRuntime` (from `createContractRuntime` / `createContractRuntimeFromClient`) instead of a raw `PolkadotClient` + descriptor. Use it when you want to share a single runtime across multiple contract handles or wire your own dry-run path.
154
+
155
+ For projects that build with `cargo pvm-contract` and don't want to maintain a `cdm.json`, the `@parity/product-sdk-contracts/pvm` subpath exports `parsePvmContractAbi`, `loadPvmContractAbi`, `loadPvmContractCode`, and `loadPvmContractArtifacts` — load the ABI (and bytecode for deployment) directly from the toolchain's `<name>.release.abi.json` / `<name>.release.polkavm` output.
134
156
 
135
157
  ## cdm.json schema
136
158
 
@@ -140,8 +162,8 @@ Top-level shape:
140
162
  {
141
163
  "targets": {
142
164
  "<targetHash>": {
143
- "asset-hub": "wss://asset-hub-paseo-rpc.n.dwellir.com",
144
- "bulletin": "https://paseo-ipfs.polkadot.io/ipfs"
165
+ "asset-hub": "wss://paseo-asset-hub-next-rpc.polkadot.io",
166
+ "bulletin": "https://paseo-bulletin-next-ipfs.polkadot.io"
145
167
  }
146
168
  },
147
169
  "dependencies": {
@@ -170,11 +192,15 @@ A real-world example: [`paritytech/playground-cli/cdm.json`](https://github.com/
170
192
  `generateContractTypes(...)` (from `@parity/product-sdk-contracts/codegen`) emits a `.d.ts` that augments the package's `Contracts` interface so `manager.getContract("@org/name")` returns a fully-typed handle:
171
193
 
172
194
  ```ts
173
- import { generateContractTypes } from "@parity/product-sdk-contracts/codegen";
174
- import cdm from "./cdm.json";
195
+ import { generateContractTypes, resolveContractTypeInputs } from "@parity/product-sdk-contracts/codegen";
175
196
  import { writeFileSync } from "node:fs";
176
197
 
177
- const src = generateContractTypes(cdm);
198
+ // Mix inline ABIs (from your cdm.json) and cargo-pvm-contract build artefacts.
199
+ const resolved = await resolveContractTypeInputs([
200
+ { library: "@example/counter", abiPath: "./target/counter.release.abi.json" },
201
+ { library: "@example/inline", abi: inlineAbi },
202
+ ]);
203
+ const src = generateContractTypes(resolved);
178
204
  writeFileSync(".cdm/contracts.d.ts", src);
179
205
  ```
180
206
 
@@ -188,21 +214,30 @@ Without codegen, `getContract()` still works — methods are accessible but unty
188
214
  | --- | --- |
189
215
  | `ContractNotFoundError` | `getContract(name)` and `name` isn't in the manifest for the active target |
190
216
  | `ContractSignerMissingError` | `.tx()` called with no signer + no signerManager + no defaultSigner |
191
- | Generic | ABI decode failures, RPC errors, gas estimation failures surface from the underlying ink SDK |
217
+ | `ContractDryRunFailedError` | `.tx()` pre-flight `ReviveApi.call` reported failure `dispatchError` carries the chain's encoded error (e.g. `ContractReverted`, `OutOfGas`, `AccountNotMapped`) |
218
+ | Generic | viem ABI decode failures, RPC errors, weight/storage-limit estimation failures — surface from the underlying PAPI typed API |
192
219
 
193
220
  ## Public API
194
221
 
195
222
  ```ts
223
+ // `@parity/product-sdk-contracts` (browser-safe runtime entry)
196
224
  export {
197
225
  ContractManager,
198
226
  createContract,
199
227
  createContractFromClient,
200
- generateContractTypes,
228
+ createContractRuntime,
229
+ createContractRuntimeFromClient,
230
+ ensureContractAccountMapped,
201
231
  ContractError,
202
232
  ContractSignerMissingError,
203
233
  ContractNotFoundError,
234
+ ContractDryRunFailedError,
204
235
  };
205
236
  export type {
237
+ ContractRuntime,
238
+ ReviveTypedApi,
239
+ ReviveDryRunResult,
240
+ ReviveDryRunCall,
206
241
  CdmJson,
207
242
  CdmJsonTarget,
208
243
  CdmJsonContract,
@@ -221,4 +256,17 @@ export type {
221
256
  ContractManagerOptions,
222
257
  ContractOptions,
223
258
  };
259
+
260
+ // `@parity/product-sdk-contracts/codegen` (Node-only — build tooling)
261
+ export { generateContractTypes, resolveContractTypeInputs };
262
+ export type { ContractTypeInput };
263
+
264
+ // `@parity/product-sdk-contracts/pvm` (Node-only — cargo-pvm-contract artefact loaders)
265
+ export {
266
+ parsePvmContractAbi,
267
+ loadPvmContractAbi,
268
+ loadPvmContractCode,
269
+ loadPvmContractArtifacts,
270
+ };
271
+ export type { PvmContractArtifacts };
224
272
  ```
@@ -0,0 +1,53 @@
1
+ // src/pvm.ts
2
+ function parsePvmContractAbi(source) {
3
+ let value = source;
4
+ if (value instanceof Uint8Array) {
5
+ value = new TextDecoder().decode(value);
6
+ }
7
+ if (typeof value === "string") {
8
+ try {
9
+ value = JSON.parse(value);
10
+ } catch (cause) {
11
+ throw new Error("Invalid PVM ABI: not valid JSON", { cause });
12
+ }
13
+ }
14
+ if (value && typeof value === "object" && !Array.isArray(value) && "abi" in value) {
15
+ value = value.abi;
16
+ }
17
+ if (!Array.isArray(value)) {
18
+ throw new Error("Invalid PVM ABI: expected an array of ABI entries");
19
+ }
20
+ for (const entry of value) {
21
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
22
+ throw new Error(
23
+ "Invalid PVM ABI: every entry must have a string `type` (function/event/constructor/...)"
24
+ );
25
+ }
26
+ const inputs = entry.inputs;
27
+ if (inputs !== void 0 && !Array.isArray(inputs)) {
28
+ throw new Error("Invalid PVM ABI: `inputs` must be an array when present");
29
+ }
30
+ }
31
+ return value;
32
+ }
33
+ async function loadPvmContractAbi(path) {
34
+ const { readFile } = await import('fs/promises');
35
+ const buf = await readFile(path);
36
+ return parsePvmContractAbi(buf);
37
+ }
38
+ async function loadPvmContractCode(path) {
39
+ const { readFile } = await import('fs/promises');
40
+ const buf = await readFile(path);
41
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
42
+ }
43
+ async function loadPvmContractArtifacts(basePath) {
44
+ const [abi, bytecode] = await Promise.all([
45
+ loadPvmContractAbi(`${basePath}.abi.json`),
46
+ loadPvmContractCode(`${basePath}.polkavm`)
47
+ ]);
48
+ return { abi, bytecode };
49
+ }
50
+
51
+ export { loadPvmContractAbi, loadPvmContractArtifacts, loadPvmContractCode, parsePvmContractAbi };
52
+ //# sourceMappingURL=chunk-YZ3YBERU.js.map
53
+ //# sourceMappingURL=chunk-YZ3YBERU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pvm.ts"],"names":[],"mappings":";AAmBO,SAAS,oBAAoB,MAAA,EAA6B;AAC7D,EAAA,IAAI,KAAA,GAAiB,MAAA;AAErB,EAAA,IAAI,iBAAiB,UAAA,EAAY;AAC7B,IAAA,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA;AAAA,EAC1C;AACA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,IAAA,IAAI;AACA,MAAA,KAAA,GAAQ,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,IAC5B,SAAS,KAAA,EAAO;AACZ,MAAA,MAAM,IAAI,KAAA,CAAM,iCAAA,EAAmC,EAAE,OAAO,CAAA;AAAA,IAChE;AAAA,EACJ;AACA,EAAA,IAAI,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,MAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,IAAS,KAAA,EAAO;AAC/E,IAAA,KAAA,GAAS,KAAA,CAA2B,GAAA;AAAA,EACxC;AACA,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACvE;AACA,EAAA,KAAA,MAAW,SAAS,KAAA,EAAO;AACvB,IAAA,IAAI,CAAC,SAAS,OAAO,KAAA,KAAU,YAAY,OAAQ,KAAA,CAAmB,SAAS,QAAA,EAAU;AACrF,MAAA,MAAM,IAAI,KAAA;AAAA,QACN;AAAA,OACJ;AAAA,IACJ;AACA,IAAA,MAAM,SAAU,KAAA,CAAmB,MAAA;AACnC,IAAA,IAAI,WAAW,MAAA,IAAa,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AAChD,MAAA,MAAM,IAAI,MAAM,yDAAyD,CAAA;AAAA,IAC7E;AAAA,EACJ;AACA,EAAA,OAAO,KAAA;AACX;AAOA,eAAsB,mBAAmB,IAAA,EAAmC;AACxE,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,MAAM,OAAO,aAAkB,CAAA;AACpD,EAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,IAAI,CAAA;AAC/B,EAAA,OAAO,oBAAoB,GAAG,CAAA;AAClC;AAYA,eAAsB,oBAAoB,IAAA,EAAmC;AACzE,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,MAAM,OAAO,aAAkB,CAAA;AACpD,EAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,IAAI,CAAA;AAC/B,EAAA,OAAO,IAAI,UAAA,CAAW,GAAA,CAAI,QAAQ,GAAA,CAAI,UAAA,EAAY,IAAI,UAAU,CAAA;AACpE;AAYA,eAAsB,yBAAyB,QAAA,EAAiD;AAC5F,EAAA,MAAM,CAAC,GAAA,EAAK,QAAQ,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IACtC,kBAAA,CAAmB,CAAA,EAAG,QAAQ,CAAA,SAAA,CAAW,CAAA;AAAA,IACzC,mBAAA,CAAoB,CAAA,EAAG,QAAQ,CAAA,QAAA,CAAU;AAAA,GAC5C,CAAA;AACD,EAAA,OAAO,EAAE,KAAK,QAAA,EAAS;AAC3B","file":"chunk-YZ3YBERU.js","sourcesContent":["import type { AbiEntry } from \"./types.js\";\n\n/** ABI + PolkaVM bytecode pair emitted by `cargo pvm-contract build`. */\nexport interface PvmContractArtifacts {\n abi: AbiEntry[];\n bytecode: Uint8Array;\n}\n\n/**\n * Parse an in-memory cargo-pvm-contract ABI artifact.\n *\n * Accepts the shapes the toolchain may produce or that products may pass:\n * - parsed JSON array — `AbiEntry[]`\n * - parsed JSON object with an `abi` property — `{ abi: AbiEntry[] }`\n * - JSON string of either of the above\n * - `Uint8Array` containing UTF-8 JSON of either of the above\n *\n * @throws if the input cannot be coerced to a non-empty `AbiEntry[]`.\n */\nexport function parsePvmContractAbi(source: unknown): AbiEntry[] {\n let value: unknown = source;\n\n if (value instanceof Uint8Array) {\n value = new TextDecoder().decode(value);\n }\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch (cause) {\n throw new Error(\"Invalid PVM ABI: not valid JSON\", { cause });\n }\n }\n if (value && typeof value === \"object\" && !Array.isArray(value) && \"abi\" in value) {\n value = (value as { abi: unknown }).abi;\n }\n if (!Array.isArray(value)) {\n throw new Error(\"Invalid PVM ABI: expected an array of ABI entries\");\n }\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\" || typeof (entry as AbiEntry).type !== \"string\") {\n throw new Error(\n \"Invalid PVM ABI: every entry must have a string `type` (function/event/constructor/...)\",\n );\n }\n const inputs = (entry as AbiEntry).inputs;\n if (inputs !== undefined && !Array.isArray(inputs)) {\n throw new Error(\"Invalid PVM ABI: `inputs` must be an array when present\");\n }\n }\n return value as AbiEntry[];\n}\n\n/**\n * Read a cargo-pvm-contract ABI file from disk and parse it.\n *\n * Node-only. For browser/in-memory inputs use {@link parsePvmContractAbi}.\n */\nexport async function loadPvmContractAbi(path: string): Promise<AbiEntry[]> {\n const { readFile } = await import(\"node:fs/promises\");\n const buf = await readFile(path);\n return parsePvmContractAbi(buf);\n}\n\n/**\n * Read the `.polkavm` bytecode artifact produced by `cargo pvm-contract build`.\n *\n * Returned bytes are ready to hand to `Revive.instantiate_with_code` (or to\n * any future deploy helper layered on top of it). Use this when you already\n * have an ABI in hand (e.g. inline or fetched separately) and only need the\n * PolkaVM blob — otherwise prefer {@link loadPvmContractArtifacts}.\n *\n * Node-only.\n */\nexport async function loadPvmContractCode(path: string): Promise<Uint8Array> {\n const { readFile } = await import(\"node:fs/promises\");\n const buf = await readFile(path);\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n}\n\n/**\n * Read both the `.abi.json` and `.polkavm` artifacts produced by\n * `cargo pvm-contract build` for a given base path.\n *\n * `basePath` is the path prefix shared by both files — typically\n * `target/<name>.release`. The function reads `${basePath}.abi.json` and\n * `${basePath}.polkavm`.\n *\n * Node-only.\n */\nexport async function loadPvmContractArtifacts(basePath: string): Promise<PvmContractArtifacts> {\n const [abi, bytecode] = await Promise.all([\n loadPvmContractAbi(`${basePath}.abi.json`),\n loadPvmContractCode(`${basePath}.polkavm`),\n ]);\n return { abi, bytecode };\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe, beforeAll, afterAll } = import.meta.vitest;\n\n const sampleAbi: AbiEntry[] = [\n { type: \"constructor\", inputs: [], stateMutability: \"nonpayable\" },\n {\n type: \"function\",\n name: \"increment\",\n inputs: [],\n outputs: [],\n stateMutability: \"nonpayable\",\n },\n {\n type: \"function\",\n name: \"get\",\n inputs: [],\n outputs: [{ name: \"\", type: \"uint32\" }],\n stateMutability: \"view\",\n },\n ];\n\n describe(\"parsePvmContractAbi\", () => {\n test(\"accepts a parsed AbiEntry[] array directly\", () => {\n expect(parsePvmContractAbi(sampleAbi)).toEqual(sampleAbi);\n });\n\n test(\"accepts a wrapped { abi } object\", () => {\n expect(parsePvmContractAbi({ abi: sampleAbi })).toEqual(sampleAbi);\n });\n\n test(\"accepts a JSON string of an array\", () => {\n expect(parsePvmContractAbi(JSON.stringify(sampleAbi))).toEqual(sampleAbi);\n });\n\n test(\"accepts a JSON string of a wrapped object\", () => {\n expect(parsePvmContractAbi(JSON.stringify({ abi: sampleAbi }))).toEqual(sampleAbi);\n });\n\n test(\"accepts a UTF-8 Uint8Array\", () => {\n const bytes = new TextEncoder().encode(JSON.stringify(sampleAbi));\n expect(parsePvmContractAbi(bytes)).toEqual(sampleAbi);\n });\n\n test(\"throws on invalid JSON string\", () => {\n expect(() => parsePvmContractAbi(\"{not json\")).toThrow(/not valid JSON/);\n });\n\n test(\"throws when input is not an array\", () => {\n expect(() => parsePvmContractAbi(42)).toThrow(/expected an array/);\n expect(() => parsePvmContractAbi({ foo: \"bar\" })).toThrow(/expected an array/);\n });\n\n test(\"throws when an entry is missing `type`\", () => {\n expect(() => parsePvmContractAbi([{ name: \"noType\" }])).toThrow(/string `type`/);\n });\n\n test(\"throws when `inputs` is not an array\", () => {\n expect(() =>\n parsePvmContractAbi([{ type: \"function\", inputs: \"not an array\" }]),\n ).toThrow(/`inputs` must be an array/);\n });\n\n test(\"treats null as invalid\", () => {\n expect(() => parsePvmContractAbi(null)).toThrow();\n });\n });\n\n describe(\"loadPvmContractAbi / loadPvmContractArtifacts\", () => {\n // Cover the Node-only filesystem helpers via a real tmpdir round-trip.\n // The cargo-pvm-contract toolchain emits files at\n // target/<name>.release.abi.json\n // target/<name>.release.polkavm\n // — we recreate that layout here.\n let dir = \"\";\n let base = \"\";\n let lonely = \"\";\n let badAbi = \"\";\n // Minimal PolkaVM magic (`PVM\\0`) is enough to exercise the path —\n // we don't validate bytecode contents in the loader.\n const fakeBytecode = new Uint8Array([0x50, 0x56, 0x4d, 0x00, 0x01, 0x02, 0x03]);\n\n beforeAll(async () => {\n const { mkdtempSync, writeFileSync } = await import(\"node:fs\");\n const { tmpdir } = await import(\"node:os\");\n const { join } = await import(\"node:path\");\n dir = mkdtempSync(join(tmpdir(), \"pvm-loader-test-\"));\n base = join(dir, \"counter.release\");\n lonely = join(dir, \"lonely.release\");\n badAbi = join(dir, \"bad.release\");\n writeFileSync(`${base}.abi.json`, JSON.stringify(sampleAbi));\n writeFileSync(`${base}.polkavm`, fakeBytecode);\n writeFileSync(`${lonely}.abi.json`, JSON.stringify(sampleAbi));\n writeFileSync(`${badAbi}.abi.json`, \"{not valid json\");\n });\n\n afterAll(async () => {\n const { rmSync } = await import(\"node:fs\");\n try {\n rmSync(dir, { recursive: true, force: true });\n } catch {\n /* ignore */\n }\n });\n\n test(\"loadPvmContractAbi parses a JSON file from disk\", async () => {\n const abi = await loadPvmContractAbi(`${base}.abi.json`);\n expect(abi).toEqual(sampleAbi);\n });\n\n test(\"loadPvmContractArtifacts reads abi + bytecode pair\", async () => {\n const out = await loadPvmContractArtifacts(base);\n expect(out.abi).toEqual(sampleAbi);\n expect(out.bytecode).toBeInstanceOf(Uint8Array);\n expect(Array.from(out.bytecode)).toEqual(Array.from(fakeBytecode));\n });\n\n test(\"loadPvmContractCode reads only the .polkavm blob\", async () => {\n const code = await loadPvmContractCode(`${base}.polkavm`);\n expect(code).toBeInstanceOf(Uint8Array);\n expect(Array.from(code)).toEqual(Array.from(fakeBytecode));\n });\n\n test(\"loadPvmContractAbi rejects a missing file\", async () => {\n await expect(loadPvmContractAbi(`${base}.does-not-exist`)).rejects.toThrow();\n });\n\n test(\"loadPvmContractArtifacts rejects when bytecode is missing\", async () => {\n // Only `${lonely}.abi.json` exists — `.polkavm` is absent.\n await expect(loadPvmContractArtifacts(lonely)).rejects.toThrow();\n });\n\n test(\"loadPvmContractAbi propagates parse errors with helpful message\", async () => {\n await expect(loadPvmContractAbi(`${badAbi}.abi.json`)).rejects.toThrow(\n /not valid JSON/,\n );\n });\n });\n}\n"]}
package/dist/codegen.d.ts CHANGED
@@ -1,4 +1,65 @@
1
- export { k as generateContractTypes } from './codegen-BPDBGrJC.js';
1
+ import { A as AbiEntry } from './types-BdHp-xWt.js';
2
2
  import 'polkadot-api';
3
3
  import '@parity/product-sdk-tx';
4
4
  import '@parity/product-sdk-signer';
5
+
6
+ /** A contract input to {@link generateContractTypes}: either an inline ABI or a `cargo-pvm-contract` artefact path. */
7
+ type ContractTypeInput = {
8
+ library: string;
9
+ abi: AbiEntry[];
10
+ } | {
11
+ library: string;
12
+ abiPath: string;
13
+ };
14
+ /**
15
+ * Resolve a heterogeneous list of {@link ContractTypeInput} entries into the
16
+ * `{ library, abi }` shape consumed by {@link generateContractTypes}. Reads any
17
+ * `abiPath` entries off disk via {@link loadPvmContractAbi} (Node only).
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const resolved = await resolveContractTypeInputs([
22
+ * { library: "@example/counter", abiPath: "./target/counter.release.abi.json" },
23
+ * { library: "@example/inline", abi: counterAbi },
24
+ * ]);
25
+ * const src = generateContractTypes(resolved);
26
+ * ```
27
+ */
28
+ declare function resolveContractTypeInputs(inputs: readonly ContractTypeInput[]): Promise<{
29
+ library: string;
30
+ abi: AbiEntry[];
31
+ }[]>;
32
+ /**
33
+ * Generate a TypeScript module augmentation that extends the
34
+ * {@link Contracts} interface with typed method signatures for each
35
+ * installed contract.
36
+ *
37
+ * The output is written to `.cdm/contracts.d.ts` (or equivalent) and
38
+ * augments `"@parity/product-sdk-contracts"` so that
39
+ * `ContractManager.getContract()` returns fully-typed handles.
40
+ *
41
+ * Accepts `{ library, abi }` directly. To pull ABIs from
42
+ * `cargo-pvm-contract` build artefacts on disk, use
43
+ * {@link resolveContractTypeInputs} first.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // Inline ABIs
48
+ * const src = generateContractTypes([
49
+ * { library: "@example/counter", abi },
50
+ * ]);
51
+ * writeFileSync(".cdm/contracts.d.ts", src);
52
+ *
53
+ * // From cargo-pvm-contract artefacts
54
+ * const resolved = await resolveContractTypeInputs([
55
+ * { library: "@example/counter", abiPath: "./target/counter.release.abi.json" },
56
+ * ]);
57
+ * writeFileSync(".cdm/contracts.d.ts", generateContractTypes(resolved));
58
+ * ```
59
+ */
60
+ declare function generateContractTypes(contracts: {
61
+ library: string;
62
+ abi: AbiEntry[];
63
+ }[]): string;
64
+
65
+ export { type ContractTypeInput, generateContractTypes, resolveContractTypeInputs };
package/dist/codegen.js CHANGED
@@ -1,3 +1,83 @@
1
- export { generateContractTypes } from './chunk-E7AP65D4.js';
1
+ import { loadPvmContractAbi } from './chunk-YZ3YBERU.js';
2
+
3
+ // src/codegen.ts
4
+ async function resolveContractTypeInputs(inputs) {
5
+ return Promise.all(
6
+ inputs.map(async (input) => {
7
+ if ("abi" in input) return { library: input.library, abi: input.abi };
8
+ return { library: input.library, abi: await loadPvmContractAbi(input.abiPath) };
9
+ })
10
+ );
11
+ }
12
+ function mapSolidityType(param) {
13
+ const t = param.type;
14
+ if (t.endsWith("[]")) {
15
+ const inner = { ...param, type: t.slice(0, -2) };
16
+ return `${mapSolidityType(inner)}[]`;
17
+ }
18
+ const fixedArrayMatch = t.match(/^(.+)\[(\d+)\]$/);
19
+ if (fixedArrayMatch) {
20
+ const inner = { ...param, type: fixedArrayMatch[1] };
21
+ return `${mapSolidityType(inner)}[]`;
22
+ }
23
+ if (t === "tuple" && param.components) {
24
+ const fields = param.components.map((c) => `${c.name}: ${mapSolidityType(c)}`);
25
+ return `{ ${fields.join("; ")} }`;
26
+ }
27
+ if (/^uint(8|16|32)$/.test(t)) return "number";
28
+ if (/^uint\d*$/.test(t)) return "bigint";
29
+ if (/^int(8|16|32)$/.test(t)) return "number";
30
+ if (/^int\d*$/.test(t)) return "bigint";
31
+ if (t === "address") return "HexString";
32
+ if (t === "bytes") return "HexString";
33
+ const bytesMatch = t.match(/^bytes(\d+)$/);
34
+ if (bytesMatch) return `SizedHex<${bytesMatch[1]}>`;
35
+ if (t === "bool") return "boolean";
36
+ if (t === "string") return "string";
37
+ return "unknown";
38
+ }
39
+ function generateMethodArgsType(inputs) {
40
+ if (inputs.length === 0) return "[]";
41
+ const parts = inputs.map((p) => `${p.name}: ${mapSolidityType(p)}`);
42
+ return `[${parts.join(", ")}]`;
43
+ }
44
+ function generateMethodResponseType(outputs) {
45
+ if (!outputs || outputs.length === 0) return "undefined";
46
+ if (outputs.length === 1) return mapSolidityType(outputs[0]);
47
+ const fields = outputs.map((o, i) => {
48
+ const name = o.name || `_${i}`;
49
+ return `${name}: ${mapSolidityType(o)}`;
50
+ });
51
+ return `{ ${fields.join("; ")} }`;
52
+ }
53
+ function generateContractTypes(contracts) {
54
+ const lines = [
55
+ "// Auto-generated by cdm install \u2014 do not edit",
56
+ 'import type { HexString, SizedHex } from "polkadot-api";',
57
+ "",
58
+ 'declare module "@parity/product-sdk-contracts" {',
59
+ " interface Contracts {"
60
+ ];
61
+ for (const contract of contracts) {
62
+ const methods = contract.abi.filter((e) => e.type === "function" && e.name);
63
+ lines.push(` "${contract.library}": {`);
64
+ lines.push(" methods: {");
65
+ for (const method of methods) {
66
+ const args = generateMethodArgsType(method.inputs);
67
+ const response = generateMethodResponseType(method.outputs);
68
+ lines.push(
69
+ ` ${method.name}: { args: ${args}; response: ${response} };`
70
+ );
71
+ }
72
+ lines.push(" };");
73
+ lines.push(" };");
74
+ }
75
+ lines.push(" }");
76
+ lines.push("}");
77
+ lines.push("");
78
+ return lines.join("\n");
79
+ }
80
+
81
+ export { generateContractTypes, resolveContractTypeInputs };
2
82
  //# sourceMappingURL=codegen.js.map
3
83
  //# sourceMappingURL=codegen.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"codegen.js"}
1
+ {"version":3,"sources":["../src/codegen.ts"],"names":[],"mappings":"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","file":"codegen.js","sourcesContent":["import type { AbiEntry, AbiParam } from \"./types.js\";\nimport { loadPvmContractAbi } from \"./pvm.js\";\n\n/** A contract input to {@link generateContractTypes}: either an inline ABI or a `cargo-pvm-contract` artefact path. */\nexport type ContractTypeInput =\n | { library: string; abi: AbiEntry[] }\n | { library: string; abiPath: string };\n\n/**\n * Resolve a heterogeneous list of {@link ContractTypeInput} entries into the\n * `{ library, abi }` shape consumed by {@link generateContractTypes}. Reads any\n * `abiPath` entries off disk via {@link loadPvmContractAbi} (Node only).\n *\n * @example\n * ```ts\n * const resolved = await resolveContractTypeInputs([\n * { library: \"@example/counter\", abiPath: \"./target/counter.release.abi.json\" },\n * { library: \"@example/inline\", abi: counterAbi },\n * ]);\n * const src = generateContractTypes(resolved);\n * ```\n */\nexport async function resolveContractTypeInputs(\n inputs: readonly ContractTypeInput[],\n): Promise<{ library: string; abi: AbiEntry[] }[]> {\n return Promise.all(\n inputs.map(async (input) => {\n if (\"abi\" in input) return { library: input.library, abi: input.abi };\n return { library: input.library, abi: await loadPvmContractAbi(input.abiPath) };\n }),\n );\n}\n\n/** Map a Solidity ABI type to its TypeScript equivalent. */\nfunction mapSolidityType(param: AbiParam): string {\n const t = param.type;\n\n // Dynamic arrays — e.g. uint256[]\n if (t.endsWith(\"[]\")) {\n const inner = { ...param, type: t.slice(0, -2) };\n return `${mapSolidityType(inner)}[]`;\n }\n\n // Fixed-size arrays — e.g. uint256[3]\n const fixedArrayMatch = t.match(/^(.+)\\[(\\d+)\\]$/);\n if (fixedArrayMatch) {\n const inner = { ...param, type: fixedArrayMatch[1] };\n return `${mapSolidityType(inner)}[]`;\n }\n\n // Tuple\n if (t === \"tuple\" && param.components) {\n const fields = param.components.map((c) => `${c.name}: ${mapSolidityType(c)}`);\n return `{ ${fields.join(\"; \")} }`;\n }\n\n // Unsigned integers — uint8/16/32 fit in JS number\n if (/^uint(8|16|32)$/.test(t)) return \"number\";\n if (/^uint\\d*$/.test(t)) return \"bigint\";\n\n // Signed integers\n if (/^int(8|16|32)$/.test(t)) return \"number\";\n if (/^int\\d*$/.test(t)) return \"bigint\";\n\n // Address\n if (t === \"address\") return \"HexString\";\n\n // Bytes — viem's ABI codec accepts hex strings (`0x…`) for both\n // variable-length `bytes` and fixed-length `bytesN`. We surface\n // `SizedHex<N>` for the latter so consumers see the byte width at\n // compile time, even though the runtime check is just on the prefix.\n if (t === \"bytes\") return \"HexString\";\n const bytesMatch = t.match(/^bytes(\\d+)$/);\n if (bytesMatch) return `SizedHex<${bytesMatch[1]}>`;\n\n // Primitives\n if (t === \"bool\") return \"boolean\";\n if (t === \"string\") return \"string\";\n\n return \"unknown\";\n}\n\nfunction generateMethodArgsType(inputs: AbiParam[]): string {\n if (inputs.length === 0) return \"[]\";\n const parts = inputs.map((p) => `${p.name}: ${mapSolidityType(p)}`);\n return `[${parts.join(\", \")}]`;\n}\n\nfunction generateMethodResponseType(outputs: AbiParam[] | undefined): string {\n if (!outputs || outputs.length === 0) return \"undefined\";\n if (outputs.length === 1) return mapSolidityType(outputs[0]);\n const fields = outputs.map((o, i) => {\n const name = o.name || `_${i}`;\n return `${name}: ${mapSolidityType(o)}`;\n });\n return `{ ${fields.join(\"; \")} }`;\n}\n\n/**\n * Generate a TypeScript module augmentation that extends the\n * {@link Contracts} interface with typed method signatures for each\n * installed contract.\n *\n * The output is written to `.cdm/contracts.d.ts` (or equivalent) and\n * augments `\"@parity/product-sdk-contracts\"` so that\n * `ContractManager.getContract()` returns fully-typed handles.\n *\n * Accepts `{ library, abi }` directly. To pull ABIs from\n * `cargo-pvm-contract` build artefacts on disk, use\n * {@link resolveContractTypeInputs} first.\n *\n * @example\n * ```ts\n * // Inline ABIs\n * const src = generateContractTypes([\n * { library: \"@example/counter\", abi },\n * ]);\n * writeFileSync(\".cdm/contracts.d.ts\", src);\n *\n * // From cargo-pvm-contract artefacts\n * const resolved = await resolveContractTypeInputs([\n * { library: \"@example/counter\", abiPath: \"./target/counter.release.abi.json\" },\n * ]);\n * writeFileSync(\".cdm/contracts.d.ts\", generateContractTypes(resolved));\n * ```\n */\nexport function generateContractTypes(contracts: { library: string; abi: AbiEntry[] }[]): string {\n const lines: string[] = [\n \"// Auto-generated by cdm install — do not edit\",\n 'import type { HexString, SizedHex } from \"polkadot-api\";',\n \"\",\n 'declare module \"@parity/product-sdk-contracts\" {',\n \" interface Contracts {\",\n ];\n\n for (const contract of contracts) {\n const methods = contract.abi.filter((e) => e.type === \"function\" && e.name);\n lines.push(` \"${contract.library}\": {`);\n lines.push(\" methods: {\");\n for (const method of methods) {\n const args = generateMethodArgsType(method.inputs);\n const response = generateMethodResponseType(method.outputs);\n lines.push(\n ` ${method.name!}: { args: ${args}; response: ${response} };`,\n );\n }\n lines.push(\" };\");\n lines.push(\" };\");\n }\n\n lines.push(\" }\");\n lines.push(\"}\");\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n\n describe(\"mapSolidityType\", () => {\n const cases: [AbiParam, string][] = [\n [{ name: \"x\", type: \"uint8\" }, \"number\"],\n [{ name: \"x\", type: \"uint16\" }, \"number\"],\n [{ name: \"x\", type: \"uint32\" }, \"number\"],\n [{ name: \"x\", type: \"uint64\" }, \"bigint\"],\n [{ name: \"x\", type: \"uint128\" }, \"bigint\"],\n [{ name: \"x\", type: \"uint256\" }, \"bigint\"],\n [{ name: \"x\", type: \"int8\" }, \"number\"],\n [{ name: \"x\", type: \"int32\" }, \"number\"],\n [{ name: \"x\", type: \"int64\" }, \"bigint\"],\n [{ name: \"x\", type: \"int256\" }, \"bigint\"],\n [{ name: \"x\", type: \"address\" }, \"HexString\"],\n [{ name: \"x\", type: \"bool\" }, \"boolean\"],\n [{ name: \"x\", type: \"string\" }, \"string\"],\n [{ name: \"x\", type: \"bytes\" }, \"HexString\"],\n [{ name: \"x\", type: \"bytes32\" }, \"SizedHex<32>\"],\n [{ name: \"x\", type: \"bytes4\" }, \"SizedHex<4>\"],\n [{ name: \"x\", type: \"uint256[]\" }, \"bigint[]\"],\n [{ name: \"x\", type: \"address[]\" }, \"HexString[]\"],\n [{ name: \"x\", type: \"uint256[3]\" }, \"bigint[]\"],\n [{ name: \"x\", type: \"somethingWeird\" }, \"unknown\"],\n ];\n\n for (const [param, expected] of cases) {\n test(`${param.type} → ${expected}`, () => {\n expect(mapSolidityType(param)).toBe(expected);\n });\n }\n\n test(\"tuple with components\", () => {\n const param: AbiParam = {\n name: \"info\",\n type: \"tuple\",\n components: [\n { name: \"addr\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ],\n };\n expect(mapSolidityType(param)).toBe(\"{ addr: HexString; amount: bigint }\");\n });\n\n test(\"nested array of tuples\", () => {\n const param: AbiParam = {\n name: \"items\",\n type: \"tuple[]\",\n components: [\n { name: \"id\", type: \"uint32\" },\n { name: \"name\", type: \"string\" },\n ],\n };\n expect(mapSolidityType(param)).toBe(\"{ id: number; name: string }[]\");\n });\n });\n\n describe(\"generateMethodArgsType\", () => {\n test(\"empty inputs\", () => {\n expect(generateMethodArgsType([])).toBe(\"[]\");\n });\n\n test(\"single input\", () => {\n expect(generateMethodArgsType([{ name: \"to\", type: \"address\" }])).toBe(\n \"[to: HexString]\",\n );\n });\n\n test(\"multiple inputs\", () => {\n const result = generateMethodArgsType([\n { name: \"to\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ]);\n expect(result).toBe(\"[to: HexString, amount: bigint]\");\n });\n });\n\n describe(\"generateMethodResponseType\", () => {\n test(\"no outputs\", () => {\n expect(generateMethodResponseType(undefined)).toBe(\"undefined\");\n expect(generateMethodResponseType([])).toBe(\"undefined\");\n });\n\n test(\"single output\", () => {\n expect(generateMethodResponseType([{ name: \"\", type: \"uint32\" }])).toBe(\"number\");\n });\n\n test(\"multiple outputs\", () => {\n const result = generateMethodResponseType([\n { name: \"balance\", type: \"uint256\" },\n { name: \"nonce\", type: \"uint32\" },\n ]);\n expect(result).toBe(\"{ balance: bigint; nonce: number }\");\n });\n\n test(\"unnamed outputs get positional names\", () => {\n const result = generateMethodResponseType([\n { name: \"\", type: \"bool\" },\n { name: \"\", type: \"uint256\" },\n ]);\n expect(result).toBe(\"{ _0: boolean; _1: bigint }\");\n });\n });\n\n describe(\"generateContractTypes\", () => {\n test(\"generates valid module augmentation\", () => {\n const result = generateContractTypes([\n {\n library: \"@example/counter\",\n abi: [\n {\n type: \"constructor\",\n inputs: [],\n stateMutability: \"nonpayable\",\n },\n {\n type: \"function\",\n name: \"getCount\",\n inputs: [],\n outputs: [{ name: \"\", type: \"uint32\" }],\n stateMutability: \"view\",\n },\n {\n type: \"function\",\n name: \"increment\",\n inputs: [],\n outputs: [],\n stateMutability: \"nonpayable\",\n },\n ],\n },\n ]);\n\n expect(result).toContain('declare module \"@parity/product-sdk-contracts\"');\n expect(result).toContain(\"interface Contracts\");\n expect(result).toContain('\"@example/counter\"');\n expect(result).toContain(\"getCount: { args: []; response: number };\");\n expect(result).toContain(\"increment: { args: []; response: undefined };\");\n // Should not include constructor\n expect(result).not.toContain(\"constructor\");\n });\n\n test(\"handles multiple contracts\", () => {\n const result = generateContractTypes([\n {\n library: \"@a/one\",\n abi: [\n {\n type: \"function\",\n name: \"foo\",\n inputs: [{ name: \"x\", type: \"uint256\" }],\n outputs: [{ name: \"\", type: \"bool\" }],\n },\n ],\n },\n {\n library: \"@b/two\",\n abi: [\n {\n type: \"function\",\n name: \"bar\",\n inputs: [],\n outputs: [{ name: \"\", type: \"address\" }],\n },\n ],\n },\n ]);\n\n expect(result).toContain('\"@a/one\"');\n expect(result).toContain('\"@b/two\"');\n expect(result).toContain(\"foo: { args: [x: bigint]; response: boolean };\");\n expect(result).toContain(\"bar: { args: []; response: HexString };\");\n });\n\n test(\"empty contracts list\", () => {\n const result = generateContractTypes([]);\n expect(result).toContain(\"interface Contracts {\");\n expect(result).toContain(\"}\");\n });\n\n test(\"includes polkadot-api type imports\", () => {\n const result = generateContractTypes([]);\n expect(result).toContain(\"import type { HexString, SizedHex }\");\n expect(result).toContain('from \"polkadot-api\"');\n });\n });\n\n describe(\"resolveContractTypeInputs\", () => {\n test(\"passes inline { library, abi } through unchanged\", async () => {\n const abi: AbiEntry[] = [\n {\n type: \"function\",\n name: \"foo\",\n inputs: [],\n outputs: [{ name: \"\", type: \"bool\" }],\n },\n ];\n const out = await resolveContractTypeInputs([{ library: \"@x/foo\", abi }]);\n expect(out).toEqual([{ library: \"@x/foo\", abi }]);\n });\n\n test(\"loads ABI from { abiPath } on disk\", async () => {\n const fs = await import(\"node:fs/promises\");\n const path = await import(\"node:path\");\n const os = await import(\"node:os\");\n const tmp = await fs.mkdtemp(path.join(os.tmpdir(), \"codegen-\"));\n const abi: AbiEntry[] = [\n {\n type: \"function\",\n name: \"ping\",\n inputs: [],\n outputs: [{ name: \"\", type: \"bool\" }],\n },\n ];\n const file = path.join(tmp, \"ping.release.abi.json\");\n await fs.writeFile(file, JSON.stringify(abi), \"utf8\");\n try {\n const out = await resolveContractTypeInputs([\n { library: \"@x/ping\", abiPath: file },\n ]);\n expect(out).toEqual([{ library: \"@x/ping\", abi }]);\n\n // And it composes with generateContractTypes end-to-end:\n const src = generateContractTypes(out);\n expect(src).toContain('\"@x/ping\"');\n expect(src).toContain(\"ping: { args: []; response: boolean };\");\n } finally {\n await fs.rm(tmp, { recursive: true, force: true });\n }\n });\n });\n}\n"]}