@midnight-ntwrk/wallet-sdk-indexer-client 1.0.0-beta.11
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 +1 -0
- package/dist/effect/ConnectionHelper.d.ts +4 -0
- package/dist/effect/ConnectionHelper.js +29 -0
- package/dist/effect/HttpQueryClient.d.ts +4 -0
- package/dist/effect/HttpQueryClient.js +38 -0
- package/dist/effect/Query.d.ts +46 -0
- package/dist/effect/Query.js +35 -0
- package/dist/effect/QueryClient.d.ts +15 -0
- package/dist/effect/QueryClient.js +3 -0
- package/dist/effect/Subscription.d.ts +21 -0
- package/dist/effect/Subscription.js +28 -0
- package/dist/effect/SubscriptionClient.d.ts +15 -0
- package/dist/effect/SubscriptionClient.js +3 -0
- package/dist/effect/WsSubscriptionClient.d.ts +4 -0
- package/dist/effect/WsSubscriptionClient.js +32 -0
- package/dist/effect/index.d.ts +7 -0
- package/dist/effect/index.js +7 -0
- package/dist/effect/test/httpQueryClient.spied.test.d.ts +1 -0
- package/dist/effect/test/httpQueryClient.spied.test.js +25 -0
- package/dist/effect/test/httpQueryClient.test.d.ts +1 -0
- package/dist/effect/test/httpQueryClient.test.js +20 -0
- package/dist/effect/test/wsSubscriptionClient.spied.test.d.ts +1 -0
- package/dist/effect/test/wsSubscriptionClient.spied.test.js +25 -0
- package/dist/effect/test/wsSubscriptionClient.test.d.ts +1 -0
- package/dist/effect/test/wsSubscriptionClient.test.js +20 -0
- package/dist/graphql/generated/fragment-masking.d.ts +19 -0
- package/dist/graphql/generated/fragment-masking.js +16 -0
- package/dist/graphql/generated/gql.d.ts +66 -0
- package/dist/graphql/generated/gql.js +14 -0
- package/dist/graphql/generated/graphql.d.ts +656 -0
- package/dist/graphql/generated/graphql.js +7 -0
- package/dist/graphql/generated/index.d.ts +2 -0
- package/dist/graphql/generated/index.js +2 -0
- package/dist/graphql/queries/BlockHash.d.ts +6 -0
- package/dist/graphql/queries/BlockHash.js +11 -0
- package/dist/graphql/queries/Connect.d.ts +6 -0
- package/dist/graphql/queries/Connect.js +7 -0
- package/dist/graphql/queries/Disconnect.d.ts +6 -0
- package/dist/graphql/queries/Disconnect.js +7 -0
- package/dist/graphql/queries/index.d.ts +3 -0
- package/dist/graphql/queries/index.js +3 -0
- package/dist/graphql/queries/test/BlockHash.test.d.ts +1 -0
- package/dist/graphql/queries/test/BlockHash.test.js +69 -0
- package/dist/graphql/subscriptions/DustLedgerEvents.d.ts +6 -0
- package/dist/graphql/subscriptions/DustLedgerEvents.js +12 -0
- package/dist/graphql/subscriptions/ShieldedTransactions.d.ts +8 -0
- package/dist/graphql/subscriptions/ShieldedTransactions.js +42 -0
- package/dist/graphql/subscriptions/UnshieldedTransactions.d.ts +8 -0
- package/dist/graphql/subscriptions/UnshieldedTransactions.js +49 -0
- package/dist/graphql/subscriptions/ZswapEvents.d.ts +6 -0
- package/dist/graphql/subscriptions/ZswapEvents.js +11 -0
- package/dist/graphql/subscriptions/index.d.ts +4 -0
- package/dist/graphql/subscriptions/index.js +4 -0
- package/dist/graphql/subscriptions/test/ShieldedTransactions.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/ShieldedTransactions.test.js +41 -0
- package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.js +38 -0
- package/dist/graphql/subscriptions/test/ZswapEvents.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/ZswapEvents.test.js +36 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +61 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const BlockHashDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "BlockHash" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "offset" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "BlockOffset" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "block" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "offset" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "offset" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "height" } }, { "kind": "Field", "name": { "kind": "Name", "value": "hash" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ledgerParameters" } }] } }] } }] };
|
|
2
|
+
export const ConnectDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "Connect" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "viewingKey" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ViewingKey" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "connect" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "viewingKey" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "viewingKey" } } }] }] } }] };
|
|
3
|
+
export const DisconnectDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "Disconnect" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "sessionId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "HexEncoded" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "disconnect" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "sessionId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "sessionId" } } }] }] } }] };
|
|
4
|
+
export const DustLedgerEventsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "subscription", "name": { "kind": "Name", "value": "DustLedgerEvents" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "dustLedgerEvents" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "alias": { "kind": "Name", "value": "type" }, "name": { "kind": "Name", "value": "__typename" } }, { "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "raw" } }, { "kind": "Field", "name": { "kind": "Name", "value": "maxId" } }] } }] } }] };
|
|
5
|
+
export const ShieldedTransactionsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "subscription", "name": { "kind": "Name", "value": "ShieldedTransactions" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "sessionId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "HexEncoded" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "index" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "shieldedTransactions" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "sessionId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "sessionId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "index" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "index" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "__typename" } }, { "kind": "InlineFragment", "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "ShieldedTransactionsProgress" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "highestEndIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "highestCheckedEndIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "highestRelevantEndIndex" } }] } }, { "kind": "InlineFragment", "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "RelevantTransaction" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "transaction" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "raw" } }, { "kind": "Field", "name": { "kind": "Name", "value": "hash" } }, { "kind": "Field", "name": { "kind": "Name", "value": "protocolVersion" } }, { "kind": "Field", "name": { "kind": "Name", "value": "identifiers" } }, { "kind": "Field", "name": { "kind": "Name", "value": "startIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "endIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "fees" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "paidFees" } }, { "kind": "Field", "name": { "kind": "Name", "value": "estimatedFees" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "transactionResult" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "segments" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "success" } }] } }] } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "collapsedMerkleTree" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "startIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "endIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "update" } }, { "kind": "Field", "name": { "kind": "Name", "value": "protocolVersion" } }] } }] } }] } }] } }] };
|
|
6
|
+
export const UnshieldedTransactionsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "subscription", "name": { "kind": "Name", "value": "UnshieldedTransactions" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "address" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "UnshieldedAddress" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "transactionId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "unshieldedTransactions" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "address" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "address" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "transactionId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "transactionId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "InlineFragment", "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "UnshieldedTransaction" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "alias": { "kind": "Name", "value": "type" }, "name": { "kind": "Name", "value": "__typename" } }, { "kind": "Field", "name": { "kind": "Name", "value": "transaction" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "alias": { "kind": "Name", "value": "type" }, "name": { "kind": "Name", "value": "__typename" } }, { "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "hash" } }, { "kind": "Field", "name": { "kind": "Name", "value": "protocolVersion" } }, { "kind": "InlineFragment", "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "RegularTransaction" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "identifiers" } }, { "kind": "Field", "name": { "kind": "Name", "value": "transactionResult" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "segments" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "success" } }] } }] } }] } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdUtxos" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "owner" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tokenType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "value" } }, { "kind": "Field", "name": { "kind": "Name", "value": "outputIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "intentHash" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ctime" } }, { "kind": "Field", "name": { "kind": "Name", "value": "registeredForDustGeneration" } }] } }, { "kind": "Field", "name": { "kind": "Name", "value": "spentUtxos" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "owner" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tokenType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "value" } }, { "kind": "Field", "name": { "kind": "Name", "value": "outputIndex" } }, { "kind": "Field", "name": { "kind": "Name", "value": "intentHash" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ctime" } }, { "kind": "Field", "name": { "kind": "Name", "value": "registeredForDustGeneration" } }] } }] } }, { "kind": "InlineFragment", "typeCondition": { "kind": "NamedType", "name": { "kind": "Name", "value": "UnshieldedTransactionsProgress" } }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "alias": { "kind": "Name", "value": "type" }, "name": { "kind": "Name", "value": "__typename" } }, { "kind": "Field", "name": { "kind": "Name", "value": "highestTransactionId" } }] } }] } }] } }] };
|
|
7
|
+
export const ZswapEventsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "subscription", "name": { "kind": "Name", "value": "ZswapEvents" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "zswapLedgerEvents" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "raw" } }, { "kind": "Field", "name": { "kind": "Name", "value": "maxId" } }] } }] } }] };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Query } from '../../effect/index.js';
|
|
2
|
+
export declare const BlockHash: Query.Query<import("../generated/graphql.js").BlockHashQuery, import("../generated/graphql.js").Exact<{
|
|
3
|
+
offset: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").BlockOffset>;
|
|
4
|
+
}>, Query.Query.QueryFn<import("../generated/graphql.js").BlockHashQuery, import("../generated/graphql.js").Exact<{
|
|
5
|
+
offset: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").BlockOffset>;
|
|
6
|
+
}>>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Query } from '../../effect/index.js';
|
|
2
|
+
import { gql } from '../generated/index.js';
|
|
3
|
+
export const BlockHash = Query.make('BlockHash', gql(`
|
|
4
|
+
query BlockHash($offset: BlockOffset) {
|
|
5
|
+
block(offset: $offset) {
|
|
6
|
+
height
|
|
7
|
+
hash
|
|
8
|
+
ledgerParameters
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
`));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Query } from '../../effect/index.js';
|
|
2
|
+
export declare const Connect: Query.Query<import("../generated/graphql.js").ConnectMutation, import("../generated/graphql.js").Exact<{
|
|
3
|
+
viewingKey: import("../generated/graphql.js").Scalars["ViewingKey"]["input"];
|
|
4
|
+
}>, Query.Query.QueryFn<import("../generated/graphql.js").ConnectMutation, import("../generated/graphql.js").Exact<{
|
|
5
|
+
viewingKey: import("../generated/graphql.js").Scalars["ViewingKey"]["input"];
|
|
6
|
+
}>>>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Query } from '../../effect/index.js';
|
|
2
|
+
export declare const Disconnect: Query.Query<import("../generated/graphql.js").DisconnectMutation, import("../generated/graphql.js").Exact<{
|
|
3
|
+
sessionId: import("../generated/graphql.js").Scalars["HexEncoded"]["input"];
|
|
4
|
+
}>, Query.Query.QueryFn<import("../generated/graphql.js").DisconnectMutation, import("../generated/graphql.js").Exact<{
|
|
5
|
+
sessionId: import("../generated/graphql.js").Scalars["HexEncoded"]["input"];
|
|
6
|
+
}>>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
import { Effect, Option } from 'effect';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { DockerComposeEnvironment, Wait } from 'testcontainers';
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { HttpQueryClient } from '../../../effect/index.js';
|
|
8
|
+
import { BlockHash } from '../BlockHash.js';
|
|
9
|
+
const COMPOSE_PATH = path.resolve(new URL(import.meta.url).pathname, '../../../../../');
|
|
10
|
+
const timeout_minutes = (mins) => 1_000 * 60 * mins;
|
|
11
|
+
describe('BlockHash query', () => {
|
|
12
|
+
describe('with available Indexer Server', () => {
|
|
13
|
+
const environmentId = randomUUID();
|
|
14
|
+
let environment = undefined;
|
|
15
|
+
const getIndexerPort = () => environment?.getContainer(`indexer_${environmentId}`)?.getMappedPort(8088) ?? 8088;
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
environment = await new DockerComposeEnvironment(COMPOSE_PATH, 'docker-compose.yml')
|
|
18
|
+
.withEnvironment({
|
|
19
|
+
TESTCONTAINERS_UID: environmentId,
|
|
20
|
+
})
|
|
21
|
+
// The test below assumes indexer is able to serve blocks, so we wait for it to index at least one block
|
|
22
|
+
// Otherwise the test below would be flakey or not precise enough to be useful
|
|
23
|
+
// Inspecting logs is not the best idea, but here it's the only way
|
|
24
|
+
.withWaitStrategy(`indexer_${environmentId}`, Wait.forLogMessage(/block indexed/))
|
|
25
|
+
.up();
|
|
26
|
+
}, timeout_minutes(3));
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await environment?.down();
|
|
29
|
+
}, timeout_minutes(1));
|
|
30
|
+
it('should fail with ClientError for unknown URL', async () => {
|
|
31
|
+
await BlockHash.run({ offset: null }).pipe(Effect.catchSome((err) => (err._tag === 'ClientError' ? Option.some(Effect.succeed(void 0)) : Option.none())), Effect.catchAll((err) => Effect.fail(`Encountered unexpected '${err._tag}' error: ${err.message}`)), Effect.flatMap((data) => (data ? Effect.fail('Unexpectedly received data') : Effect.succeed(void 0))), Effect.provide(HttpQueryClient.layer({ url: `http://127.0.0.1:${getIndexerPort()}/a__p__i/v3/graphql` })), Effect.scoped, Effect.runPromise);
|
|
32
|
+
}, timeout_minutes(1));
|
|
33
|
+
it('should invoke GraphQL query', async () => {
|
|
34
|
+
// Expect a result containing a block with any height and hash value.
|
|
35
|
+
const blockExpectation = expect.objectContaining({
|
|
36
|
+
block: expect.objectContaining({
|
|
37
|
+
height: expect.any(Number),
|
|
38
|
+
hash: expect.any(String),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
await Effect.gen(function* () {
|
|
42
|
+
const query = yield* BlockHash;
|
|
43
|
+
const result = yield* query({ offset: null });
|
|
44
|
+
expect(result).toEqual(blockExpectation);
|
|
45
|
+
}).pipe(Effect.provide(HttpQueryClient.layer({ url: `http://127.0.0.1:${getIndexerPort()}/api/v3/graphql` })), Effect.scoped, Effect.catchAll((err) => Effect.fail(`Encountered unexpected error: ${err.message}`)), Effect.runPromise);
|
|
46
|
+
}, timeout_minutes(1));
|
|
47
|
+
});
|
|
48
|
+
it('should support query function injection', async () => {
|
|
49
|
+
const block = { block: { height: 1_000, hash: 'SOME_HASH', ledgerParameters: '0x0' } };
|
|
50
|
+
const blockExpectation = expect.objectContaining({
|
|
51
|
+
block: expect.objectContaining({
|
|
52
|
+
height: block.block.height,
|
|
53
|
+
hash: block.block.hash,
|
|
54
|
+
ledgerParameters: block.block.ledgerParameters,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
const mockedQueryFn = vi.fn();
|
|
58
|
+
mockedQueryFn.mockReturnValue(Effect.succeed(block));
|
|
59
|
+
await Effect.gen(function* () {
|
|
60
|
+
const query = yield* BlockHash;
|
|
61
|
+
const result = yield* query({ offset: null });
|
|
62
|
+
expect(result).toEqual(blockExpectation);
|
|
63
|
+
}).pipe(Effect.provideService(BlockHash.tag, mockedQueryFn), Effect.provide(HttpQueryClient.layer({ url: 'http://127.0.0.1:8088/a__p__i/v3/graphql' })), Effect.scoped, Effect.catchAll((err) => Effect.fail(`Encountered unexpected error: ${err.message}`)), Effect.runPromise);
|
|
64
|
+
await Effect.gen(function* () {
|
|
65
|
+
const result = yield* BlockHash.run({ offset: null });
|
|
66
|
+
expect(result).toEqual(blockExpectation);
|
|
67
|
+
}).pipe(Effect.provideService(BlockHash.tag, mockedQueryFn), Effect.provide(HttpQueryClient.layer({ url: 'http://127.0.0.1:8088/a__p__i/v3/graphql' })), Effect.scoped, Effect.catchAll((err) => Effect.fail(`Encountered unexpected error: ${err.message}`)), Effect.runPromise);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
export declare const DustLedgerEvents: Subscription.Subscription<import("../generated/graphql.js").DustLedgerEventsSubscription, import("../generated/graphql.js").Exact<{
|
|
3
|
+
id: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
4
|
+
}>, Subscription.Subscription.SubscriptionFn<import("../generated/graphql.js").DustLedgerEventsSubscription, import("../generated/graphql.js").Exact<{
|
|
5
|
+
id: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
6
|
+
}>>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
import { gql } from '../generated/index.js';
|
|
3
|
+
export const DustLedgerEvents = Subscription.make('DustLedgerEvents', gql(`
|
|
4
|
+
subscription DustLedgerEvents($id: Int) {
|
|
5
|
+
dustLedgerEvents(id: $id) {
|
|
6
|
+
type: __typename
|
|
7
|
+
id
|
|
8
|
+
raw
|
|
9
|
+
maxId
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
`));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
export declare const ShieldedTransactions: Subscription.Subscription<import("../generated/graphql.js").ShieldedTransactionsSubscription, import("../generated/graphql.js").Exact<{
|
|
3
|
+
sessionId: import("../generated/graphql.js").Scalars["HexEncoded"]["input"];
|
|
4
|
+
index: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
5
|
+
}>, Subscription.Subscription.SubscriptionFn<import("../generated/graphql.js").ShieldedTransactionsSubscription, import("../generated/graphql.js").Exact<{
|
|
6
|
+
sessionId: import("../generated/graphql.js").Scalars["HexEncoded"]["input"];
|
|
7
|
+
index: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
8
|
+
}>>>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
import { gql } from '../generated/index.js';
|
|
3
|
+
export const ShieldedTransactions = Subscription.make('ShieldedTransactions', gql(`
|
|
4
|
+
subscription ShieldedTransactions($sessionId: HexEncoded!, $index: Int) {
|
|
5
|
+
shieldedTransactions(sessionId: $sessionId, index: $index) {
|
|
6
|
+
__typename
|
|
7
|
+
... on ShieldedTransactionsProgress {
|
|
8
|
+
highestEndIndex
|
|
9
|
+
highestCheckedEndIndex
|
|
10
|
+
highestRelevantEndIndex
|
|
11
|
+
}
|
|
12
|
+
... on RelevantTransaction {
|
|
13
|
+
transaction {
|
|
14
|
+
id
|
|
15
|
+
raw
|
|
16
|
+
hash
|
|
17
|
+
protocolVersion
|
|
18
|
+
identifiers
|
|
19
|
+
startIndex
|
|
20
|
+
endIndex
|
|
21
|
+
fees {
|
|
22
|
+
paidFees
|
|
23
|
+
estimatedFees
|
|
24
|
+
}
|
|
25
|
+
transactionResult {
|
|
26
|
+
status
|
|
27
|
+
segments {
|
|
28
|
+
id
|
|
29
|
+
success
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
collapsedMerkleTree {
|
|
34
|
+
startIndex
|
|
35
|
+
endIndex
|
|
36
|
+
update
|
|
37
|
+
protocolVersion
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
export declare const UnshieldedTransactions: Subscription.Subscription<import("../generated/graphql.js").UnshieldedTransactionsSubscription, import("../generated/graphql.js").Exact<{
|
|
3
|
+
address: import("../generated/graphql.js").Scalars["UnshieldedAddress"]["input"];
|
|
4
|
+
transactionId: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
5
|
+
}>, Subscription.Subscription.SubscriptionFn<import("../generated/graphql.js").UnshieldedTransactionsSubscription, import("../generated/graphql.js").Exact<{
|
|
6
|
+
address: import("../generated/graphql.js").Scalars["UnshieldedAddress"]["input"];
|
|
7
|
+
transactionId: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
8
|
+
}>>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
import { gql } from '../generated/index.js';
|
|
3
|
+
export const UnshieldedTransactions = Subscription.make('UnshieldedTransactions', gql(`
|
|
4
|
+
subscription UnshieldedTransactions($address: UnshieldedAddress!, $transactionId: Int) {
|
|
5
|
+
unshieldedTransactions(address: $address, transactionId: $transactionId) {
|
|
6
|
+
... on UnshieldedTransaction {
|
|
7
|
+
type: __typename
|
|
8
|
+
transaction {
|
|
9
|
+
type: __typename
|
|
10
|
+
id
|
|
11
|
+
hash
|
|
12
|
+
protocolVersion
|
|
13
|
+
... on RegularTransaction {
|
|
14
|
+
identifiers
|
|
15
|
+
transactionResult {
|
|
16
|
+
status
|
|
17
|
+
segments {
|
|
18
|
+
id
|
|
19
|
+
success
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
createdUtxos {
|
|
25
|
+
owner
|
|
26
|
+
tokenType
|
|
27
|
+
value
|
|
28
|
+
outputIndex
|
|
29
|
+
intentHash
|
|
30
|
+
ctime
|
|
31
|
+
registeredForDustGeneration
|
|
32
|
+
}
|
|
33
|
+
spentUtxos {
|
|
34
|
+
owner
|
|
35
|
+
tokenType
|
|
36
|
+
value
|
|
37
|
+
outputIndex
|
|
38
|
+
intentHash
|
|
39
|
+
ctime
|
|
40
|
+
registeredForDustGeneration
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
... on UnshieldedTransactionsProgress {
|
|
44
|
+
type: __typename
|
|
45
|
+
highestTransactionId
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
export declare const ZswapEvents: Subscription.Subscription<import("../generated/graphql.js").ZswapEventsSubscription, import("../generated/graphql.js").Exact<{
|
|
3
|
+
id: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
4
|
+
}>, Subscription.Subscription.SubscriptionFn<import("../generated/graphql.js").ZswapEventsSubscription, import("../generated/graphql.js").Exact<{
|
|
5
|
+
id: import("../generated/graphql.js").InputMaybe<import("../generated/graphql.js").Scalars["Int"]["input"]>;
|
|
6
|
+
}>>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Subscription } from '../../effect/index.js';
|
|
2
|
+
import { gql } from '../generated/index.js';
|
|
3
|
+
export const ZswapEvents = Subscription.make('ZswapEvents', gql(`
|
|
4
|
+
subscription ZswapEvents($id: Int) {
|
|
5
|
+
zswapLedgerEvents(id: $id) {
|
|
6
|
+
id
|
|
7
|
+
raw
|
|
8
|
+
maxId
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
`));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Effect, Stream } from 'effect';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { DockerComposeEnvironment, Wait } from 'testcontainers';
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
6
|
+
import { HttpQueryClient, WsSubscriptionClient } from '../../../effect/index.js';
|
|
7
|
+
import { Connect, Disconnect } from '../../queries/index.js';
|
|
8
|
+
import { ShieldedTransactions } from '../ShieldedTransactions.js';
|
|
9
|
+
const COMPOSE_PATH = path.resolve(new URL(import.meta.url).pathname, '../../../../../');
|
|
10
|
+
const KNOWN_VIEWING_KEY = 'mn_shield-esk_undeployed1d45kgmnfva58gwn9de3hy7tsw35k7m3dwdjkxun9wskkketetdmrzhf6dlyj7u8juj68fd4psnkqhjxh32sec0q480vzswg8kd485e2kljcsmxqc0u';
|
|
11
|
+
const timeout_minutes = (mins) => 1_000 * 60 * mins;
|
|
12
|
+
describe('Wallet subscription', () => {
|
|
13
|
+
describe('with available Indexer Server', () => {
|
|
14
|
+
const environmentId = randomUUID();
|
|
15
|
+
let environment = undefined;
|
|
16
|
+
const getIndexerPort = () => environment?.getContainer(`indexer_${environmentId}`).getMappedPort(8088) ?? 8088;
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
environment = await new DockerComposeEnvironment(COMPOSE_PATH, 'docker-compose.yml')
|
|
19
|
+
.withEnvironment({
|
|
20
|
+
TESTCONTAINERS_UID: environmentId,
|
|
21
|
+
})
|
|
22
|
+
.withWaitStrategy(`node_${environmentId}`, Wait.forListeningPorts())
|
|
23
|
+
.withWaitStrategy(`indexer_${environmentId}`, Wait.forListeningPorts())
|
|
24
|
+
.up();
|
|
25
|
+
}, timeout_minutes(3));
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await environment?.down();
|
|
28
|
+
}, timeout_minutes(1));
|
|
29
|
+
it('should stream GraphQL subscription', async () => {
|
|
30
|
+
const makeScopedSession = Effect.acquireRelease(Connect.run({ viewingKey: KNOWN_VIEWING_KEY }), (session) => Disconnect.run({ sessionId: session.connect }).pipe(Effect.catchAll((_) => Effect.void)));
|
|
31
|
+
await Effect.gen(function* () {
|
|
32
|
+
const session = yield* makeScopedSession;
|
|
33
|
+
const events = yield* ShieldedTransactions.run({
|
|
34
|
+
sessionId: session.connect,
|
|
35
|
+
index: null,
|
|
36
|
+
}).pipe(Stream.take(2), Stream.tap((data) => Effect.log(data.shieldedTransactions.__typename)), Stream.runCollect);
|
|
37
|
+
expect(events).toHaveLength(2);
|
|
38
|
+
}).pipe(Effect.provide(HttpQueryClient.layer({ url: `http://127.0.0.1:${getIndexerPort()}/api/v3/graphql` })), Effect.provide(WsSubscriptionClient.layer({ url: `ws://127.0.0.1:${getIndexerPort()}/api/v3/graphql/ws` })), Effect.scoped, Effect.runPromise);
|
|
39
|
+
}, timeout_minutes(1));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Stream } from 'effect';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { DockerComposeEnvironment, Wait } from 'testcontainers';
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
6
|
+
import { WsSubscriptionClient } from '../../../effect/index.js';
|
|
7
|
+
import { UnshieldedTransactions } from '../UnshieldedTransactions.js';
|
|
8
|
+
const COMPOSE_PATH = path.resolve(new URL(import.meta.url).pathname, '../../../../../');
|
|
9
|
+
const timeout_minutes = (mins) => 1_000 * 60 * mins;
|
|
10
|
+
const ADDRESS = 'mn_addr_undeployed1rhqz8aq6t74ym2uq5gh53t9x02gducxnamtdvnjxfhelxwaf8ztqpmrwwj';
|
|
11
|
+
describe('Wallet subscription', () => {
|
|
12
|
+
describe('with available Indexer Server', () => {
|
|
13
|
+
const environmentId = randomUUID();
|
|
14
|
+
let environment = undefined;
|
|
15
|
+
const getIndexerPort = () => environment?.getContainer(`indexer_${environmentId}`).getMappedPort(8088) ?? 8088;
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
environment = await new DockerComposeEnvironment(COMPOSE_PATH, 'docker-compose.yml')
|
|
18
|
+
.withEnvironment({
|
|
19
|
+
TESTCONTAINERS_UID: environmentId,
|
|
20
|
+
})
|
|
21
|
+
.withWaitStrategy(`node_${environmentId}`, Wait.forListeningPorts())
|
|
22
|
+
.withWaitStrategy(`indexer_${environmentId}`, Wait.forLogMessage(/block indexed/))
|
|
23
|
+
.up();
|
|
24
|
+
}, timeout_minutes(3));
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await environment?.down();
|
|
27
|
+
}, timeout_minutes(1));
|
|
28
|
+
it('should stream GraphQL subscription', async () => {
|
|
29
|
+
await Effect.gen(function* () {
|
|
30
|
+
const events = yield* UnshieldedTransactions.run({
|
|
31
|
+
address: ADDRESS,
|
|
32
|
+
transactionId: 0,
|
|
33
|
+
}).pipe(Stream.take(2), Stream.tap((data) => Effect.log(data.unshieldedTransactions.type)), Stream.runCollect);
|
|
34
|
+
expect(events).toHaveLength(2);
|
|
35
|
+
}).pipe(Effect.provide(WsSubscriptionClient.layer({ url: `ws://127.0.0.1:${getIndexerPort()}/api/v3/graphql/ws` })), Effect.scoped, Effect.runPromise);
|
|
36
|
+
}, timeout_minutes(1));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, Stream } from 'effect';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { DockerComposeEnvironment, Wait } from 'testcontainers';
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
6
|
+
import { WsSubscriptionClient } from '../../../effect/index.js';
|
|
7
|
+
import { ZswapEvents } from '../ZswapEvents.js';
|
|
8
|
+
const COMPOSE_PATH = path.resolve(new URL(import.meta.url).pathname, '../../../../../');
|
|
9
|
+
const timeout_minutes = (mins) => 1_000 * 60 * mins;
|
|
10
|
+
describe('ZSwap events subscription', () => {
|
|
11
|
+
describe('with available Indexer Server', () => {
|
|
12
|
+
const environmentId = randomUUID();
|
|
13
|
+
let environment = undefined;
|
|
14
|
+
const getIndexerPort = () => environment?.getContainer(`indexer_${environmentId}`).getMappedPort(8088) ?? 8088;
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
environment = await new DockerComposeEnvironment(COMPOSE_PATH, 'docker-compose.yml')
|
|
17
|
+
.withEnvironment({
|
|
18
|
+
TESTCONTAINERS_UID: environmentId,
|
|
19
|
+
})
|
|
20
|
+
.withWaitStrategy(`node_${environmentId}`, Wait.forListeningPorts())
|
|
21
|
+
.withWaitStrategy(`indexer_${environmentId}`, Wait.forLogMessage(/block indexed/))
|
|
22
|
+
.up();
|
|
23
|
+
}, timeout_minutes(3));
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await environment?.down();
|
|
26
|
+
}, timeout_minutes(1));
|
|
27
|
+
it('should stream GraphQL subscription', async () => {
|
|
28
|
+
await Effect.gen(function* () {
|
|
29
|
+
const events = yield* ZswapEvents.run({
|
|
30
|
+
id: 0,
|
|
31
|
+
}).pipe(Stream.take(2), Stream.tap((data) => Effect.log(`ID=${data.zswapLedgerEvents.id}, MAX_ID=${data.zswapLedgerEvents.maxId}`)), Stream.runCollect);
|
|
32
|
+
expect(events).toHaveLength(2);
|
|
33
|
+
}).pipe(Effect.provide(WsSubscriptionClient.layer({ url: `ws://127.0.0.1:${getIndexerPort()}/api/v3/graphql/ws` })), Effect.scoped, Effect.runPromise);
|
|
34
|
+
}, timeout_minutes(1));
|
|
35
|
+
});
|
|
36
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@midnight-ntwrk/wallet-sdk-indexer-client",
|
|
3
|
+
"version": "1.0.0-beta.11",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"module": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"author": "IOHK",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"registry": "https://npm.pkg.github.com/"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/midnight-ntwrk/artifacts.git"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./effect": {
|
|
25
|
+
"types": "./dist/effect/index.d.ts",
|
|
26
|
+
"import": "./dist/effect/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@effect/platform": "^0.90.0",
|
|
31
|
+
"@graphql-typed-document-node/core": "^3.2.0",
|
|
32
|
+
"@midnight-ntwrk/wallet-sdk-abstractions": "1.0.0-beta.8",
|
|
33
|
+
"@midnight-ntwrk/wallet-sdk-utilities": "1.0.0-beta.7",
|
|
34
|
+
"effect": "^3.17.3",
|
|
35
|
+
"graphql": "^16.11.0",
|
|
36
|
+
"graphql-http": "^1.22.4",
|
|
37
|
+
"graphql-ws": "^6.0.5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@graphql-codegen/cli": "^5.0.7",
|
|
41
|
+
"@graphql-codegen/client-preset": "^4.8.2",
|
|
42
|
+
"@graphql-codegen/typescript": "^4.1.6",
|
|
43
|
+
"@graphql-codegen/typescript-operations": "^4.6.1",
|
|
44
|
+
"eslint": "^9.37.0",
|
|
45
|
+
"publint": "~0.3.14",
|
|
46
|
+
"rimraf": "^6.0.1",
|
|
47
|
+
"testcontainers": "^11.4.0",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^3.2.4"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"gql:codegen": "graphql-codegen",
|
|
53
|
+
"typecheck": "tsc -b ./tsconfig.json --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"lint": "eslint",
|
|
56
|
+
"dist": "tsc -b ./tsconfig.build.json",
|
|
57
|
+
"dist:publish": "tsc -b ./tsconfig.publish.json",
|
|
58
|
+
"clean": "rimraf --glob dist 'tsconfig.*.tsbuildinfo' && date +%s > .clean-timestamp",
|
|
59
|
+
"publint": "publint --strict"
|
|
60
|
+
}
|
|
61
|
+
}
|