@parity/product-sdk-cloud-storage 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/LICENSE +201 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.js +534 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/src/authorization.ts +540 -0
- package/src/cid.ts +214 -0
- package/src/client.ts +316 -0
- package/src/errors.ts +162 -0
- package/src/index.ts +107 -0
- package/src/lazy-signer.ts +113 -0
- package/src/networks.ts +47 -0
- package/src/query.ts +216 -0
- package/src/resolve-query.ts +198 -0
- package/src/types.ts +49 -0
- package/src/verify.ts +384 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
2
|
+
import { submitAndWatch, type TxStatus, type WaitFor } from "@parity/product-sdk-tx";
|
|
3
|
+
import type { PolkadotSigner } from "polkadot-api";
|
|
4
|
+
import { Enum } from "polkadot-api";
|
|
5
|
+
|
|
6
|
+
import { CloudStorageAuthorizationError, ProductCloudStorageError } from "./errors.js";
|
|
7
|
+
import type { AuthorizationStatus, CloudStorageApi } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const log = createLogger("cloudStorage");
|
|
10
|
+
|
|
11
|
+
const NOT_AUTHORIZED: AuthorizationStatus = Object.freeze({
|
|
12
|
+
authorized: false,
|
|
13
|
+
remainingTransactions: 0,
|
|
14
|
+
remainingBytes: 0n,
|
|
15
|
+
expiration: 0,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check whether an account is authorized to store data in Cloud Storage.
|
|
20
|
+
*
|
|
21
|
+
* Queries `TransactionStorage.Authorizations` for the given address and returns
|
|
22
|
+
* the raw authorization quota. Use this as a pre-flight check before calling
|
|
23
|
+
* {@link CloudStorageClient.store} to provide clear UX ("not authorized" / "insufficient quota")
|
|
24
|
+
* instead of letting the transaction fail mid-execution.
|
|
25
|
+
*
|
|
26
|
+
* The expiration block number is returned as-is — the chain enforces expiration
|
|
27
|
+
* at submission time, so callers can optionally compare against the current
|
|
28
|
+
* block for display purposes.
|
|
29
|
+
*
|
|
30
|
+
* @param api - Typed Cloud Storage API instance.
|
|
31
|
+
* @param address - SS58-encoded account address to check.
|
|
32
|
+
* @returns Authorization status with remaining quota.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { checkAuthorization } from "@parity/product-sdk-cloud-storage";
|
|
37
|
+
*
|
|
38
|
+
* const auth = await checkAuthorization(api, address);
|
|
39
|
+
* if (!auth.authorized) {
|
|
40
|
+
* console.error("Account is not authorized for cloud storage");
|
|
41
|
+
* } else if (auth.remainingBytes < BigInt(fileBytes.length)) {
|
|
42
|
+
* console.error(`Insufficient quota: ${auth.remainingBytes} bytes remaining`);
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see {@link CloudStorageClient.checkAuthorization} for the client method equivalent.
|
|
47
|
+
*/
|
|
48
|
+
export async function checkAuthorization(
|
|
49
|
+
api: CloudStorageApi,
|
|
50
|
+
address: string,
|
|
51
|
+
): Promise<AuthorizationStatus> {
|
|
52
|
+
let auth;
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
auth = await (api as any).query.TransactionStorage.Authorizations.getValue(
|
|
56
|
+
Enum("Account", address),
|
|
57
|
+
);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
log.error("checkAuthorization: query failed", { address, error });
|
|
60
|
+
throw new CloudStorageAuthorizationError(address, error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!auth) {
|
|
64
|
+
log.debug("checkAuthorization: no authorization found", { address });
|
|
65
|
+
return NOT_AUTHORIZED;
|
|
66
|
+
}
|
|
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.
|
|
73
|
+
const status: AuthorizationStatus = {
|
|
74
|
+
authorized: true,
|
|
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),
|
|
80
|
+
expiration: auth.expiration,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
log.debug("checkAuthorization", {
|
|
84
|
+
address,
|
|
85
|
+
remainingTransactions: status.remainingTransactions,
|
|
86
|
+
remainingBytes: status.remainingBytes.toString(),
|
|
87
|
+
expiration: status.expiration,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return status;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Options for {@link authorizeAccount}.
|
|
95
|
+
*/
|
|
96
|
+
export interface AuthorizeAccountOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Wrap the extrinsic in `Sudo.sudo(...)` before submission. Default: `false`.
|
|
99
|
+
*
|
|
100
|
+
* Use `true` on networks where granting cloud storage authorization
|
|
101
|
+
* requires sudo permissions (most production / managed test networks).
|
|
102
|
+
* Use `false` (default) when the account self-authorizes — typical for
|
|
103
|
+
* local development chains.
|
|
104
|
+
*/
|
|
105
|
+
viaSudo?: boolean;
|
|
106
|
+
/** When to resolve: `"best-block"` (default) or `"finalized"`. */
|
|
107
|
+
waitFor?: WaitFor;
|
|
108
|
+
/** Timeout in ms. Default: 300_000 (5 min). */
|
|
109
|
+
timeoutMs?: number;
|
|
110
|
+
/** Lifecycle status callback for UI progress. */
|
|
111
|
+
onStatus?: (status: TxStatus) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Grant an account authorization to store data in Cloud Storage.
|
|
116
|
+
*
|
|
117
|
+
* Submits a `TransactionStorage.authorize_account` extrinsic, optionally
|
|
118
|
+
* wrapped in `Sudo.sudo(...)` for networks that require sudo to grant
|
|
119
|
+
* authorization. Mirrors the call shape of {@link CloudStorageClient.store} — top-level
|
|
120
|
+
* function, takes an explicit signer, returns a block hash on success.
|
|
121
|
+
*
|
|
122
|
+
* Pair with {@link checkAuthorization} for a typical "check, grant if
|
|
123
|
+
* insufficient, then upload" flow.
|
|
124
|
+
*
|
|
125
|
+
* ## Additive semantics — call once per authorization need
|
|
126
|
+
*
|
|
127
|
+
* `authorize_account` is **additive** within an unexpired authorization window
|
|
128
|
+
* for `AuthorizationScope::Account` (see `pallet-bulletin-transaction-storage`,
|
|
129
|
+
* `fn authorize`). Each successful call **adds** to the existing
|
|
130
|
+
* `transactions_allowance` and `bytes_allowance` rather than overwriting them.
|
|
131
|
+
*
|
|
132
|
+
* Implications for callers:
|
|
133
|
+
*
|
|
134
|
+
* - Calling this function twice with `(100, 1MB)` while the previous
|
|
135
|
+
* authorization is still active leaves the account with quota for `200`
|
|
136
|
+
* transactions and `2MB` — likely unintended.
|
|
137
|
+
* - **This function does NOT use `withRetry`.** Retrying a successful-but-
|
|
138
|
+
* acknowledgment-lost submission would double-grant the quota. Callers
|
|
139
|
+
* needing retry should wrap this function and use {@link checkAuthorization}
|
|
140
|
+
* to verify the post-state before retrying.
|
|
141
|
+
* - To "reset" a quota, let the existing authorization expire
|
|
142
|
+
* (`AuthorizationPeriod` blocks). The next call after expiry creates a fresh
|
|
143
|
+
* authorization rather than adding.
|
|
144
|
+
*
|
|
145
|
+
* Note: `AuthorizationScope::Preimage` uses `set` semantics in the same
|
|
146
|
+
* pallet. This helper is for account-scope authorization only.
|
|
147
|
+
*
|
|
148
|
+
* @param api - Typed Cloud Storage API instance.
|
|
149
|
+
* @param who - SS58-encoded account to authorize.
|
|
150
|
+
* @param transactions - Number of transactions to **add** to the account's allowance.
|
|
151
|
+
* @param bytes - Byte budget to **add** to the account's allowance.
|
|
152
|
+
* @param signer - Signer for the extrinsic. On `viaSudo: true` this must be the sudo key.
|
|
153
|
+
* @param options - Optional `viaSudo` flag plus standard submission controls.
|
|
154
|
+
* @returns Block hash where the extrinsic was included.
|
|
155
|
+
* @throws {ProductCloudStorageError} If `viaSudo: true` is requested but the chain has no `Sudo` pallet.
|
|
156
|
+
*
|
|
157
|
+
* @example Direct (account self-authorizes — local dev)
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { authorizeAccount } from "@parity/product-sdk-cloud-storage";
|
|
160
|
+
*
|
|
161
|
+
* await authorizeAccount(api, address, 100, 100n * 1024n * 1024n, signer);
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @example Sudo-wrapped (managed test network)
|
|
165
|
+
* ```ts
|
|
166
|
+
* await authorizeAccount(api, userAddress, 100, 1_000_000n, sudoSigner, {
|
|
167
|
+
* viaSudo: true,
|
|
168
|
+
* });
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @see {@link checkAuthorization} for the read counterpart.
|
|
172
|
+
* @see {@link CloudStorageClient.authorizeAccount} for the client method equivalent.
|
|
173
|
+
*/
|
|
174
|
+
export async function authorizeAccount(
|
|
175
|
+
api: CloudStorageApi,
|
|
176
|
+
who: string,
|
|
177
|
+
transactions: number,
|
|
178
|
+
bytes: bigint,
|
|
179
|
+
signer: PolkadotSigner,
|
|
180
|
+
options: AuthorizeAccountOptions = {},
|
|
181
|
+
): Promise<{ blockHash: string }> {
|
|
182
|
+
const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;
|
|
183
|
+
|
|
184
|
+
// Single `as any` cast for the whole function. `CloudStorageApi` is upstream-
|
|
185
|
+
// typed as `TypedApi<any>` (see types.ts), so member access is loose by
|
|
186
|
+
// design. Same pattern as upload.ts:57 — narrowing it requires retyping
|
|
187
|
+
// CloudStorageApi against a bundled descriptor (out of scope here).
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
const apiTx = (api as any).tx;
|
|
190
|
+
|
|
191
|
+
log.info("authorizeAccount: building extrinsic", {
|
|
192
|
+
who,
|
|
193
|
+
transactions,
|
|
194
|
+
bytes: bytes.toString(),
|
|
195
|
+
viaSudo,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (viaSudo && !apiTx.Sudo?.sudo) {
|
|
199
|
+
throw new ProductCloudStorageError(
|
|
200
|
+
"viaSudo: true requires the Sudo pallet, which is not available on this network. " +
|
|
201
|
+
"On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism.",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const authorizeTx = apiTx.TransactionStorage.authorize_account({
|
|
206
|
+
who,
|
|
207
|
+
transactions,
|
|
208
|
+
bytes,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;
|
|
212
|
+
|
|
213
|
+
// NOTE: Intentionally NOT using `withRetry` here. `authorize_account` is
|
|
214
|
+
// additive (see JSDoc above), so a retry after a successful-but-lost
|
|
215
|
+
// submission would double-grant the quota. Caller-side retry must verify
|
|
216
|
+
// post-state via `checkAuthorization` first.
|
|
217
|
+
const result = await submitAndWatch(txToSubmit, signer, {
|
|
218
|
+
waitFor,
|
|
219
|
+
timeoutMs,
|
|
220
|
+
onStatus,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
log.info("authorizeAccount: included in block", {
|
|
224
|
+
who,
|
|
225
|
+
blockHash: result.block.hash,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { blockHash: result.block.hash };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (import.meta.vitest) {
|
|
232
|
+
const { describe, test, expect, vi } = import.meta.vitest;
|
|
233
|
+
|
|
234
|
+
function createMockApi(authResult: unknown) {
|
|
235
|
+
return {
|
|
236
|
+
query: {
|
|
237
|
+
TransactionStorage: {
|
|
238
|
+
Authorizations: {
|
|
239
|
+
getValue: vi.fn().mockResolvedValue(authResult),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
} as unknown as CloudStorageApi;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
describe("checkAuthorization", () => {
|
|
247
|
+
test("returns not authorized when no authorization exists", async () => {
|
|
248
|
+
const api = createMockApi(undefined);
|
|
249
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
250
|
+
|
|
251
|
+
expect(status.authorized).toBe(false);
|
|
252
|
+
expect(status.remainingTransactions).toBe(0);
|
|
253
|
+
expect(status.remainingBytes).toBe(0n);
|
|
254
|
+
expect(status.expiration).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("returns authorization with full quota (fresh authorize, nothing consumed)", async () => {
|
|
258
|
+
const api = createMockApi({
|
|
259
|
+
extent: {
|
|
260
|
+
transactions: 0,
|
|
261
|
+
transactions_allowance: 10,
|
|
262
|
+
bytes: 0n,
|
|
263
|
+
bytes_permanent: 0n,
|
|
264
|
+
bytes_allowance: 1_000_000n,
|
|
265
|
+
},
|
|
266
|
+
expiration: 999,
|
|
267
|
+
});
|
|
268
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
269
|
+
|
|
270
|
+
expect(status.authorized).toBe(true);
|
|
271
|
+
expect(status.remainingTransactions).toBe(10);
|
|
272
|
+
expect(status.remainingBytes).toBe(1_000_000n);
|
|
273
|
+
expect(status.expiration).toBe(999);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("returns authorization with zero transactions remaining (fully consumed)", async () => {
|
|
277
|
+
const api = createMockApi({
|
|
278
|
+
extent: {
|
|
279
|
+
transactions: 5,
|
|
280
|
+
transactions_allowance: 5,
|
|
281
|
+
bytes: 0n,
|
|
282
|
+
bytes_permanent: 0n,
|
|
283
|
+
bytes_allowance: 1_000_000n,
|
|
284
|
+
},
|
|
285
|
+
expiration: 999,
|
|
286
|
+
});
|
|
287
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
288
|
+
|
|
289
|
+
expect(status.authorized).toBe(true);
|
|
290
|
+
expect(status.remainingTransactions).toBe(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("returns authorization with zero bytes remaining (fully consumed)", async () => {
|
|
294
|
+
const api = createMockApi({
|
|
295
|
+
extent: {
|
|
296
|
+
transactions: 0,
|
|
297
|
+
transactions_allowance: 5,
|
|
298
|
+
bytes: 1_000n,
|
|
299
|
+
bytes_permanent: 0n,
|
|
300
|
+
bytes_allowance: 1_000n,
|
|
301
|
+
},
|
|
302
|
+
expiration: 999,
|
|
303
|
+
});
|
|
304
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
305
|
+
|
|
306
|
+
expect(status.authorized).toBe(true);
|
|
307
|
+
expect(status.remainingBytes).toBe(0n);
|
|
308
|
+
});
|
|
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
|
+
|
|
328
|
+
test("preserves expiration block number", async () => {
|
|
329
|
+
const api = createMockApi({
|
|
330
|
+
extent: {
|
|
331
|
+
transactions: 0,
|
|
332
|
+
transactions_allowance: 1,
|
|
333
|
+
bytes: 0n,
|
|
334
|
+
bytes_permanent: 0n,
|
|
335
|
+
bytes_allowance: 500n,
|
|
336
|
+
},
|
|
337
|
+
expiration: 12345,
|
|
338
|
+
});
|
|
339
|
+
const status = await checkAuthorization(api, "5GrwvaEF...");
|
|
340
|
+
|
|
341
|
+
expect(status.expiration).toBe(12345);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("throws CloudStorageAuthorizationError when query fails", async () => {
|
|
345
|
+
const api = {
|
|
346
|
+
query: {
|
|
347
|
+
TransactionStorage: {
|
|
348
|
+
Authorizations: {
|
|
349
|
+
getValue: vi.fn().mockRejectedValue(new Error("RPC connection lost")),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
} as unknown as CloudStorageApi;
|
|
354
|
+
|
|
355
|
+
const err = await checkAuthorization(api, "5GrwvaEF...").catch((e: unknown) => e);
|
|
356
|
+
expect(err).toBeInstanceOf(CloudStorageAuthorizationError);
|
|
357
|
+
const error = err as CloudStorageAuthorizationError;
|
|
358
|
+
expect(error.address).toBe("5GrwvaEF...");
|
|
359
|
+
expect(error.cause).toBeInstanceOf(Error);
|
|
360
|
+
expect((error.cause as Error).message).toBe("RPC connection lost");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("passes correct Enum key to the query", async () => {
|
|
364
|
+
const getValue = vi.fn().mockResolvedValue(undefined);
|
|
365
|
+
const api = {
|
|
366
|
+
query: {
|
|
367
|
+
TransactionStorage: {
|
|
368
|
+
Authorizations: { getValue },
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
} as unknown as CloudStorageApi;
|
|
372
|
+
|
|
373
|
+
await checkAuthorization(api, "5GrwvaEF...");
|
|
374
|
+
|
|
375
|
+
expect(getValue).toHaveBeenCalledTimes(1);
|
|
376
|
+
const arg = getValue.mock.calls[0][0];
|
|
377
|
+
expect(arg.type).toBe("Account");
|
|
378
|
+
expect(arg.value).toBe("5GrwvaEF...");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Mock factory for `authorizeAccount` tests.
|
|
384
|
+
*
|
|
385
|
+
* Mirrors the mocking style used in `upload.ts` — we don't mock
|
|
386
|
+
* `submitAndWatch` (the SDK helper) directly. Instead we let it call
|
|
387
|
+
* through to a fake `signSubmitAndWatch` on the api, which emits the
|
|
388
|
+
* lifecycle events `submitAndWatch` listens for.
|
|
389
|
+
*/
|
|
390
|
+
function createMockApiForAuthorize(blockHash = "0xblockhash") {
|
|
391
|
+
const fakeTx = {
|
|
392
|
+
decodedCall: { fakeCall: true } as unknown,
|
|
393
|
+
signSubmitAndWatch: vi.fn().mockReturnValue({
|
|
394
|
+
subscribe: (handlers: { next: (e: unknown) => void }) => {
|
|
395
|
+
queueMicrotask(() => {
|
|
396
|
+
handlers.next({ type: "signed", txHash: "0xtxhash" });
|
|
397
|
+
handlers.next({
|
|
398
|
+
type: "txBestBlocksState",
|
|
399
|
+
txHash: "0xtxhash",
|
|
400
|
+
found: true,
|
|
401
|
+
ok: true,
|
|
402
|
+
block: { hash: blockHash, number: 1, index: 0 },
|
|
403
|
+
events: [],
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
return { unsubscribe: vi.fn() };
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
api: {
|
|
413
|
+
tx: {
|
|
414
|
+
TransactionStorage: {
|
|
415
|
+
authorize_account: vi.fn().mockReturnValue(fakeTx),
|
|
416
|
+
},
|
|
417
|
+
Sudo: {
|
|
418
|
+
sudo: vi.fn().mockReturnValue(fakeTx),
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
fakeTx,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const mockSigner = {} as PolkadotSigner;
|
|
427
|
+
|
|
428
|
+
describe("authorizeAccount", () => {
|
|
429
|
+
test("direct path: calls TransactionStorage.authorize_account with the right params", async () => {
|
|
430
|
+
const { api } = createMockApiForAuthorize();
|
|
431
|
+
|
|
432
|
+
await authorizeAccount(
|
|
433
|
+
api as unknown as CloudStorageApi,
|
|
434
|
+
"5GrwvaEF...",
|
|
435
|
+
100,
|
|
436
|
+
1_000_000n,
|
|
437
|
+
mockSigner,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(api.tx.TransactionStorage.authorize_account).toHaveBeenCalledOnce();
|
|
441
|
+
const arg = api.tx.TransactionStorage.authorize_account.mock.calls[0][0];
|
|
442
|
+
expect(arg.who).toBe("5GrwvaEF...");
|
|
443
|
+
expect(arg.transactions).toBe(100);
|
|
444
|
+
expect(arg.bytes).toBe(1_000_000n);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("direct path: does NOT call Sudo.sudo when viaSudo is false (default)", async () => {
|
|
448
|
+
const { api } = createMockApiForAuthorize();
|
|
449
|
+
|
|
450
|
+
await authorizeAccount(
|
|
451
|
+
api as unknown as CloudStorageApi,
|
|
452
|
+
"5GrwvaEF...",
|
|
453
|
+
10,
|
|
454
|
+
100n,
|
|
455
|
+
mockSigner,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(api.tx.Sudo.sudo).not.toHaveBeenCalled();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("direct path: returns the block hash from submission", async () => {
|
|
462
|
+
const { api } = createMockApiForAuthorize("0xdeadbeef");
|
|
463
|
+
|
|
464
|
+
const result = await authorizeAccount(
|
|
465
|
+
api as unknown as CloudStorageApi,
|
|
466
|
+
"5GrwvaEF...",
|
|
467
|
+
10,
|
|
468
|
+
100n,
|
|
469
|
+
mockSigner,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
expect(result).toEqual({ blockHash: "0xdeadbeef" });
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("sudo path: wraps the authorize_account call inside Sudo.sudo", async () => {
|
|
476
|
+
const { api, fakeTx } = createMockApiForAuthorize();
|
|
477
|
+
|
|
478
|
+
await authorizeAccount(
|
|
479
|
+
api as unknown as CloudStorageApi,
|
|
480
|
+
"5GrwvaEF...",
|
|
481
|
+
10,
|
|
482
|
+
100n,
|
|
483
|
+
mockSigner,
|
|
484
|
+
{ viaSudo: true },
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
expect(api.tx.Sudo.sudo).toHaveBeenCalledOnce();
|
|
488
|
+
const sudoArg = api.tx.Sudo.sudo.mock.calls[0][0];
|
|
489
|
+
expect(sudoArg.call).toBe(fakeTx.decodedCall);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("sudo path: still returns the block hash from the sudo extrinsic", async () => {
|
|
493
|
+
const { api } = createMockApiForAuthorize("0xsudoblock");
|
|
494
|
+
|
|
495
|
+
const result = await authorizeAccount(
|
|
496
|
+
api as unknown as CloudStorageApi,
|
|
497
|
+
"5GrwvaEF...",
|
|
498
|
+
10,
|
|
499
|
+
100n,
|
|
500
|
+
mockSigner,
|
|
501
|
+
{ viaSudo: true },
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(result).toEqual({ blockHash: "0xsudoblock" });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("throws ProductCloudStorageError when viaSudo is true but the chain lacks a Sudo pallet", async () => {
|
|
508
|
+
const apiWithoutSudo = {
|
|
509
|
+
tx: {
|
|
510
|
+
TransactionStorage: {
|
|
511
|
+
authorize_account: vi.fn().mockReturnValue({
|
|
512
|
+
decodedCall: {} as unknown,
|
|
513
|
+
signSubmitAndWatch: vi.fn(),
|
|
514
|
+
}),
|
|
515
|
+
},
|
|
516
|
+
// Sudo intentionally absent — represents production Polkadot/Kusama
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const err = await authorizeAccount(
|
|
521
|
+
apiWithoutSudo as unknown as CloudStorageApi,
|
|
522
|
+
"5GrwvaEF...",
|
|
523
|
+
10,
|
|
524
|
+
100n,
|
|
525
|
+
mockSigner,
|
|
526
|
+
{ viaSudo: true },
|
|
527
|
+
).catch((e: unknown) => e);
|
|
528
|
+
|
|
529
|
+
expect(err).toBeInstanceOf(ProductCloudStorageError);
|
|
530
|
+
expect((err as Error).message).toMatch(/Sudo pallet/i);
|
|
531
|
+
// Verify we did NOT proceed to submit anything
|
|
532
|
+
expect(apiWithoutSudo.tx.TransactionStorage.authorize_account).not.toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Note: submission-failure propagation is tested upstream in
|
|
536
|
+
// @parity/product-sdk-tx's own suite. Re-testing it here would just
|
|
537
|
+
// re-test submitAndWatch's error path through a brittle mock, which
|
|
538
|
+
// depends on internal event-shape details outside this file's contract.
|
|
539
|
+
});
|
|
540
|
+
}
|