@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 +63 -15
- package/dist/chunk-YZ3YBERU.js +53 -0
- package/dist/chunk-YZ3YBERU.js.map +1 -0
- package/dist/codegen.d.ts +62 -1
- package/dist/codegen.js +81 -1
- package/dist/codegen.js.map +1 -1
- package/dist/index.d.ts +230 -50
- package/dist/index.js +192 -79
- package/dist/index.js.map +1 -1
- package/dist/pvm.d.ts +52 -0
- package/dist/pvm.js +3 -0
- package/dist/pvm.js.map +1 -0
- package/dist/{codegen-BPDBGrJC.d.ts → types-BdHp-xWt.d.ts} +42 -41
- package/package.json +10 -5
- package/dist/chunk-E7AP65D4.js +0 -73
- package/dist/chunk-E7AP65D4.js.map +0 -1
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 —
|
|
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,
|
|
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-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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-
|
|
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-
|
|
144
|
-
"bulletin": "https://paseo-ipfs.polkadot.io
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/codegen.js.map
CHANGED
|
@@ -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"]}
|