Commit 9676f68
authored
feat: compact-deployer tool (#86)
* feat(deployer): add compact-deployer tool and CLI deploy command
Introduce the `@openzeppelin/compact-deployer` package: a programmatic
and CLI deployer for Compact contracts. Covers config loading, artifact
and constructor-arg resolution, wallet/keystore handling, network and
proof-server providers, and deployment-record bookkeeping.
Wire the deployer into `@openzeppelin/compact-cli` via the new
`compact-deploy` command (runDeploy), with passphrase prompting and
JSON/verbose output modes.
* Add root resolutions for the midnight stack and a `coverage` turbo
task so the new package can report coverage.
Part 1 of 2 splitting the original deployer PR. Examples and the
integration-test suite follow in a stacked PR that builds on this one.
Refs: #86
* fix(deployer): address CodeRabbit review feedback
Harden the deployer tool and CLI against the issues raised in the #86
review. Each fix verified against current code and covered by tests.
* cli/logger: route `--json` pino logs to STDERR (fd 2) so STDOUT
carries only the single result object.
* cli/prompt: write the passphrase prompt + newline to STDERR, and
settle the promise on stdin `end`/`error` so non-interactive input
(closed/newline-less pipe) can no longer hang the CLI.
* config/schema: mark the file/module ref union members `.strict()`
so an ambiguous `{ file, module }` is rejected instead of silently
dropping a key.
* deployments: write the ledger atomically (temp file + rename) so a
crash mid-write can't truncate `*.json`.
* loaders/artifact: bind compiled assets to `dirname(entry)` rather
than a hardcoded `contract/`, fixing the top-level-`index.js` case.
* loaders/constructor-meta: allow an empty named-args object for a
no-arg constructor (resolves to `[]`); `reorderNamedArgs` still
rejects unexpected keys.
* loaders/contract-resolve: use `path.isAbsolute` instead of
`startsWith('/')` for OS-correct absolute-path detection.
* providers/network: drop `mainnet` from the allow-list while the
deployer is testnet/preview-only.
* providers/proof-server: reject partial-numeric and out-of-range
`PROOF_SERVER_PORT` values (full `\d+` match + 1..65535 bounds).
* wallet/handler: use `path.dirname`/`basename` instead of manual
`/` splitting so cache paths work on Windows.
* wallet/keystore: validate shape in `fromJSON` and throw `WalletError`
(not a raw `TypeError`) on malformed keystore JSON.
Refs: #86
* fix(deployer): unblock preprod wallet sync
preprod's dust history is a ~1M-event global stream that every client
replays on sync. Deploying any contract there failed before a tx was
ever built: the dust wallet OOMed mid-replay, and once that was worked
around the sync gate never completed. Both trace to the pre-fix
wallet-sdk-dust-wallet@4.0.0 pin (midnightntwrk/midnight-wallet#425).
* wallet/handler: set batchUpdates = { size: 5000, timeout: 1,
spacing: 4 } on the shared wallet config so the fresh-sync and the
cache-restore paths (shielded + dust alike) stream the replay in
larger chunks instead of exhausting the V8 heap at the SDK default
batch size of 10.
* deployer: replace the strict FacadeState.isSynced tip gate with
isCompleteWithin(50) on each sub-wallet. On a live chain the dust
stream advances continuously, so strict completion never fires and
the gate timed out on a fully usable wallet. The gate still waits on
all three sub-wallets to avoid the custom-error-170 regression.
* cli + README: document the NODE_OPTIONS max-old-space-size bump
still useful for a first preprod sync, plus the tolerant tip gate.
Verified on preprod: ShieldedFungibleToken dry-run synced to tip in
~1m12s from a warm cache (dust ~1.08M events, no OOM) and the gate
completed even though dust never reached strict completion.
Closes #115
* feat(deployer): make sync batch size configurable
The dust/shielded sync batch size was hard-coded to 5000 (the #115 OOM
workaround). Expose it as a knob so operators can trade memory against
replay speed without recompiling, while keeping 5000 as the default.
* wallet/handler: add `syncBatchSize` to WalletHandlerBuildOptions;
fall back to DEFAULT_SYNC_BATCH_SIZE (5000). timeout/spacing stay at
the validated values.
* deployer: thread `syncBatchSize` from DeployerOptions through to the
owned-wallet build (ignored when a wallet is injected).
* cli: add `--sync-batch-size <n>` (positive-integer validated, mirrors
--sync-timeout) and document it in --help and the README.
Refs: #115
* feat(deployer): allow sync knobs in compact.toml
The sync batch size and sync timeout were CLI/programmatic only, so an
operator had to pass `--sync-batch-size` / `--sync-timeout` on every run
for a long-history network like preprod. Let them live in config too.
* config/schema: add optional `sync_timeout` (seconds) and
`sync_batch_size` to `[networks.X]`, both positive integers.
* deployer: resolve each with precedence CLI/programmatic option >
TOML network value > built-in default (10 min / 5000). Applies only
to the owned-wallet build; ignored when a wallet is injected.
* README: document the keys, the precedence, and a preprod example.
Refs: #115
* docs(deployer): document npx + local-install for the CLI
Add an "Install & run" section: the `compact-deploy` bin ships in
@openzeppelin/compact-cli, and `npx compact-deploy` resolves the local
install so the deployer and the compiled contracts share one
compact-runtime copy (required for a real deploy). Note that fully
ephemeral `npx @openzeppelin/compact-cli …` is fine for help/dry-run but
not for real deploys, since its cache-tree runtime differs from the
project's and the submit fails the ContractMaintenanceAuthority check.
Refs: #115
* fix(deployer): apply review fixes and drop the barrel
Secret handling, dead dependency pruning, keystore hardening, and the
--version flag, from the review of PR #86.
Replaces src/index.ts with explicit package.json subpath exports. Each
module is now reached by its own path, so the noBarrelFile suppression
is gone and consumers no longer pull the whole package to import one
symbol. Internals stay unexported.
* refactor(deployer): split services out and cache the unshielded wallet
Extracts five single-responsibility modules under src/services/:
file-lock, atomic-json, wallet-sync, wallet-cache, and deploy-tx. They
stay internal, with no package.json subpath, so the public surface is
unchanged. deployments.ts, deployer.ts, and wallet/handler.ts shed the
locking, syncing, persistence, and tx-submission code they had absorbed.
Caches the unshielded sub-wallet alongside shielded and dust, with a
matching --seed-cache-from-unshielded flag. The sync gate already waited
on unshielded progress, so an uncached one re-synced from genesis and
dominated every warm boot.
Narrows two swallowed errors in file-lock. A stale-break fault that is
not ENOENT now surfaces instead of retrying into a misleading lock
timeout, and a failed release warns rather than vanishing. Release still
never throws: it runs in the caller's finally, where throwing would mask
a real error or fail an already-successful deploy.
Coverage thresholds move to 100 in all four metrics, which the package
now meets.
* fix(deployer): address CodeRabbit review on PR 86
Catch a rejected onCheckpoint in syncAndVerifyFunds. The callback ran as
`onCheckpoint().finally(...)`, so a rejection was never consumed and Node
would terminate the process, contradicting the documented best-effort
contract of the surrounding comment.
Reject a keystore dklen other than 32 in encrypt. The value reached
scryptSync directly, and a shorter derived key left an empty MAC key,
so encrypt would write a keystore that its own schema then refuses to
read back.
Fix the default_network test fixture. It appended a second [profile]
table, which smol-toml rejects as a redefinition, so the test passed on
the TOML parse error and never reached the validation branch it names.
Covering that branch properly left the invalid-TOML path untested, so it
gets its own case.
Document compact-deploy in the CLI README, which listed only the compiler
and builder binaries while requiring Node 24 for a deployer it never
mentioned. Correct the deployer README's real-network guidance, which
pointed at preview while its own known-issues section records preview as
null-routed.
* build(deployer): move to the ledger-v8 8.1 stack
ledger-v8 8.1.2, midnight-js and testkit-js 4.1.1, wallet-sdk-facade
4.0.1 with dust-wallet 4.1.0, shielded 3.0.1, unshielded 3.1.0 and
address-format 3.1.2. compact-runtime stays on 0.16.0. compact-js stays
on 2.5.1: 2.5.3 depends on an unpublished ledger-v9 alpha.
The five wallet-sdk root resolutions are load-bearing. testkit-js pulls
wallet-sdk 1.1.0, whose ^4.0.1 facade range resolves to 4.1.0, and that
version imports a nonexistent Clock export and fails to load. No
resolution is set for midnight-js-protocol: the 4.1.1 packages pin it
exactly, and a global pin would downgrade the simulator's 5.0.0-beta.7
tree.
* fix(deployer): keep state under the config dir
The wallet-state cache and the LevelDB private-state store resolved
against process.cwd(), so a deploy run from any other directory
re-synced from genesis and started from an empty private state. Both
now hang off the compact.toml directory. rootDir is a required option
on WalletHandler, WalletCache and buildProviders so a silent CWD
default cannot come back. --seed-cache-from-* paths stay CWD-relative
because the user typed them at the shell.
Wallet-SDK rejections are effect-style tagged records, not Errors, so
the warn sites logged "[object Object]". formatError renders _tag and
message instead.
README: record the 2026-09-04 preprod dry run on this stack (synced to
tip, the old deserialize failure did not reproduce) and why the
wallet-sdk resolutions must survive dependency bumps.
* fix(deployer): format tagged SDK errors on exit paths
formatError was wired into the warn-and-continue sites only. A tagged
wallet-SDK rejection escaping the sync or the deploy call still hit
`(e as Error).message` and printed "[DEPLOY] undefined". The CLI catch,
the deploy wrapper and the proof-server dispose now go through it, so
the module gains a package export for the CLI.
The fallbacks also lose less: an Error appends its cause chain, and
util.inspect replaces the hand-rolled JSON serializer, rendering bigint
and marking circular references instead of degrading to
"[object Object]".
* refactor(deployer): drop SDK casts, dedupe rootDir doc
The restore casts in wallet-cache were redundant: the wallet-SDK factory
and DustWallet.restore already return types assignable to
ShieldedWalletAPI and DustWalletAPI. describeProgress cast its input to
a loose shape; it now takes a union of the two sub-wallet progress
shapes, mirrored structurally because neither SDK type reaches a package
root the deployer depends on. Test fixtures gained the counters the
real SDK always populates, so the `?? 0n` props are gone.
The rootDir explanation lived on three option types; it now lives once
on CompactConfig.rootDir with one-line pointers elsewhere.
* docs(deployer): state the LevelDB lock scope
Both the wallet cache and the LevelDB store now hang off the
compact.toml directory, so the project is the unit of state and a
second concurrent compact-deploy in the same project fails on the
LevelDB lock.
* build(deployer): drop unused axios and testcontainers
Neither is imported anywhere under packages/deployer/src. The only
container use goes through @midnight-ntwrk/testkit-js, which brings its
own testcontainers 12; the direct ^10 pin added a second copy with a
second dockerode major.
* ci(release): wire compact-deployer into the release flow
compact-cli depends on @openzeppelin/compact-deployer via workspace:^,
which yarn rewrites to a concrete version at pack time, but neither
release workflow knew the package, so a cli release after merge would
point at an npm version nothing can publish. The deployer joins the
workflow choice lists, gets its repository field, and moves ahead of
the cli in the first-release order.
RELEASING.md requires a CHANGELOG.md per published package. Both new
files start with an Unreleased section; the cli entry records the Node
24 floor as Breaking, since compact-deploy relies on explicit resource
management (await using, AsyncDisposableStack) that Node 22 lacks and
the bump lands on compact-compiler and compact-builder users too. The
changelogs are added to the files arrays so they ship in the tarballs.
* docs(deployer): state the supported Midnight stack
The deployer pins compact-runtime 0.16.0 and ledger-v8, and needs
artifacts from compactc 0.31.1; the default 0.34 compiler targets
runtime 0.19.0 and the deploy fails with a version mismatch. The
README now says so up front, notes that compact-contracts main (runtime
0.19.0, ledger-v9) is not deployable with this tool yet, and names the
blocker: compact-js 2.5.3 depends on an unpublished ledger-v9 alpha.
The root README gains the deployer package and corrects the Node
requirements per package.
* docs(deployer): library callers must inject the private-state provider
The default LevelDB store is opened per deploy and holds its lock until
the process exits, so a script that deploys several contracts in one
process needs privateStateProvider next to walletProvider.
* chore: gitignore compact-deploy verbose logs
* fix(deployer): resolve --seed-file against the CWD
--config and --seed-cache-from-* already resolve against the shell CWD;
--seed-file alone resolved against the compact.toml directory, so a
relative path typed from a subdirectory read the wrong file. A relative
[wallet].keystore keeps the rootDir anchor because it comes from the
config file, not the shell.
* fix(deployer): reject scrypt params the KDF itself refuses
The schema bounded n and r separately, but scryptSync also enforces a
memory budget and an n < 2^(16r) ceiling, so a keystore with n=2^20 r=8
passed validation and then surfaced as a raw OpenSSL RangeError. One
predicate now models both limits and runs in the schema and in encrypt.
fromJSON also lost its hand-written pre-checks: they restated what the
zod schema already enforces, and the schema error names the field.
* fix(deployer): type the wallet sync timeout as WalletError
A bare Error exited 1; errors.ts promises a stable code per failure
mode, and a sync timeout is a wallet failure (exit 3).
* fix(deployer): break stale locks without a double acquire
Two waiters that both saw a stale lock could each unlink it: the first
re-created the lock, the second removed that fresh file and acquired
alongside the holder. The break now renames the lock aside (atomic, one
winner), re-checks the parked file's age, and restores it if a waiter
had already taken over. The lock timeout is a DeployError instead of a
bare Error.
* refactor(deployer): drop the unreachable mainnet fee branch
applyNetwork rejects mainnet, so the additionalFeeOverhead fallback to
the testkit default could never run.
* fix(deployer): load args and init state before the wallet sync
Both depend only on the config and the artifact, and a typo in either
used to surface after a 30-60 min first preprod sync.
* fix(deployer): format tagged SDK rejections in runDeploy
The library entrypoint still rendered a wallet-SDK tagged record as
"[object Object]"; 298def0 fixed the CLI arm only.
* fix(deployer): record the deploy before waiting for finalization
deployContract fuses submission and the wait for finalization and yields
no identifier until the tx has landed, so a WebSocket drop or indexer
stall left the CLI spinning with nothing to reconcile, and any failure
in that window reported a deploy that may already be on chain as
failed. The three SDK steps now run explicitly: createUnprovenDeployTx,
submitTxAsync, then watchForTxData raced against --tx-timeout (default
600 s), with the private-state and signing-key writes only after a
SucceedEntirely status.
The deployments ledger gains a pending state. A record with the address
and txId is written as soon as the node accepts the tx and promoted to
confirmed on finalization. A timeout or rejection leaves it pending and
names both identifiers in the error; the next deploy of that contract
refuses until --force, and the check runs before proving so a blocked
deploy costs no fees.
Every ledger write is logged at info first and a failing write (lock
timeout, EACCES, malformed <network>.json) rethrows as
DeploymentsFileError, exit 6, carrying address, txId and txHash.
readJson wraps a SyntaxError with the file path instead of leaking it.
IndexerUnreachableError and ProofServerUnreachableError had no call
site and are gone; exit code 4 is unused.
* test(deployer): cover the untagged-record message fallback
* build(cli): drop the ws WebSocket shim
Node 24 ships a native global WebSocket and midnight-js reads that
global rather than importing ws. Verified on a local node: with the
native class pinned, sync, deploy and watchForTxData all completed;
with WebSocket forced to undefined the sync never connected. The shim
and the ws / @types/ws dependencies go; ws stays in the lockfile only
as a transitive dependency of the indexer provider and testkit-js.
* fix(deployer): silence the testkit-js module logger
testkit-js builds a pino logger at import and pretty-prints through it
to stdout, ignoring the logger passed into every call, so
`compact-deploy --json | jq` failed on an "Initializing wallet builder"
line that landed before the result object. The logger is exported;
setting its level to silent at module load stops the line. The empty
`<cwd>/logs/tests/` file the same logger creates on import cannot be
prevented from user code and needs an upstream change.
* fix(deployer): surface the cause behind Effect failures
With the proof server down the CLI printed only "Failed to prove
transaction". The wallet SDK does attach the connection error as the
cause, but Effect.runPromise rejects with a FiberFailure that copies the
message and leaves `cause` unset; the real failure hangs off the
registered symbol effect/Runtime/FiberFailure/Cause. formatError now
unwraps that symbol (no runtime dependency on effect) and collapses a
wrapper whose cause repeats its message, so the line reads
"Failed to prove transaction: Failed to connect to Proof Server:
Transport error (POST http://127.0.0.1:6300/prove): fetch failed".1 parent fc2a152 commit 9676f68
83 files changed
Lines changed: 18092 additions & 735 deletions
File tree
- .github/workflows
- packages
- cli
- src
- test
- deployer
- src
- config
- loaders
- providers
- services
- wallet
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
67 | 68 | | |
68 | 69 | | |
69 | 70 | | |
| 71 | + | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
| |||
85 | 87 | | |
86 | 88 | | |
87 | 89 | | |
| 90 | + | |
88 | 91 | | |
89 | 92 | | |
90 | 93 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
85 | 86 | | |
86 | 87 | | |
87 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
88 | 92 | | |
89 | 93 | | |
90 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
14 | 21 | | |
15 | 22 | | |
16 | 23 | | |
| |||
22 | 29 | | |
23 | 30 | | |
24 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
25 | 35 | | |
26 | 36 | | |
27 | 37 | | |
28 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
29 | 64 | | |
30 | 65 | | |
31 | 66 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
39 | 43 | | |
40 | 44 | | |
41 | | - | |
42 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
43 | 48 | | |
44 | 49 | | |
45 | 50 | | |
46 | 51 | | |
| 52 | + | |
47 | 53 | | |
48 | 54 | | |
49 | 55 | | |
| |||
53 | 59 | | |
54 | 60 | | |
55 | 61 | | |
56 | | - | |
| 62 | + | |
57 | 63 | | |
58 | 64 | | |
59 | 65 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
| 88 | + | |
88 | 89 | | |
| 90 | + | |
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
92 | | - | |
| 94 | + | |
93 | 95 | | |
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
97 | 99 | | |
98 | | - | |
99 | | - | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
100 | 103 | | |
101 | | - | |
| 104 | + | |
102 | 105 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| 21 | + | |
20 | 22 | | |
| 23 | + | |
| 24 | + | |
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
25 | 40 | | |
26 | 41 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| 21 | + | |
19 | 22 | | |
20 | 23 | | |
21 | 24 | | |
| |||
34 | 37 | | |
35 | 38 | | |
36 | 39 | | |
37 | | - | |
38 | | - | |
39 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
40 | 54 | | |
41 | 55 | | |
42 | 56 | | |
| |||
55 | 69 | | |
56 | 70 | | |
57 | 71 | | |
58 | | - | |
| 72 | + | |
59 | 73 | | |
60 | 74 | | |
61 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
22 | | - | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
| 29 | + | |
27 | 30 | | |
28 | 31 | | |
29 | 32 | | |
30 | | - | |
| 33 | + | |
31 | 34 | | |
32 | 35 | | |
33 | 36 | | |
34 | | - | |
| 37 | + | |
| 38 | + | |
35 | 39 | | |
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
39 | 43 | | |
| 44 | + | |
40 | 45 | | |
41 | 46 | | |
42 | 47 | | |
| |||
47 | 52 | | |
48 | 53 | | |
49 | 54 | | |
| 55 | + | |
50 | 56 | | |
51 | | - | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
52 | 60 | | |
53 | 61 | | |
0 commit comments