@acala-network/chopsticks-utils 0.9.2-2

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.
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: all[name]
9
+ });
10
+ }
11
+ _export(exports, {
12
+ createConfig: function() {
13
+ return createConfig;
14
+ },
15
+ defer: function() {
16
+ return defer;
17
+ },
18
+ sendTransaction: function() {
19
+ return sendTransaction;
20
+ },
21
+ setupContext: function() {
22
+ return setupContext;
23
+ },
24
+ setupContextWithConfig: function() {
25
+ return setupContextWithConfig;
26
+ },
27
+ setupNetworks: function() {
28
+ return setupNetworks;
29
+ },
30
+ testingPairs: function() {
31
+ return testingPairs;
32
+ }
33
+ });
34
+ const _api = require("@polkadot/api");
35
+ const _chopsticks = require("@acala-network/chopsticks");
36
+ const _keyring = require("@polkadot/keyring");
37
+ _export_star(require("./signFake.js"), exports);
38
+ function _export_star(from, to) {
39
+ Object.keys(from).forEach(function(k) {
40
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
41
+ Object.defineProperty(to, k, {
42
+ enumerable: true,
43
+ get: function() {
44
+ return from[k];
45
+ }
46
+ });
47
+ }
48
+ });
49
+ return from;
50
+ }
51
+ const createConfig = ({ endpoint, blockNumber, blockHash, wasmOverride, db, timeout, port, maxMemoryBlockCount, resume, runtimeLogLevel })=>{
52
+ // random port if not specified
53
+ port = port ?? Math.floor(Math.random() * 10000) + 10000;
54
+ const config = {
55
+ endpoint,
56
+ port,
57
+ block: blockNumber || blockHash,
58
+ 'mock-signature-host': true,
59
+ 'build-block-mode': _chopsticks.BuildBlockMode.Manual,
60
+ 'max-memory-block-count': maxMemoryBlockCount ?? 100,
61
+ 'runtime-log-level': runtimeLogLevel,
62
+ db,
63
+ 'wasm-override': wasmOverride,
64
+ timeout,
65
+ resume: resume ?? false
66
+ };
67
+ return config;
68
+ };
69
+ const setupContext = async (option)=>{
70
+ return setupContextWithConfig(createConfig(option));
71
+ };
72
+ const setupContextWithConfig = async ({ timeout, ...config })=>{
73
+ const { chain, listenPort, close } = await (0, _chopsticks.setupWithServer)(config);
74
+ const url = `ws://localhost:${listenPort}`;
75
+ const ws = new _api.WsProvider(url, 3_000, undefined, timeout);
76
+ const api = await _api.ApiPromise.create({
77
+ provider: ws,
78
+ noInitWarn: true
79
+ });
80
+ return {
81
+ url,
82
+ chain,
83
+ ws,
84
+ api,
85
+ dev: {
86
+ newBlock: (param)=>{
87
+ return ws.send('dev_newBlock', [
88
+ param
89
+ ]);
90
+ },
91
+ setStorage: (values, blockHash)=>{
92
+ return ws.send('dev_setStorage', [
93
+ values,
94
+ blockHash
95
+ ]);
96
+ },
97
+ timeTravel: (date)=>{
98
+ return ws.send('dev_timeTravel', [
99
+ date
100
+ ]);
101
+ },
102
+ setHead: (hashOrNumber)=>{
103
+ return ws.send('dev_setHead', [
104
+ hashOrNumber
105
+ ]);
106
+ }
107
+ },
108
+ async teardown () {
109
+ await api.disconnect();
110
+ await close();
111
+ },
112
+ async pause () {
113
+ await ws.send('dev_setBlockBuildMode', [
114
+ _chopsticks.BuildBlockMode.Instant
115
+ ]);
116
+ // log a bit later to ensure the message is visible
117
+ setTimeout(()=>console.log(`Test paused. Polkadot.js apps URL: https://polkadot.js.org/apps/?rpc=${url}`), 100);
118
+ return new Promise((_resolve)=>{}) // wait forever
119
+ ;
120
+ }
121
+ };
122
+ };
123
+ const setupNetworks = async (networkOptions)=>{
124
+ const ret = {};
125
+ let wasmOverriden = false;
126
+ for (const [name, options] of Object.entries(networkOptions)){
127
+ const config = typeof options === 'string' ? await (0, _chopsticks.fetchConfig)(options) : options ?? await (0, _chopsticks.fetchConfig)(name);
128
+ ret[name] = await setupContextWithConfig(config);
129
+ wasmOverriden ||= config['wasm-override'] != null;
130
+ }
131
+ const relaychainName = Object.keys(ret).filter((x)=>[
132
+ 'polkadot',
133
+ 'kusama'
134
+ ].includes(x.toLocaleLowerCase()))[0];
135
+ const { [relaychainName]: relaychain, ...parachains } = ret;
136
+ if (relaychain) {
137
+ for (const parachain of Object.values(parachains)){
138
+ await (0, _chopsticks.connectVertical)(relaychain.chain, parachain.chain);
139
+ }
140
+ }
141
+ const parachainList = Object.values(parachains).map((i)=>i.chain);
142
+ if (parachainList.length > 0) {
143
+ await (0, _chopsticks.connectParachains)(parachainList);
144
+ }
145
+ if (wasmOverriden) {
146
+ // trigger runtime upgrade if needed (due to wasm override)
147
+ for (const chain of Object.values(ret)){
148
+ await chain.dev.newBlock();
149
+ }
150
+ // handle xcm version message if needed (due to wasm override triggered xcm version upgrade)
151
+ for (const chain of Object.values(ret)){
152
+ await chain.dev.newBlock();
153
+ }
154
+ }
155
+ return ret;
156
+ };
157
+ function defer() {
158
+ const deferred = {};
159
+ deferred.promise = new Promise((resolve, reject)=>{
160
+ deferred.resolve = resolve;
161
+ deferred.reject = reject;
162
+ });
163
+ return deferred;
164
+ }
165
+ const sendTransaction = async (tx)=>{
166
+ const signed = await tx;
167
+ const deferred = defer();
168
+ await signed.send((status)=>{
169
+ console.log('tranaction status: ', status.status.toHuman());
170
+ if (status.isInBlock || status.isFinalized) {
171
+ deferred.resolve(status.events);
172
+ }
173
+ if (status.isError) {
174
+ deferred.reject(status.status);
175
+ }
176
+ });
177
+ return {
178
+ events: deferred.promise
179
+ };
180
+ };
181
+ const testingPairs = (keyringType = 'ed25519', ss58Format)=>{
182
+ const keyringEth = (0, _keyring.createTestKeyring)({
183
+ type: 'ethereum'
184
+ });
185
+ // default to ed25519 because sr25519 signature is non-deterministic
186
+ const keyring = new _keyring.Keyring({
187
+ type: keyringType,
188
+ ss58Format
189
+ });
190
+ return {
191
+ alice: keyring.addFromUri('//Alice'),
192
+ bob: keyring.addFromUri('//Bob'),
193
+ charlie: keyring.addFromUri('//Charlie'),
194
+ dave: keyring.addFromUri('//Dave'),
195
+ eve: keyring.addFromUri('//Eve'),
196
+ alith: keyringEth.getPair('0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac'),
197
+ baltathar: keyringEth.getPair('0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0'),
198
+ charleth: keyringEth.getPair('0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc'),
199
+ dorothy: keyringEth.getPair('0x773539d4Ac0e786233D90A233654ccEE26a613D9'),
200
+ ethan: keyringEth.getPair('0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB'),
201
+ keyring,
202
+ keyringEth
203
+ };
204
+ };
@@ -0,0 +1 @@
1
+ {"type": "commonjs"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: all[name]
9
+ });
10
+ }
11
+ _export(exports, {
12
+ signFake: function() {
13
+ return signFake;
14
+ },
15
+ signFakeWithApi: function() {
16
+ return signFakeWithApi;
17
+ }
18
+ });
19
+ const signFakeWithApi = async (api, tx, addr, options = {})=>{
20
+ const nonce = options.nonce ?? (await api.query.system.account(addr)).nonce;
21
+ signFake(tx, addr, {
22
+ nonce,
23
+ genesisHash: api.genesisHash,
24
+ runtimeVersion: api.runtimeVersion,
25
+ blockHash: api.genesisHash,
26
+ ...options
27
+ });
28
+ };
29
+ const signFake = (tx, addr, options)=>{
30
+ const mockSignature = new Uint8Array(64);
31
+ mockSignature.fill(0xcd);
32
+ mockSignature.set([
33
+ 0xde,
34
+ 0xad,
35
+ 0xbe,
36
+ 0xef
37
+ ]);
38
+ tx.signFake(addr, options);
39
+ tx.signature.set(mockSignature);
40
+ };
@@ -0,0 +1,158 @@
1
+ import { ApiPromise, WsProvider } from '@polkadot/api';
2
+ import { BuildBlockMode, connectParachains, connectVertical, fetchConfig, setupWithServer } from '@acala-network/chopsticks';
3
+ import { Keyring, createTestKeyring } from '@polkadot/keyring';
4
+ export * from './signFake.js';
5
+ export const createConfig = ({ endpoint, blockNumber, blockHash, wasmOverride, db, timeout, port, maxMemoryBlockCount, resume, runtimeLogLevel })=>{
6
+ // random port if not specified
7
+ port = port ?? Math.floor(Math.random() * 10000) + 10000;
8
+ const config = {
9
+ endpoint,
10
+ port,
11
+ block: blockNumber || blockHash,
12
+ 'mock-signature-host': true,
13
+ 'build-block-mode': BuildBlockMode.Manual,
14
+ 'max-memory-block-count': maxMemoryBlockCount ?? 100,
15
+ 'runtime-log-level': runtimeLogLevel,
16
+ db,
17
+ 'wasm-override': wasmOverride,
18
+ timeout,
19
+ resume: resume ?? false
20
+ };
21
+ return config;
22
+ };
23
+ export const setupContext = async (option)=>{
24
+ return setupContextWithConfig(createConfig(option));
25
+ };
26
+ export const setupContextWithConfig = async ({ timeout, ...config })=>{
27
+ const { chain, listenPort, close } = await setupWithServer(config);
28
+ const url = `ws://localhost:${listenPort}`;
29
+ const ws = new WsProvider(url, 3_000, undefined, timeout);
30
+ const api = await ApiPromise.create({
31
+ provider: ws,
32
+ noInitWarn: true
33
+ });
34
+ return {
35
+ url,
36
+ chain,
37
+ ws,
38
+ api,
39
+ dev: {
40
+ newBlock: (param)=>{
41
+ return ws.send('dev_newBlock', [
42
+ param
43
+ ]);
44
+ },
45
+ setStorage: (values, blockHash)=>{
46
+ return ws.send('dev_setStorage', [
47
+ values,
48
+ blockHash
49
+ ]);
50
+ },
51
+ timeTravel: (date)=>{
52
+ return ws.send('dev_timeTravel', [
53
+ date
54
+ ]);
55
+ },
56
+ setHead: (hashOrNumber)=>{
57
+ return ws.send('dev_setHead', [
58
+ hashOrNumber
59
+ ]);
60
+ }
61
+ },
62
+ async teardown () {
63
+ await api.disconnect();
64
+ await close();
65
+ },
66
+ async pause () {
67
+ await ws.send('dev_setBlockBuildMode', [
68
+ BuildBlockMode.Instant
69
+ ]);
70
+ // log a bit later to ensure the message is visible
71
+ setTimeout(()=>console.log(`Test paused. Polkadot.js apps URL: https://polkadot.js.org/apps/?rpc=${url}`), 100);
72
+ return new Promise((_resolve)=>{}) // wait forever
73
+ ;
74
+ }
75
+ };
76
+ };
77
+ export const setupNetworks = async (networkOptions)=>{
78
+ const ret = {};
79
+ let wasmOverriden = false;
80
+ for (const [name, options] of Object.entries(networkOptions)){
81
+ const config = typeof options === 'string' ? await fetchConfig(options) : options ?? await fetchConfig(name);
82
+ ret[name] = await setupContextWithConfig(config);
83
+ wasmOverriden ||= config['wasm-override'] != null;
84
+ }
85
+ const relaychainName = Object.keys(ret).filter((x)=>[
86
+ 'polkadot',
87
+ 'kusama'
88
+ ].includes(x.toLocaleLowerCase()))[0];
89
+ const { [relaychainName]: relaychain, ...parachains } = ret;
90
+ if (relaychain) {
91
+ for (const parachain of Object.values(parachains)){
92
+ await connectVertical(relaychain.chain, parachain.chain);
93
+ }
94
+ }
95
+ const parachainList = Object.values(parachains).map((i)=>i.chain);
96
+ if (parachainList.length > 0) {
97
+ await connectParachains(parachainList);
98
+ }
99
+ if (wasmOverriden) {
100
+ // trigger runtime upgrade if needed (due to wasm override)
101
+ for (const chain of Object.values(ret)){
102
+ await chain.dev.newBlock();
103
+ }
104
+ // handle xcm version message if needed (due to wasm override triggered xcm version upgrade)
105
+ for (const chain of Object.values(ret)){
106
+ await chain.dev.newBlock();
107
+ }
108
+ }
109
+ return ret;
110
+ };
111
+ export function defer() {
112
+ const deferred = {};
113
+ deferred.promise = new Promise((resolve, reject)=>{
114
+ deferred.resolve = resolve;
115
+ deferred.reject = reject;
116
+ });
117
+ return deferred;
118
+ }
119
+ export const sendTransaction = async (tx)=>{
120
+ const signed = await tx;
121
+ const deferred = defer();
122
+ await signed.send((status)=>{
123
+ console.log('tranaction status: ', status.status.toHuman());
124
+ if (status.isInBlock || status.isFinalized) {
125
+ deferred.resolve(status.events);
126
+ }
127
+ if (status.isError) {
128
+ deferred.reject(status.status);
129
+ }
130
+ });
131
+ return {
132
+ events: deferred.promise
133
+ };
134
+ };
135
+ export const testingPairs = (keyringType = 'ed25519', ss58Format)=>{
136
+ const keyringEth = createTestKeyring({
137
+ type: 'ethereum'
138
+ });
139
+ // default to ed25519 because sr25519 signature is non-deterministic
140
+ const keyring = new Keyring({
141
+ type: keyringType,
142
+ ss58Format
143
+ });
144
+ return {
145
+ alice: keyring.addFromUri('//Alice'),
146
+ bob: keyring.addFromUri('//Bob'),
147
+ charlie: keyring.addFromUri('//Charlie'),
148
+ dave: keyring.addFromUri('//Dave'),
149
+ eve: keyring.addFromUri('//Eve'),
150
+ alith: keyringEth.getPair('0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac'),
151
+ baltathar: keyringEth.getPair('0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0'),
152
+ charleth: keyringEth.getPair('0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc'),
153
+ dorothy: keyringEth.getPair('0x773539d4Ac0e786233D90A233654ccEE26a613D9'),
154
+ ethan: keyringEth.getPair('0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB'),
155
+ keyring,
156
+ keyringEth
157
+ };
158
+ };
@@ -0,0 +1,22 @@
1
+ export const signFakeWithApi = async (api, tx, addr, options = {})=>{
2
+ const nonce = options.nonce ?? (await api.query.system.account(addr)).nonce;
3
+ signFake(tx, addr, {
4
+ nonce,
5
+ genesisHash: api.genesisHash,
6
+ runtimeVersion: api.runtimeVersion,
7
+ blockHash: api.genesisHash,
8
+ ...options
9
+ });
10
+ };
11
+ export const signFake = (tx, addr, options)=>{
12
+ const mockSignature = new Uint8Array(64);
13
+ mockSignature.fill(0xcd);
14
+ mockSignature.set([
15
+ 0xde,
16
+ 0xad,
17
+ 0xbe,
18
+ 0xef
19
+ ]);
20
+ tx.signFake(addr, options);
21
+ tx.signature.set(mockSignature);
22
+ };
@@ -0,0 +1,101 @@
1
+ import { ApiPromise, WsProvider } from '@polkadot/api';
2
+ import { StorageValues } from '@acala-network/chopsticks';
3
+ import { Codec } from '@polkadot/types/types';
4
+ import { Config } from '@acala-network/chopsticks/schema/index.js';
5
+ import { HexString } from '@polkadot/util/types';
6
+ import { Keyring } from '@polkadot/keyring';
7
+ import { SubmittableExtrinsic } from '@polkadot/api-base/types';
8
+ export * from './signFake.js';
9
+ export type SetupOption = {
10
+ endpoint: string;
11
+ blockNumber?: number;
12
+ blockHash?: HexString;
13
+ wasmOverride?: string;
14
+ db?: string;
15
+ timeout?: number;
16
+ port?: number;
17
+ maxMemoryBlockCount?: number;
18
+ resume?: boolean | HexString | number;
19
+ runtimeLogLevel?: number;
20
+ };
21
+ export type SetupConfig = Config & {
22
+ timeout?: number;
23
+ };
24
+ export declare const createConfig: ({ endpoint, blockNumber, blockHash, wasmOverride, db, timeout, port, maxMemoryBlockCount, resume, runtimeLogLevel, }: SetupOption) => SetupConfig;
25
+ export declare const setupContext: (option: SetupOption) => Promise<{
26
+ url: string;
27
+ chain: import("@acala-network/chopsticks").Blockchain;
28
+ ws: WsProvider;
29
+ api: ApiPromise;
30
+ dev: {
31
+ newBlock: (param?: {
32
+ count?: number;
33
+ to?: number;
34
+ unsafeBlockHeight?: number;
35
+ }) => Promise<string>;
36
+ setStorage: (values: StorageValues, blockHash?: string) => Promise<any>;
37
+ timeTravel: (date: string | number) => Promise<number>;
38
+ setHead: (hashOrNumber: string | number) => Promise<any>;
39
+ };
40
+ teardown(): Promise<void>;
41
+ pause(): Promise<unknown>;
42
+ }>;
43
+ export declare const setupContextWithConfig: ({ timeout, ...config }: SetupConfig) => Promise<{
44
+ url: string;
45
+ chain: import("@acala-network/chopsticks").Blockchain;
46
+ ws: WsProvider;
47
+ api: ApiPromise;
48
+ dev: {
49
+ newBlock: (param?: {
50
+ count?: number;
51
+ to?: number;
52
+ unsafeBlockHeight?: number;
53
+ }) => Promise<string>;
54
+ setStorage: (values: StorageValues, blockHash?: string) => Promise<any>;
55
+ timeTravel: (date: string | number) => Promise<number>;
56
+ setHead: (hashOrNumber: string | number) => Promise<any>;
57
+ };
58
+ teardown(): Promise<void>;
59
+ pause(): Promise<unknown>;
60
+ }>;
61
+ export type NetworkContext = Awaited<ReturnType<typeof setupContext>>;
62
+ export declare const setupNetworks: (networkOptions: Partial<Record<string, Config | string | undefined>>) => Promise<Record<string, {
63
+ url: string;
64
+ chain: import("@acala-network/chopsticks").Blockchain;
65
+ ws: WsProvider;
66
+ api: ApiPromise;
67
+ dev: {
68
+ newBlock: (param?: {
69
+ count?: number;
70
+ to?: number;
71
+ unsafeBlockHeight?: number;
72
+ }) => Promise<string>;
73
+ setStorage: (values: StorageValues, blockHash?: string) => Promise<any>;
74
+ timeTravel: (date: string | number) => Promise<number>;
75
+ setHead: (hashOrNumber: string | number) => Promise<any>;
76
+ };
77
+ teardown(): Promise<void>;
78
+ pause(): Promise<unknown>;
79
+ }>>;
80
+ export declare function defer<T>(): {
81
+ resolve: (value: any) => void;
82
+ reject: (reason: any) => void;
83
+ promise: Promise<T>;
84
+ };
85
+ export declare const sendTransaction: (tx: Promise<SubmittableExtrinsic<'promise'>>) => Promise<{
86
+ events: Promise<Codec[]>;
87
+ }>;
88
+ export declare const testingPairs: (keyringType?: 'ed25519' | 'sr25519', ss58Format?: number) => {
89
+ alice: import("@polkadot/keyring/types").KeyringPair;
90
+ bob: import("@polkadot/keyring/types").KeyringPair;
91
+ charlie: import("@polkadot/keyring/types").KeyringPair;
92
+ dave: import("@polkadot/keyring/types").KeyringPair;
93
+ eve: import("@polkadot/keyring/types").KeyringPair;
94
+ alith: import("@polkadot/keyring/types").KeyringPair;
95
+ baltathar: import("@polkadot/keyring/types").KeyringPair;
96
+ charleth: import("@polkadot/keyring/types").KeyringPair;
97
+ dorothy: import("@polkadot/keyring/types").KeyringPair;
98
+ ethan: import("@polkadot/keyring/types").KeyringPair;
99
+ keyring: Keyring;
100
+ keyringEth: import("@polkadot/keyring/types").KeyringInstance;
101
+ };
@@ -0,0 +1,6 @@
1
+ import { ApiPromise } from '@polkadot/api';
2
+ import { GenericExtrinsic } from '@polkadot/types';
3
+ import { SignatureOptions } from '@polkadot/types/types';
4
+ export type SignFakeOptions = Partial<SignatureOptions>;
5
+ export declare const signFakeWithApi: (api: ApiPromise, tx: GenericExtrinsic, addr: string, options?: SignFakeOptions) => Promise<void>;
6
+ export declare const signFake: (tx: GenericExtrinsic, addr: string, options: SignatureOptions) => void;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@acala-network/chopsticks-utils",
3
+ "version": "0.9.2-2",
4
+ "author": "Acala Developers <hello@acala.network>",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "scripts": {
8
+ "clean": "rm -rf dist",
9
+ "build": "yarn clean && yarn build:cjs && yarn build:esm && yarn build:types",
10
+ "build:cjs": "swc ./src --config-file ../../.cjsswcrc -d dist/cjs && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
11
+ "build:esm": "swc ./src --config-file ../../.esmswcrc -d dist/esm",
12
+ "build:types": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist/types"
13
+ },
14
+ "dependencies": {
15
+ "@acala-network/chopsticks": "0.9.2-5"
16
+ },
17
+ "devDependencies": {
18
+ "@swc/cli": "0.1.62",
19
+ "@swc/core": "^1.3.96",
20
+ "typescript": "^5.2.2"
21
+ },
22
+ "files": [
23
+ "dist/esm/**",
24
+ "dist/cjs/**",
25
+ "dist/types/**"
26
+ ],
27
+ "main": "./dist/cjs/index.js",
28
+ "module": "./dist/esm/index.js",
29
+ "types": "./dist/types/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/types/index.d.ts",
33
+ "require": "./dist/cjs/index.js",
34
+ "import": "./dist/esm/index.js",
35
+ "default": "./dist/esm/index.js"
36
+ },
37
+ "./*": {
38
+ "types": "./dist/types/*.d.ts",
39
+ "require": "./dist/cjs/*.js",
40
+ "import": "./dist/esm/*.js",
41
+ "default": "./dist/esm/*.js"
42
+ },
43
+ "./package.json": "./package.json",
44
+ "./package.cjs.json": "./dist/cjs/package.json"
45
+ }
46
+ }