@distrohelena/canton-typescript-sdk 0.1.31 → 0.1.33
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 +56 -0
- package/dist/cjs/services/party-management/decentralized-party-lifecycle.js +16 -2
- package/dist/services/party-management/decentralized-party-lifecycle.js +16 -2
- package/node/participant-358-synchronizer.mjs +142 -0
- package/node/start-local-participant-358.sh +184 -0
- package/node/start-local.sh +5 -4
- package/node/stop-local-participant-358.sh +18 -0
- package/node/test-participant-358-sidecar-smoke.sh +19 -0
- package/node/test-participant-358-sidecar.sh +83 -0
- package/node/test-start-local.sh +34 -2
- package/package.json +8 -2
- package/node/.generated/start-local/additional-config.extra-participants.conf +0 -144
- package/node/.generated/start-local/additional-config.extra-validators.conf +0 -120
- package/node/.generated/start-local/compose-extra-participants.yaml +0 -459
- package/node/.generated/start-local/extra-participants.env +0 -88
package/README.md
CHANGED
|
@@ -86,6 +86,12 @@ Participant 3.5.7. In particular, external-party allocation signs the generated
|
|
|
86
86
|
combined topology hash (`multi_hash`) rather than each topology transaction
|
|
87
87
|
individually.
|
|
88
88
|
|
|
89
|
+
The decentralized Ed25519 example was also run end-to-end against an isolated
|
|
90
|
+
Canton Participant 3.5.8 sidecar. On both 3.5.7 and 3.5.8 it creates the same
|
|
91
|
+
topology shape: a serial-1 decentralized namespace definition, one self-root
|
|
92
|
+
namespace certificate per owner, and a PartyToParticipant containing the party
|
|
93
|
+
signing key. No deprecated PartyToKeyMapping is created.
|
|
94
|
+
|
|
89
95
|
## Localnet launchers
|
|
90
96
|
|
|
91
97
|
The published package includes launchers for an existing CN Quickstart checkout.
|
|
@@ -110,6 +116,56 @@ npm exec --package @distrohelena/canton-typescript-sdk canton-localnet-stop
|
|
|
110
116
|
These commands launch and stop CN Quickstart; they do not provision a
|
|
111
117
|
Quickstart checkout.
|
|
112
118
|
|
|
119
|
+
### Optional Canton 3.5.8 participant sidecar
|
|
120
|
+
|
|
121
|
+
For SDK compatibility work, an isolated Canton 3.5.8 participant can join an
|
|
122
|
+
already-running CN Quickstart localnet without changing its files or Compose
|
|
123
|
+
project:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
canton-localnet-participant-358-start
|
|
127
|
+
canton-localnet-participant-358-stop
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The sidecar owns `.generated/participant-358`, its `canton-participant-358`
|
|
131
|
+
Compose project, its Postgres container, and host ports `8901` (Ledger), `8902`
|
|
132
|
+
(Admin), and `8975` (JSON). The start launcher reads the existing localnet's
|
|
133
|
+
registered synchronizer via its Admin API, stores the exported connection
|
|
134
|
+
configuration in the sidecar runtime directory, and connects the sidecar with
|
|
135
|
+
its own Admin API. It generates a five-minute 3.5.8-compatible development JWT
|
|
136
|
+
at `.generated/participant-358/ledger-api-user.token` and prints both live-test
|
|
137
|
+
and example endpoint/token exports when the connection is healthy.
|
|
138
|
+
`cn-quickstart` is strictly read-only: this launcher never writes to, starts,
|
|
139
|
+
or stops the normal Quickstart stack.
|
|
140
|
+
|
|
141
|
+
After copying the printed `SDK_EXAMPLE_*` exports into the current shell, run
|
|
142
|
+
the standalone decentralized-party comparison example with:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run example:party:decentralized
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The defaults target the normal insecure shared-secret localnet at
|
|
149
|
+
`localhost:3902` on its `quickstart` Docker network. Override them when your
|
|
150
|
+
localnet differs:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT=localhost:3902 \
|
|
154
|
+
PARTICIPANT_358_SOURCE_ADMIN_BEARER_TOKEN="$TOKEN" \
|
|
155
|
+
PARTICIPANT_358_NETWORK=quickstart \
|
|
156
|
+
canton-localnet-participant-358-start
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `PARTICIPANT_358_CANTON_IMAGE`, `PARTICIPANT_358_PROJECT_NAME`,
|
|
160
|
+
`PARTICIPANT_358_RUNTIME_DIR`, `PARTICIPANT_358_LEDGER_PORT`,
|
|
161
|
+
`PARTICIPANT_358_ADMIN_PORT`, and `PARTICIPANT_358_JSON_PORT` to make an
|
|
162
|
+
explicitly isolated variant. The opt-in Docker check requires a running
|
|
163
|
+
localnet and is never part of the normal test suite:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
PARTICIPANT_358_SMOKE_TEST=1 npm run test:participant-358-sidecar-smoke
|
|
167
|
+
```
|
|
168
|
+
|
|
113
169
|
### Optional ES256 bearer tokens
|
|
114
170
|
|
|
115
171
|
Set `LOCALNET_ES256_JWT=1` when starting the localnet to add ES256 JWT
|
|
@@ -15,6 +15,17 @@ async function prepareDecentralizedPartyAsync(request, localParticipant, topolog
|
|
|
15
15
|
const partyId = `${request.partyHint}::${decentralizedNamespace}`;
|
|
16
16
|
const owners = request.owners.map(toTopologyKey);
|
|
17
17
|
const partyKeys = request.partySigningKeys.map(toTopologyKey);
|
|
18
|
+
const store = {
|
|
19
|
+
store: {
|
|
20
|
+
oneofKind: "synchronizer",
|
|
21
|
+
synchronizer: {
|
|
22
|
+
kind: {
|
|
23
|
+
oneofKind: "id",
|
|
24
|
+
id: request.synchronizer,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
18
29
|
const participants = [
|
|
19
30
|
{
|
|
20
31
|
participantUid: localParticipant.participantId,
|
|
@@ -30,6 +41,7 @@ async function prepareDecentralizedPartyAsync(request, localParticipant, topolog
|
|
|
30
41
|
{
|
|
31
42
|
operation: topology_js_1.Enums_TopologyChangeOp.ADD_REPLACE,
|
|
32
43
|
serial: 1,
|
|
44
|
+
store,
|
|
33
45
|
mapping: {
|
|
34
46
|
mapping: {
|
|
35
47
|
oneofKind: "decentralizedNamespaceDefinition",
|
|
@@ -41,14 +53,15 @@ async function prepareDecentralizedPartyAsync(request, localParticipant, topolog
|
|
|
41
53
|
},
|
|
42
54
|
},
|
|
43
55
|
},
|
|
44
|
-
...owners.map((targetKey) => ({
|
|
56
|
+
...owners.map((targetKey, index) => ({
|
|
45
57
|
operation: topology_js_1.Enums_TopologyChangeOp.ADD_REPLACE,
|
|
46
58
|
serial: 1,
|
|
59
|
+
store,
|
|
47
60
|
mapping: {
|
|
48
61
|
mapping: {
|
|
49
62
|
oneofKind: "namespaceDelegation",
|
|
50
63
|
namespaceDelegation: {
|
|
51
|
-
namespace:
|
|
64
|
+
namespace: ownerFingerprints[index],
|
|
52
65
|
targetKey,
|
|
53
66
|
isRootDelegation: true,
|
|
54
67
|
restriction: { oneofKind: undefined },
|
|
@@ -59,6 +72,7 @@ async function prepareDecentralizedPartyAsync(request, localParticipant, topolog
|
|
|
59
72
|
{
|
|
60
73
|
operation: topology_js_1.Enums_TopologyChangeOp.ADD_REPLACE,
|
|
61
74
|
serial: 1,
|
|
75
|
+
store,
|
|
62
76
|
mapping: {
|
|
63
77
|
mapping: {
|
|
64
78
|
oneofKind: "partyToParticipant",
|
|
@@ -12,6 +12,17 @@ export async function prepareDecentralizedPartyAsync(request, localParticipant,
|
|
|
12
12
|
const partyId = `${request.partyHint}::${decentralizedNamespace}`;
|
|
13
13
|
const owners = request.owners.map(toTopologyKey);
|
|
14
14
|
const partyKeys = request.partySigningKeys.map(toTopologyKey);
|
|
15
|
+
const store = {
|
|
16
|
+
store: {
|
|
17
|
+
oneofKind: "synchronizer",
|
|
18
|
+
synchronizer: {
|
|
19
|
+
kind: {
|
|
20
|
+
oneofKind: "id",
|
|
21
|
+
id: request.synchronizer,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
15
26
|
const participants = [
|
|
16
27
|
{
|
|
17
28
|
participantUid: localParticipant.participantId,
|
|
@@ -27,6 +38,7 @@ export async function prepareDecentralizedPartyAsync(request, localParticipant,
|
|
|
27
38
|
{
|
|
28
39
|
operation: Enums_TopologyChangeOp.ADD_REPLACE,
|
|
29
40
|
serial: 1,
|
|
41
|
+
store,
|
|
30
42
|
mapping: {
|
|
31
43
|
mapping: {
|
|
32
44
|
oneofKind: "decentralizedNamespaceDefinition",
|
|
@@ -38,14 +50,15 @@ export async function prepareDecentralizedPartyAsync(request, localParticipant,
|
|
|
38
50
|
},
|
|
39
51
|
},
|
|
40
52
|
},
|
|
41
|
-
...owners.map((targetKey) => ({
|
|
53
|
+
...owners.map((targetKey, index) => ({
|
|
42
54
|
operation: Enums_TopologyChangeOp.ADD_REPLACE,
|
|
43
55
|
serial: 1,
|
|
56
|
+
store,
|
|
44
57
|
mapping: {
|
|
45
58
|
mapping: {
|
|
46
59
|
oneofKind: "namespaceDelegation",
|
|
47
60
|
namespaceDelegation: {
|
|
48
|
-
namespace:
|
|
61
|
+
namespace: ownerFingerprints[index],
|
|
49
62
|
targetKey,
|
|
50
63
|
isRootDelegation: true,
|
|
51
64
|
restriction: { oneofKind: undefined },
|
|
@@ -56,6 +69,7 @@ export async function prepareDecentralizedPartyAsync(request, localParticipant,
|
|
|
56
69
|
{
|
|
57
70
|
operation: Enums_TopologyChangeOp.ADD_REPLACE,
|
|
58
71
|
serial: 1,
|
|
72
|
+
store,
|
|
59
73
|
mapping: {
|
|
60
74
|
mapping: {
|
|
61
75
|
oneofKind: "partyToParticipant",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { credentials } from "@grpc/grpc-js";
|
|
4
|
+
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
|
|
5
|
+
import { SynchronizerConnectivityServiceClient } from "../dist/transports/grpc/generated/canton/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.client.js";
|
|
6
|
+
import { ListRegisteredSynchronizersResponse_Status } from "../dist/transports/grpc/generated/canton/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.js";
|
|
7
|
+
import { SequencerConnectionValidation } from "../dist/transports/grpc/generated/canton/com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.js";
|
|
8
|
+
import { VersionServiceClient } from "../dist/transports/grpc/generated/canton/com/daml/ledger/api/v2/version_service.client.js";
|
|
9
|
+
|
|
10
|
+
function getRequiredEnvironment(name) {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (!value) throw new Error(`${name} must be set.`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createSharedSecretToken() {
|
|
17
|
+
const secret = process.env.PARTICIPANT_358_SOURCE_SHARED_SECRET ?? "unsafe";
|
|
18
|
+
const audience = process.env.PARTICIPANT_358_SOURCE_AUTH_AUDIENCE
|
|
19
|
+
?? "https://canton.network.global";
|
|
20
|
+
const subject = process.env.PARTICIPANT_358_SOURCE_AUTH_SUBJECT
|
|
21
|
+
?? "ledger-api-user";
|
|
22
|
+
const ttlSeconds = Number.parseInt(
|
|
23
|
+
process.env.PARTICIPANT_358_TOKEN_TTL_SECONDS ?? "300",
|
|
24
|
+
10,
|
|
25
|
+
);
|
|
26
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
27
|
+
throw new Error("PARTICIPANT_358_TOKEN_TTL_SECONDS must be a positive integer.");
|
|
28
|
+
}
|
|
29
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
30
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
31
|
+
const payload = Buffer.from(JSON.stringify({
|
|
32
|
+
aud: audience,
|
|
33
|
+
sub: subject,
|
|
34
|
+
iat: issuedAt,
|
|
35
|
+
exp: issuedAt + ttlSeconds,
|
|
36
|
+
})).toString("base64url");
|
|
37
|
+
const signature = createHmac("sha256", secret)
|
|
38
|
+
.update(`${header}.${payload}`)
|
|
39
|
+
.digest("base64url");
|
|
40
|
+
return `${header}.${payload}.${signature}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function writeLedgerToken() {
|
|
44
|
+
const targetFile = getRequiredEnvironment("PARTICIPANT_358_LEDGER_TOKEN_FILE");
|
|
45
|
+
await writeFile(targetFile, `${createSharedSecretToken()}\n`, { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createClient(endpoint, token) {
|
|
49
|
+
const transport = new GrpcTransport({
|
|
50
|
+
host: endpoint.replace(/^https?:\/\//, ""),
|
|
51
|
+
channelCredentials: credentials.createInsecure(),
|
|
52
|
+
});
|
|
53
|
+
const options = token ? { meta: { authorization: `Bearer ${token}` } } : undefined;
|
|
54
|
+
return { client: new SynchronizerConnectivityServiceClient(transport), options, transport };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function encode(value) {
|
|
58
|
+
if (value instanceof Uint8Array) return { $bytes: Buffer.from(value).toString("base64") };
|
|
59
|
+
if (Array.isArray(value)) return value.map(encode);
|
|
60
|
+
if (value && typeof value === "object") {
|
|
61
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, encode(nested)]));
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function decode(value) {
|
|
67
|
+
if (Array.isArray(value)) return value.map(decode);
|
|
68
|
+
if (value && typeof value === "object") {
|
|
69
|
+
if (typeof value.$bytes === "string") return Buffer.from(value.$bytes, "base64");
|
|
70
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, decode(nested)]));
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function exportSynchronizerConfig() {
|
|
76
|
+
const targetFile = getRequiredEnvironment("PARTICIPANT_358_SYNCHRONIZER_CONFIG");
|
|
77
|
+
const sourceEndpoint = getRequiredEnvironment("PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT");
|
|
78
|
+
const sourceToken = process.env.PARTICIPANT_358_SOURCE_ADMIN_BEARER_TOKEN ?? createSharedSecretToken();
|
|
79
|
+
const { client, options, transport } = createClient(sourceEndpoint, sourceToken);
|
|
80
|
+
try {
|
|
81
|
+
const response = await client.listRegisteredSynchronizers({ allStatuses: false }, options).response;
|
|
82
|
+
const connected = response.results.filter((result) => result.connected && result.config);
|
|
83
|
+
if (connected.length !== 1) {
|
|
84
|
+
throw new Error(`Expected exactly one connected synchronizer from ${sourceEndpoint}; found ${connected.length}. Set PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT to a participant with one connected synchronizer.`);
|
|
85
|
+
}
|
|
86
|
+
await writeFile(targetFile, `${JSON.stringify(encode(connected[0].config), null, 2)}\n`, { mode: 0o600 });
|
|
87
|
+
} finally {
|
|
88
|
+
transport.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function connectSynchronizer() {
|
|
93
|
+
const configFile = getRequiredEnvironment("PARTICIPANT_358_SYNCHRONIZER_CONFIG");
|
|
94
|
+
const targetEndpoint = getRequiredEnvironment("PARTICIPANT_358_ADMIN_ENDPOINT");
|
|
95
|
+
const targetToken = process.env.PARTICIPANT_358_ADMIN_BEARER_TOKEN ?? createSharedSecretToken();
|
|
96
|
+
const config = decode(JSON.parse(await readFile(configFile, "utf8")));
|
|
97
|
+
const { client, options, transport } = createClient(targetEndpoint, targetToken);
|
|
98
|
+
try {
|
|
99
|
+
const connectResponse = await client.connectSynchronizer({
|
|
100
|
+
config,
|
|
101
|
+
sequencerConnectionValidation: SequencerConnectionValidation.ALL,
|
|
102
|
+
}, options).response;
|
|
103
|
+
if (!connectResponse.connectedSuccessfully) {
|
|
104
|
+
throw new Error("ConnectSynchronizer completed without a healthy connection.");
|
|
105
|
+
}
|
|
106
|
+
const registered = await client.listRegisteredSynchronizers({ allStatuses: false }, options).response;
|
|
107
|
+
const isHealthy = registered.results.some(
|
|
108
|
+
(result) => result.connected
|
|
109
|
+
&& result.status === ListRegisteredSynchronizersResponse_Status.ACTIVE
|
|
110
|
+
&& result.config?.synchronizerAlias === config.synchronizerAlias,
|
|
111
|
+
);
|
|
112
|
+
if (!isHealthy) throw new Error("Synchronizer did not reach active/connected state.");
|
|
113
|
+
} finally {
|
|
114
|
+
transport.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readLedgerApiVersion() {
|
|
119
|
+
const endpoint = getRequiredEnvironment("PARTICIPANT_358_LEDGER_ENDPOINT");
|
|
120
|
+
const token = process.env.PARTICIPANT_358_LEDGER_BEARER_TOKEN ?? createSharedSecretToken();
|
|
121
|
+
const { options, transport } = createClient(endpoint, token);
|
|
122
|
+
try {
|
|
123
|
+
const response = await new VersionServiceClient(transport)
|
|
124
|
+
.getLedgerApiVersion({}, options).response;
|
|
125
|
+
process.stdout.write(`${response.version}\n`);
|
|
126
|
+
} finally {
|
|
127
|
+
transport.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const command = process.argv[2];
|
|
132
|
+
if (command === "export") {
|
|
133
|
+
await exportSynchronizerConfig();
|
|
134
|
+
} else if (command === "connect") {
|
|
135
|
+
await connectSynchronizer();
|
|
136
|
+
} else if (command === "ledger-api-version") {
|
|
137
|
+
await readLedgerApiVersion();
|
|
138
|
+
} else if (command === "mint-ledger-token") {
|
|
139
|
+
await writeLedgerToken();
|
|
140
|
+
} else {
|
|
141
|
+
throw new Error("Expected 'export', 'connect', 'ledger-api-version', or 'mint-ledger-token'.");
|
|
142
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
RUNTIME_DIR="${PARTICIPANT_358_RUNTIME_DIR:-$REPO_ROOT/.generated/participant-358}"
|
|
7
|
+
PROJECT_NAME="${PARTICIPANT_358_PROJECT_NAME:-canton-participant-358}"
|
|
8
|
+
CANTON_IMAGE="${PARTICIPANT_358_CANTON_IMAGE:-ghcr.io/digital-asset/decentralized-canton-sync/docker/canton:0.6.12}"
|
|
9
|
+
POSTGRES_IMAGE="${PARTICIPANT_358_POSTGRES_IMAGE:-postgres:16}"
|
|
10
|
+
NETWORK_NAME="${PARTICIPANT_358_NETWORK:-quickstart}"
|
|
11
|
+
LEDGER_PORT="${PARTICIPANT_358_LEDGER_PORT:-8901}"
|
|
12
|
+
ADMIN_PORT="${PARTICIPANT_358_ADMIN_PORT:-8902}"
|
|
13
|
+
JSON_PORT="${PARTICIPANT_358_JSON_PORT:-8975}"
|
|
14
|
+
SOURCE_ADMIN_ENDPOINT="${PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT:-localhost:3902}"
|
|
15
|
+
DOCKER_COMPOSE_CMD=()
|
|
16
|
+
|
|
17
|
+
resolve_docker_compose_cmd() {
|
|
18
|
+
if docker compose version >/dev/null 2>&1; then DOCKER_COMPOSE_CMD=(docker compose); return; fi
|
|
19
|
+
if docker-compose version >/dev/null 2>&1; then DOCKER_COMPOSE_CMD=(docker-compose); return; fi
|
|
20
|
+
echo "Unable to find a working Docker Compose command." >&2
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
validate_port() {
|
|
25
|
+
local value="$1"
|
|
26
|
+
[[ "$value" =~ ^[1-9][0-9]{0,4}$ ]] && (( value <= 65535 )) || { echo "Invalid port: $value" >&2; exit 1; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for port in "$LEDGER_PORT" "$ADMIN_PORT" "$JSON_PORT"; do validate_port "$port"; done
|
|
30
|
+
if [[ "$LEDGER_PORT" == "$ADMIN_PORT" || "$LEDGER_PORT" == "$JSON_PORT" || "$ADMIN_PORT" == "$JSON_PORT" ]]; then
|
|
31
|
+
echo "Participant 3.5.8 Ledger, Admin, and JSON ports must be distinct." >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
for normal_port in 3901 3902 3975; do
|
|
35
|
+
if [[ "$LEDGER_PORT" == "$normal_port" || "$ADMIN_PORT" == "$normal_port" || "$JSON_PORT" == "$normal_port" ]]; then
|
|
36
|
+
echo "Participant 3.5.8 ports must not collide with CN Quickstart ports." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
assert_host_port_available() {
|
|
42
|
+
local port="$1"
|
|
43
|
+
if command -v ss >/dev/null 2>&1 && ss -ltn "sport = :$port" | tail -n +2 | grep -q .; then
|
|
44
|
+
echo "Host port $port is already in use. Set the corresponding PARTICIPANT_358_*_PORT value." >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
for port in "$LEDGER_PORT" "$ADMIN_PORT" "$JSON_PORT"; do assert_host_port_available "$port"; done
|
|
49
|
+
|
|
50
|
+
mkdir -p "$RUNTIME_DIR"
|
|
51
|
+
chmod 700 "$RUNTIME_DIR"
|
|
52
|
+
SYNCHRONIZER_CONFIG="$RUNTIME_DIR/registered-synchronizer.json"
|
|
53
|
+
LEDGER_TOKEN_FILE="$RUNTIME_DIR/ledger-api-user.token"
|
|
54
|
+
PARTICIPANT_358_LEDGER_TOKEN_FILE="$LEDGER_TOKEN_FILE" \
|
|
55
|
+
node "$SCRIPT_DIR/participant-358-synchronizer.mjs" mint-ledger-token
|
|
56
|
+
cat > "$RUNTIME_DIR/participant-358.env" <<EOF
|
|
57
|
+
PARTICIPANT_358_SYNCHRONIZER_CONFIG=$SYNCHRONIZER_CONFIG
|
|
58
|
+
PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT=$SOURCE_ADMIN_ENDPOINT
|
|
59
|
+
PARTICIPANT_358_ADMIN_ENDPOINT=localhost:$ADMIN_PORT
|
|
60
|
+
PARTICIPANT_358_LEDGER_TOKEN_FILE=$LEDGER_TOKEN_FILE
|
|
61
|
+
EOF
|
|
62
|
+
chmod 600 "$RUNTIME_DIR/participant-358.env"
|
|
63
|
+
|
|
64
|
+
cat > "$RUNTIME_DIR/canton.conf" <<EOF
|
|
65
|
+
canton {
|
|
66
|
+
parameters { manual-start = no }
|
|
67
|
+
participants.participant358 {
|
|
68
|
+
storage {
|
|
69
|
+
type = postgres
|
|
70
|
+
config {
|
|
71
|
+
dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
|
|
72
|
+
properties {
|
|
73
|
+
serverName = \${?DB_SERVER}
|
|
74
|
+
portNumber = 5432
|
|
75
|
+
databaseName = participant358
|
|
76
|
+
currentSchema = participant358
|
|
77
|
+
user = \${?DB_USER}
|
|
78
|
+
password = \${?DB_PASSWORD}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
init {
|
|
83
|
+
generate-topology-transactions-and-keys = true
|
|
84
|
+
identity.type = auto
|
|
85
|
+
}
|
|
86
|
+
admin-api {
|
|
87
|
+
address = "0.0.0.0"
|
|
88
|
+
port = 5002
|
|
89
|
+
}
|
|
90
|
+
ledger-api {
|
|
91
|
+
address = "0.0.0.0"
|
|
92
|
+
port = 5001
|
|
93
|
+
auth-services = [{
|
|
94
|
+
type = unsafe-jwt-hmac-256
|
|
95
|
+
target-audience = "https://canton.network.global"
|
|
96
|
+
secret = "unsafe"
|
|
97
|
+
}]
|
|
98
|
+
user-management-service.additional-admin-user-id = "ledger-api-user"
|
|
99
|
+
}
|
|
100
|
+
http-ledger-api {
|
|
101
|
+
address = "0.0.0.0"
|
|
102
|
+
port = 7575
|
|
103
|
+
}
|
|
104
|
+
monitoring.http-health-server {
|
|
105
|
+
address = "0.0.0.0"
|
|
106
|
+
port = 7000
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
EOF
|
|
111
|
+
|
|
112
|
+
cat > "$RUNTIME_DIR/compose.yaml" <<EOF
|
|
113
|
+
name: $PROJECT_NAME
|
|
114
|
+
services:
|
|
115
|
+
postgres:
|
|
116
|
+
image: $POSTGRES_IMAGE
|
|
117
|
+
environment:
|
|
118
|
+
POSTGRES_DB: participant358
|
|
119
|
+
POSTGRES_USER: participant358
|
|
120
|
+
POSTGRES_PASSWORD: participant358
|
|
121
|
+
healthcheck:
|
|
122
|
+
test: ["CMD-SHELL", "pg_isready -U participant358 -d participant358"]
|
|
123
|
+
interval: 2s
|
|
124
|
+
timeout: 3s
|
|
125
|
+
retries: 30
|
|
126
|
+
participant358:
|
|
127
|
+
image: $CANTON_IMAGE
|
|
128
|
+
depends_on:
|
|
129
|
+
postgres:
|
|
130
|
+
condition: service_healthy
|
|
131
|
+
environment:
|
|
132
|
+
DB_SERVER: postgres
|
|
133
|
+
DB_USER: participant358
|
|
134
|
+
DB_PASSWORD: participant358
|
|
135
|
+
volumes:
|
|
136
|
+
- "$RUNTIME_DIR/canton.conf:/app/app.conf:ro"
|
|
137
|
+
ports:
|
|
138
|
+
- "127.0.0.1:$LEDGER_PORT:5001"
|
|
139
|
+
- "127.0.0.1:$ADMIN_PORT:5002"
|
|
140
|
+
- "127.0.0.1:$JSON_PORT:7575"
|
|
141
|
+
networks:
|
|
142
|
+
- default
|
|
143
|
+
- localnet
|
|
144
|
+
networks:
|
|
145
|
+
localnet:
|
|
146
|
+
external: true
|
|
147
|
+
name: $NETWORK_NAME
|
|
148
|
+
EOF
|
|
149
|
+
|
|
150
|
+
if [[ "${PARTICIPANT_358_SKIP_SYNCHRONIZER_CONNECT:-0}" != "1" ]]; then
|
|
151
|
+
PARTICIPANT_358_SYNCHRONIZER_CONFIG="$SYNCHRONIZER_CONFIG" \
|
|
152
|
+
PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT="$SOURCE_ADMIN_ENDPOINT" \
|
|
153
|
+
PARTICIPANT_358_SOURCE_ADMIN_BEARER_TOKEN="${PARTICIPANT_358_SOURCE_ADMIN_BEARER_TOKEN:-}" \
|
|
154
|
+
node "$SCRIPT_DIR/participant-358-synchronizer.mjs" export
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
resolve_docker_compose_cmd
|
|
158
|
+
"${DOCKER_COMPOSE_CMD[@]}" --project-name "$PROJECT_NAME" --file "$RUNTIME_DIR/compose.yaml" up -d
|
|
159
|
+
|
|
160
|
+
if [[ "${PARTICIPANT_358_SKIP_SYNCHRONIZER_CONNECT:-0}" != "1" ]]; then
|
|
161
|
+
deadline=$((SECONDS + ${PARTICIPANT_358_CONNECT_TIMEOUT_SECONDS:-90}))
|
|
162
|
+
until PARTICIPANT_358_SYNCHRONIZER_CONFIG="$SYNCHRONIZER_CONFIG" \
|
|
163
|
+
PARTICIPANT_358_ADMIN_ENDPOINT="localhost:$ADMIN_PORT" \
|
|
164
|
+
PARTICIPANT_358_ADMIN_BEARER_TOKEN="${PARTICIPANT_358_ADMIN_BEARER_TOKEN:-}" \
|
|
165
|
+
node "$SCRIPT_DIR/participant-358-synchronizer.mjs" connect
|
|
166
|
+
do
|
|
167
|
+
if (( SECONDS >= deadline )); then
|
|
168
|
+
echo "Participant 3.5.8 sidecar did not become ready. Inspect: ${DOCKER_COMPOSE_CMD[*]} --project-name $PROJECT_NAME --file $RUNTIME_DIR/compose.yaml logs participant358" >&2
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
sleep 2
|
|
172
|
+
done
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
cat <<EOF
|
|
176
|
+
Participant 3.5.8 sidecar is ready.
|
|
177
|
+
export SDK_TEST_LEDGER_ENDPOINT=localhost:$LEDGER_PORT
|
|
178
|
+
export SDK_TEST_LEDGER_ADMIN_ENDPOINT=localhost:$LEDGER_PORT
|
|
179
|
+
export SDK_TEST_PARTICIPANT_ADMIN_ENDPOINT=localhost:$ADMIN_PORT
|
|
180
|
+
export SDK_EXAMPLE_LEDGER_ENDPOINT=localhost:$LEDGER_PORT
|
|
181
|
+
export SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT=localhost:$LEDGER_PORT
|
|
182
|
+
export SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT=localhost:$ADMIN_PORT
|
|
183
|
+
export SDK_EXAMPLE_BEARER_TOKEN="\$(cat $LEDGER_TOKEN_FILE)"
|
|
184
|
+
EOF
|
package/node/start-local.sh
CHANGED
|
@@ -911,7 +911,7 @@ extra_participants_healthy() {
|
|
|
911
911
|
local prefix=$((index + 4))
|
|
912
912
|
local health_port
|
|
913
913
|
health_port="$(participant_port "$prefix" "$CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX")"
|
|
914
|
-
checks+=("
|
|
914
|
+
checks+=("wget -q --timeout=2 --tries=1 -O /dev/null http://localhost:${health_port}/health")
|
|
915
915
|
done
|
|
916
916
|
|
|
917
917
|
local joined_checks
|
|
@@ -968,7 +968,7 @@ container_health_is_ready() {
|
|
|
968
968
|
local _container="$1"
|
|
969
969
|
local _lifecycle_state="$2"
|
|
970
970
|
local health_status="$3"
|
|
971
|
-
[[ "$health_status" == "healthy" ]]
|
|
971
|
+
[[ "$lifecycle_state" == "running" && "$health_status" == "healthy" ]]
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
canton_and_extras_are_ready() {
|
|
@@ -976,7 +976,8 @@ canton_and_extras_are_ready() {
|
|
|
976
976
|
local _lifecycle_state="$2"
|
|
977
977
|
local health_status="$3"
|
|
978
978
|
local extra_participants="$4"
|
|
979
|
-
[[ "$health_status" == "healthy" ]]
|
|
979
|
+
[[ "$lifecycle_state" == "running" && "$health_status" == "healthy" ]] \
|
|
980
|
+
&& extra_participants_healthy "$extra_participants"
|
|
980
981
|
}
|
|
981
982
|
|
|
982
983
|
print_container_logs() {
|
|
@@ -1008,7 +1009,7 @@ wait_for_container_readiness() {
|
|
|
1008
1009
|
lifecycle_state="${lifecycle_state:-missing}"
|
|
1009
1010
|
health_status="${health_status:-unknown}"
|
|
1010
1011
|
|
|
1011
|
-
if [[ "$lifecycle_state" != "running" ]]; then
|
|
1012
|
+
if [[ "$lifecycle_state" != "running" && "$lifecycle_state" != "restarting" ]]; then
|
|
1012
1013
|
echo "$container is not running (state: $lifecycle_state)." >&2
|
|
1013
1014
|
print_container_logs "$container"
|
|
1014
1015
|
return 1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
RUNTIME_DIR="${PARTICIPANT_358_RUNTIME_DIR:-$REPO_ROOT/.generated/participant-358}"
|
|
7
|
+
PROJECT_NAME="${PARTICIPANT_358_PROJECT_NAME:-canton-participant-358}"
|
|
8
|
+
DOCKER_COMPOSE_CMD=()
|
|
9
|
+
|
|
10
|
+
if [[ ! -f "$RUNTIME_DIR/compose.yaml" ]]; then
|
|
11
|
+
echo "Participant 3.5.8 sidecar is not configured. Nothing to stop."
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
if docker compose version >/dev/null 2>&1; then DOCKER_COMPOSE_CMD=(docker compose); elif docker-compose version >/dev/null 2>&1; then DOCKER_COMPOSE_CMD=(docker-compose); else
|
|
15
|
+
echo "Unable to find a working Docker Compose command." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
"${DOCKER_COMPOSE_CMD[@]}" --project-name "$PROJECT_NAME" --file "$RUNTIME_DIR/compose.yaml" down --volumes --remove-orphans
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ "${PARTICIPANT_358_SMOKE_TEST:-0}" != "1" ]]; then
|
|
5
|
+
echo "Set PARTICIPANT_358_SMOKE_TEST=1 to run this Docker smoke test against an already-running CN Quickstart localnet."
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
trap 'bash "$SCRIPT_DIR/stop-local-participant-358.sh"' EXIT
|
|
12
|
+
|
|
13
|
+
bash "$SCRIPT_DIR/start-local-participant-358.sh"
|
|
14
|
+
version="$(PARTICIPANT_358_LEDGER_ENDPOINT="localhost:${PARTICIPANT_358_LEDGER_PORT:-8901}" node "$SCRIPT_DIR/participant-358-synchronizer.mjs" ledger-api-version)"
|
|
15
|
+
[[ "$version" == 3.5.8* ]] || {
|
|
16
|
+
echo "Expected Canton 3.5.8 Ledger API response, received: $version" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
}
|
|
19
|
+
echo "Participant 3.5.8 Ledger API response: $version"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
START_SCRIPT="$REPO_ROOT/node/start-local-participant-358.sh"
|
|
6
|
+
STOP_SCRIPT="$REPO_ROOT/node/stop-local-participant-358.sh"
|
|
7
|
+
|
|
8
|
+
[[ -x "$START_SCRIPT" ]]
|
|
9
|
+
[[ -x "$STOP_SCRIPT" ]]
|
|
10
|
+
grep -Fq 'export SDK_EXAMPLE_LEDGER_ENDPOINT=localhost:$LEDGER_PORT' "$START_SCRIPT"
|
|
11
|
+
grep -Fq 'export SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT=localhost:$LEDGER_PORT' "$START_SCRIPT"
|
|
12
|
+
grep -Fq 'export SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT=localhost:$ADMIN_PORT' "$START_SCRIPT"
|
|
13
|
+
|
|
14
|
+
tmpdir="$(mktemp -d)"
|
|
15
|
+
trap 'rm -rf "$tmpdir"' EXIT
|
|
16
|
+
|
|
17
|
+
stubbin="$tmpdir/bin"
|
|
18
|
+
mkdir -p "$stubbin"
|
|
19
|
+
cat > "$stubbin/docker" <<'EOF'
|
|
20
|
+
#!/usr/bin/env bash
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
printf '%s\n' "$*" >> "$DOCKER_LOG"
|
|
23
|
+
if [[ "$1" == "compose" && "$2" == "version" ]]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
EOF
|
|
27
|
+
chmod +x "$stubbin/docker"
|
|
28
|
+
|
|
29
|
+
runtime_dir="$tmpdir/runtime"
|
|
30
|
+
docker_log="$tmpdir/docker.log"
|
|
31
|
+
PATH="$stubbin:$PATH" \
|
|
32
|
+
DOCKER_LOG="$docker_log" \
|
|
33
|
+
PARTICIPANT_358_RUNTIME_DIR="$runtime_dir" \
|
|
34
|
+
PARTICIPANT_358_SKIP_SYNCHRONIZER_CONNECT=1 \
|
|
35
|
+
"$START_SCRIPT"
|
|
36
|
+
|
|
37
|
+
grep -Fqx 'name: canton-participant-358' "$runtime_dir/compose.yaml"
|
|
38
|
+
grep -Fqx ' participant358:' "$runtime_dir/compose.yaml"
|
|
39
|
+
if grep -Fqx ' canton:' "$runtime_dir/compose.yaml"; then
|
|
40
|
+
echo 'sidecar must not claim the quickstart canton DNS alias' >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
grep -Fqx ' image: ghcr.io/digital-asset/decentralized-canton-sync/docker/canton:0.6.12' "$runtime_dir/compose.yaml"
|
|
44
|
+
grep -Fqx ' image: postgres:16' "$runtime_dir/compose.yaml"
|
|
45
|
+
grep -Fqx ' - default' "$runtime_dir/compose.yaml"
|
|
46
|
+
grep -Fqx ' - localnet' "$runtime_dir/compose.yaml"
|
|
47
|
+
grep -Fqx ' databaseName = participant358' "$runtime_dir/canton.conf"
|
|
48
|
+
grep -Fqx ' admin-api {' "$runtime_dir/canton.conf"
|
|
49
|
+
grep -Fqx ' address = "0.0.0.0"' "$runtime_dir/canton.conf"
|
|
50
|
+
grep -Fqx ' port = 5002' "$runtime_dir/canton.conf"
|
|
51
|
+
grep -Fqx ' - "127.0.0.1:8901:5001"' "$runtime_dir/compose.yaml"
|
|
52
|
+
grep -Fqx ' port = 5002' "$runtime_dir/canton.conf"
|
|
53
|
+
grep -Fqx ' port = 7575' "$runtime_dir/canton.conf"
|
|
54
|
+
grep -Fq 'PARTICIPANT_358_SYNCHRONIZER_CONFIG=' "$runtime_dir/participant-358.env"
|
|
55
|
+
grep -Fq 'PARTICIPANT_358_LEDGER_TOKEN_FILE=' "$runtime_dir/participant-358.env"
|
|
56
|
+
test -s "$runtime_dir/ledger-api-user.token"
|
|
57
|
+
node -e '
|
|
58
|
+
const token = require("node:fs").readFileSync(process.argv[1], "utf8").trim();
|
|
59
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url"));
|
|
60
|
+
if (payload.sub !== "ledger-api-user") throw new Error("unexpected token subject");
|
|
61
|
+
if (payload.aud !== "https://canton.network.global") throw new Error("unexpected token audience");
|
|
62
|
+
if (!Number.isInteger(payload.iat)) throw new Error("token is missing iat");
|
|
63
|
+
if (!Number.isInteger(payload.exp) || payload.exp <= payload.iat) throw new Error("token is missing a future exp");
|
|
64
|
+
if (payload.exp - payload.iat !== 300) throw new Error("token lifetime must be 300 seconds");
|
|
65
|
+
' "$runtime_dir/ledger-api-user.token"
|
|
66
|
+
grep -F -- '--project-name canton-participant-358' "$docker_log"
|
|
67
|
+
grep -Fqx ' name: quickstart' "$runtime_dir/compose.yaml"
|
|
68
|
+
|
|
69
|
+
if PATH="$stubbin:$PATH" PARTICIPANT_358_LEDGER_PORT=3901 "$START_SCRIPT" >/dev/null 2>&1; then
|
|
70
|
+
echo 'sidecar launcher accepted a CN Quickstart Ledger port' >&2
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
PATH="$stubbin:$PATH" \
|
|
75
|
+
DOCKER_LOG="$docker_log" \
|
|
76
|
+
PARTICIPANT_358_RUNTIME_DIR="$runtime_dir" \
|
|
77
|
+
"$STOP_SCRIPT"
|
|
78
|
+
|
|
79
|
+
grep -F -- '--project-name canton-participant-358' "$docker_log"
|
|
80
|
+
if grep -Eq -- '(^| )down( |$).*quickstart|quickstart.*(^| )down( |$)' "$docker_log"; then
|
|
81
|
+
echo 'stop launcher targeted quickstart' >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|