Skip to content

feat(Daemon-SQS): publish Atlas SWAP events for native peg-in and peg-out - #481

Open
ronaldsg20 wants to merge 5 commits into
mainfrom
feat/sqs-events
Open

feat(Daemon-SQS): publish Atlas SWAP events for native peg-in and peg-out#481
ronaldsg20 wants to merge 5 commits into
mainfrom
feat/sqs-events

Conversation

@ronaldsg20

@ronaldsg20 ronaldsg20 commented Aug 27, 2026

Copy link
Copy Markdown
Member

📌 Summary

The daemon now publishes Atlas SWAP Event Schema v1.0 events to an SQS FIFO queue as it processes Bridge transactions, making native peg-in and peg-out volume, duration and rejection reasons visible to the analytics side. The feature ships dark behind the ATLAS_EVENTS_ENABLED kill switch.

🔍 Description

What was changed

Event model (src/models/atlas/)

  • atlas-event.model.ts: envelope and the four in-scope payload types (swap.created, swap.pending, swap.completed, swap.rejected). expired, refund_pending, refunded, claim_pending and claimed are out of scope for native pegs.
  • atlas-chain.ts: qualified chain ids (rootstock_mainnet / bitcoin_testnet / …) derived from NETWORK, with assertNetworkConfigured() deliberately not defaulting to testnet.
  • atlas-identifiers.ts: swap_id / wallet_address normalization (0x-prefixed, lowercase) so one transaction cannot reach Atlas under two spellings; Bitcoin addresses are left untouched because base58/bech32 is case sensitive.
  • atlas-amount.ts: big.js-backed satoshi → fixed 8-decimal string formatting, so large values never lose precision through Number arithmetic.
  • atlas-pegin-reasons.ts: translation tables for rskj's RejectedPeginReason and NonRefundablePeginReason (verified against rsksmart/rskj@161c3f1), plus error_category classification.

Publishing (src/services/atlas/)

  • AtlasEventPublisher interface plus two implementations: SqsAtlasEventPublisher (FIFO queue, MessageGroupId = swap_id, MessageDeduplicationId = event_id) and NoopAtlasEventPublisher, bound while the kill switch is off so no SQS client is ever built.
  • AtlasEventMetrics: per status/flow/event-type counters, one log line per publication carrying metric: 'atlas_events_published_total'. That field name is the contract with the log aggregator and is pinned by a test.
  • PegoutAtlasEventBuilder: one event per peg-out transition — RECEIVEDswap.created, WAITING_FOR_CONFIRMATIONswap.pending, RELEASE_BTCswap.completed (with duration_ms measured from the RECEIVED status), REJECTEDswap.rejected. WAITING_FOR_SIGNATURE publishes nothing.
  • PeginAtlasEventBuilder: two events per outcome, keyed by btcTxIdLOCKEDswap.created + swap.completed, each rejection → swap.created + swap.rejected. Amount and addresses are read from the Bridge logs via extractContext(), because PeginStatusDataModel persists neither.

Wiring

  • New DependencyInjectionHandler.configureDaemonDependencies(), called only by DaemonRunner: the publisher and both transaction processors are registered there and are simply absent from the API process — a controller cannot inject what was never bound.
  • DaemonRunner calls assertNetworkConfigured() on start, so a daemon with a missing or bogus NETWORK aborts instead of mislabelling every event.
  • Dockerfile gains ENV APP_MODE=ALL and CMD node . --appmode=$APP_MODE, so the orchestrator selects API or DAEMON explicitly rather than relying on an omitted flag.

Peg-out processing fixes found while wiring emission

  • btcTxHash now uses parsedBtcTransaction.getId() (canonical big-endian txid) instead of getHash() (internal little-endian hash), which no explorer or Bitcoin node resolves. btcTxHash was also missing from the Mongo PegoutStatusSchema and is now declared, so the value is actually persisted.
  • release_btc outputs whose script is not an addressable recipient (federation change) no longer throw out of the loop; they are skipped.
  • When a batch pays the same Bitcoin address more than once, the new selectPegoutForOutput() breaks the tie by batchPegoutIndex (compared numerically — the model types it as a number while Mongo stores a String). Previously both candidate rows were skipped and peg-outs that really were paid on Bitcoin never left WAITING_FOR_SIGNATURE.
  • A peg-in the Bridge rejected with no refund branch at all (no release_requested, no unrefundable_pegin) is now recorded as REJECTED_NO_REFUND instead of being dropped, which used to leave the user with no status.

Why it was changed

Atlas is the analytics side of the swap pipeline and had no visibility into native pegs: only Flyover quotes were reachable. Emitting the same schema for powpeg peg-in and peg-out puts both flows in one dataset, which is what makes peg volume, end-to-end duration and rejection-reason breakdowns answerable.

Technical root causes addressed

  • Two enums in one position. rejected_pegin and unrefundable_pegin carry different rskj enums in the same log position: reason 3 means LEGACY_PEGIN_UNDETERMINED_SENDER in the first and INVALID_AMOUNT in the second. atlas-pegin-reasons.ts branches on the event name first and the number second; unknown values degrade to UNKNOWN with a warn rather than silently changing meaning.
  • Amount units differ per flow. Peg-out Bridge logs report weis, while pegin_btc / lock_btc / release_requested report satoshis. Running peg-in amounts through the peg-out fromWeiNumberToSatoshiNumber helper divides by 1e10 and collapses every realistic peg-in to zero, so the peg-in path formats satoshis directly.
  • Little-endian txid. getHash() returns the internal hash; the value stored in btcTxHash and shipped as destination_tx_hash has to be the canonical txid.

Scope of impact

  • Daemon only. The REST API process binds no publisher and its behaviour is unchanged.
  • Off by default. Only the literal ATLAS_EVENTS_ENABLED=true enables publication; a typo fails closed.
  • Publication happens after the status is written to Mongo and never fails the caller: a transport failure is logged at error level, counted as a loss, and block processing continues.
  • Peg-out status persistence changes behaviour even with the kill switch off: the corrected btcTxHash, the new schema field, the federation-change skip and the batch disambiguation apply unconditionally.

Supporting artifacts

  • schemas/atlas-swap-event.schema.json — executable draft-07 contract for the v1.0 envelope and all four payloads, asserted against every message in the integration suite.
  • docker-compose.yml + ci/localstack-init/01-create-queue.sh — LocalStack SQS for local development.
  • ci/create-atlas-queue.js — queue creation for CI, where LocalStack service containers do not run init/ready.d hooks.
  • New integration-tests job in .github/workflows/build.yml, and a separate npm run integration-test script kept out of test:all so the fast unit cycle stays fast.
  • ENV_VARIABLES.md and README.md document every new variable, what each event carries, and the known event-loss window.

🧪 How to Test

1. Unit suite (no infrastructure needed)

npm ci
npm run unit-test
npm run eslint

Expect the full suite green. New coverage: the two builders, the SQS publisher, the metrics counter, the chain/identifier/reason models, the DI handler (that the API does not bind services.AtlasEventPublisher and the daemon does), and the peg-in / peg-out processors.

2. Integration suite against LocalStack

docker-compose up -d localstack
npm run integration-test

This publishes real messages to a real FIFO queue and asserts, for every message, that it validates against schemas/atlas-swap-event.schema.json. It covers: peg-out created → pending → completed arriving in order, a distinct message group per peg-out, event_id deduplication inside the FIFO window, a rejected peg-out as a single message, and the four peg-in paths (locked, refundable rejection, unrefundable rejection, no-refund-branch rejection).

3. Kill switch (negative path)

  • With ATLAS_EVENTS_ENABLED unset, false, or True/TRUE/1, start the daemon and confirm NoopAtlasEventPublisher is bound: no SQS client is constructed, nothing reaches the queue, and the metrics counters stay at zero.
  • With ATLAS_EVENTS_ENABLED=true but ATLAS_SQS_QUEUE_URL pointing nowhere, confirm the daemon keeps processing blocks, logs Could not publish the Atlas event at error level, and emits a status: 'failure' metric line.

4. NETWORK validation (negative path)

Start the daemon with NETWORK unset, and again with NETWORK=regtest. Both must abort at startup with the explicit message rather than booting and labelling events as testnet.

5. Process isolation

Start the API alone (--appmode=API) and confirm it boots with no Atlas binding and serves /explorer as before. Start the daemon alone (--appmode=DAEMON) and confirm it publishes. Confirm the default APP_MODE=ALL in the image keeps today's behaviour.

6. End-to-end on testnet

Point the daemon at a testnet node with the switch on, drain the queue, and verify against the explorer:

  • A successful peg-in yields swap.created then swap.completed, same swap_id (btcTxId, 0x-prefixed lowercase), fee: "0.00000000", duration_ms: null, and a destination_tx_hash that resolves to the Rootstock transaction crediting the RBTC.
  • A peg-out yields created → pending → completed with destination_tx_hash resolving on a Bitcoin explorer (this is the getId() fix) and input_amount − output_amount equal to the reported fee.
  • A batch peg-out paying one address twice produces two distinct swaps, each with the output value that matches its batchPegoutIndex.

7. Metrics

Confirm each publication logs exactly one line with metric: 'atlas_events_published_total', status, flow, eventType and an incrementing total, and that the field name is unchanged — an alert on lost events queries it.

  • I have tested this locally
  • Unit / integration tests added or updated
  • Manual QA performed

Verified in this branch: npm run unit-test387 passing, 0 failing; npm run integration-test against LocalStack → 8 passing; npm run eslint0 errors (204 pre-existing warnings). Items 3–7 above are the remaining manual QA.

🧠 Known Issues / Limitations

  • Events lost on transport failure are not recovered. The publisher swallows SQS errors by design so a persisted peg transition is never rolled back for analytics. The metrics counter makes the loss visible; recovering it needs an outbox or at least an atlasPublishedAt flag on the status. Idempotency by btcTxId means a re-sync will not retry a peg-in it already recorded.
  • atlas-pegin-reasons.ts must stay aligned with rskj. A reason added to RejectedPeginReason / NonRefundablePeginReason upstream lands here as UNKNOWN with a warn — safe, but only actionable if someone reads the warning.
  • input_amount_usd / output_amount_usd / wallet_type / quote_id always travel null. No price oracle or wallet metadata is available in the daemon.
  • Peg-in swap.pending is never emitted and peg-in duration_ms is always null: the daemon observes Rootstock only and never sees the deposit on Bitcoin. A zero would drag the average swap duration down instead of leaving it unmeasured.
  • An unrefundable peg-in reports input_amount: "0.00000000" — neither rejected_pegin nor unrefundable_pegin carries an amount and there is no other transaction to read it from.
  • Peg-out WAITING_FOR_SIGNATURE has no event, so a peg-out sits in swap.pending for the whole federation signing window.
  • The queue must have content-based deduplication disabled, since MessageDeduplicationId is the event_id. A queue provisioned with it enabled would drop legitimate transitions.
  • The metrics counter is in-memory and resets with the process; it is only meaningful through the logged totals.
  • integration-test is intentionally excluded from test:all and runs in its own CI job, so a local test:all does not exercise the SQS path.

📎 Related task

📸 Screenshots / Logs (if applicable)

Metric line emitted per publication:

{"level":"info","name":"atlasEventMetrics","metric":"atlas_events_published_total",
 "status":"success","flow":"pegout","eventType":"swap.created","total":1,
 "message":"Atlas event published"}

Integration suite against LocalStack:

✔ delivers created, pending and completed of one peg-out in order
✔ uses a distinct message group per peg-out
✔ deduplicates a re-sent event_id inside the FIFO deduplication window
✔ delivers a rejected peg-out as a single message
✔ delivers created then completed for a locked peg-in, in order
✔ delivers created before rejected for a refundable peg-in, with the amount
✔ delivers a rejection the Bridge left with no refund branch
✔ delivers created before rejected for a rejected peg-in

8 passing (38s)

Copilot AI lite review requested due to automatic review settings August 27, 2026 22:19
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
npm/@aws-sdk/client-sqs 3.1119.0 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/core 3.977.9 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-env 3.972.70 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-http 3.972.72 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-ini 3.973.15 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-login 3.972.77 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-node 3.972.81 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-process 3.972.70 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-sso 3.973.14 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/credential-provider-web-identity 3.972.76 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/middleware-sdk-sqs 3.972.42 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/nested-clients 3.997.44 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/signature-v4-multi-region 3.996.46 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/token-providers 3.1116.0 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/types 3.974.5 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/xml-builder 3.972.40 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws/lambda-invoke-store 0.3.0 UnknownUnknown
npm/@smithy/core 3.33.3 UnknownUnknown
npm/@smithy/credential-provider-imds 4.5.2 UnknownUnknown
npm/@smithy/fetch-http-handler 5.7.2 UnknownUnknown
npm/@smithy/node-http-handler 4.11.3 UnknownUnknown
npm/@smithy/signature-v4 5.7.3 UnknownUnknown
npm/@smithy/types 4.17.2 UnknownUnknown
npm/bowser 2.14.1 UnknownUnknown
npm/ajv ^8.20.0 UnknownUnknown
npm/ajv-formats ^3.0.1 UnknownUnknown

Scanned Files

  • package-lock.json
  • package.json

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “Atlas SWAP event” publishing pipeline to the daemon so each native peg-out transition emits a schema-validated event to an SQS FIFO queue for analytics (volume, duration, rejection reasons).

Changes:

  • Introduces Atlas event schema/models plus a PegoutAtlasEventBuilder that maps peg-out statuses to swap.created|pending|completed|rejected.
  • Adds an AtlasEventPublisher abstraction with SQS + noop implementations behind an ATLAS_EVENTS_ENABLED kill switch.
  • Wires publishing into PegoutDataProcessor, adds unit/integration tests, and provisions LocalStack + CI job for integration testing.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/services/pegout-data.processor.ts Publishes an Atlas event after persisting each relevant peg-out transition.
src/services/atlas/sqs-atlas-event-publisher.ts Implements FIFO SQS publishing (MessageGroupId=swap_id, DeduplicationId=event_id).
src/services/atlas/pegout-atlas-event.builder.ts Builds Atlas SWAP events (envelope + data) from persisted peg-out statuses.
src/services/atlas/noop-atlas-event-publisher.ts No-op publisher used when Atlas events are disabled.
src/services/atlas/atlas-event-publisher.ts Defines publisher interface and the ATLAS_EVENTS_ENABLED flag reader.
src/models/atlas/atlas-event.model.ts Defines Atlas event types, envelope, and payload data shapes.
src/models/atlas/atlas-chain.ts Resolves chain IDs from NETWORK and validates configuration.
src/dependency-injection-handler.ts Binds the publisher implementation based on the kill switch.
src/dependency-injection-bindings.ts Adds DI bindings for Atlas events.
src/daemon-runner.ts Validates NETWORK during daemon startup.
src/tests/unit/services/pegout-data.processor.unit.ts Adds coverage ensuring events publish after persistence and don’t break peg-out processing.
src/tests/unit/services/daemon.service.unit.ts Updates daemon tests to construct PegoutDataProcessor with an Atlas publisher.
src/tests/unit/services/atlas/sqs-atlas-event-publisher.unit.ts Unit-tests SQS publishing behavior + DI flag binding.
src/tests/unit/services/atlas/pegout-atlas-event.builder.unit.ts Validates builder output against the JSON schema (AJV).
src/tests/unit/models/atlas/atlas-chain.unit.ts Tests chain-id resolution and NETWORK validation behavior.
src/tests/unit/daemon-runner.unit.ts Tests daemon startup behavior when NETWORK is missing/invalid.
src/tests/integration/atlas-pegout-events.integration.ts Integration test that publishes to LocalStack SQS and validates ordering/dedup.
schemas/atlas-swap-event.schema.json JSON Schema contract for Atlas SWAP events v1.0.
README.md Documents integration test command + LocalStack setup for Atlas events.
ENV_VARIABLES.md Documents new Atlas env vars and event semantics.
docker-compose.yml Adds LocalStack service for local SQS and updates service dependencies.
ci/localstack-init/01-create-queue.sh LocalStack init hook to create the FIFO queue.
ci/create-atlas-queue.js GitHub Actions helper to create the queue for service containers.
.github/workflows/build.yml Adds a dedicated integration-test job with LocalStack.
.eslintignore Ignores ci/ from eslint.
.env.test Adds Atlas env vars for test environments.
package.json Adds AWS SQS client dependency and AJV dev deps; adds integration-test script.
package-lock.json Locks new dependencies.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docker-compose.yml Outdated
Comment thread src/services/atlas/sqs-atlas-event-publisher.ts
Comment thread src/daemon-runner.ts
Copilot AI review requested due to automatic review settings September 1, 2026 14:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 48 changed files in this pull request and generated 5 comments.

Suppressed comments (1)

src/daemon-runner.ts:12

  • DaemonRunner now refuses to start unless NETWORK is set to mainnet/testnet, even when Atlas event publication is disabled. If the stricter validation is only required to prevent mislabeling Atlas events, consider enforcing it only when ATLAS_EVENTS_ENABLED=true to avoid breaking existing daemon deployments that relied on the prior defaulting behavior elsewhere in the codebase.
    assertNetworkConfigured();

Comment thread tools/atlas-replay/dump-bridge-events.js Outdated
Comment thread src/__tests__/integration/atlas-pegout-events.integration.ts Outdated
Comment thread ENV_VARIABLES.md Outdated
Comment thread schemas/atlas-swap-event.schema.json Outdated
Comment thread src/services/pegin-data.processor.ts
Copilot AI review requested due to automatic review settings September 1, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 39 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/tests/integration/atlas-pegout-events.integration.ts:249

  • The peg-in integration test passes a context field named amountInWeis, but PeginAtlasEventBuilder reads amountInSatoshis. As written, this test will publish a swap.created with a zero amount, reducing the value of the integration coverage.
    const context = {
      amountInWeis: '500000000000000000',
      rskRecipient: '0x2D623170Cb518434af6c02602334610f194818c1',
    };

schemas/atlas-swap-event.schema.json:5

  • The JSON schema description says it is emitted "for native peg-out", but the schema and this PR cover both peg-in and peg-out events. This can mislead consumers/readers of the schema.
  "description": "Executable contract for the Atlas SWAP Event Schema v1.0 as emitted by the 2wp-api daemon for native peg-out.",

src/services/pegin-data.processor.ts:14

  • The PR title/description focus on native peg-out, but this change introduces Atlas event publication for peg-ins as well (new PeginAtlasEventBuilder usage and publisher injection). Either update the PR metadata to reflect the broader scope, or drop peg-in publication if it’s out of scope for this change.
    src/services/atlas/sqs-atlas-event-publisher.ts:29
  • When ATLAS_EVENTS_ENABLED=true but ATLAS_SQS_QUEUE_URL is missing, the publisher will attempt to send with an empty QueueUrl and log an error for every event. Consider failing fast at construction time to surface misconfiguration early and avoid noisy per-event errors.

Copilot AI review requested due to automatic review settings September 1, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 37 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/tests/integration/atlas-pegout-events.integration.ts:249

  • In the peg-in integration tests, the context is passed with amountInWeis, but PeginAtlasEventBuilder expects amountInSatoshis. This makes swap.created publish input_amount as zero, reducing the test’s ability to catch regressions and producing an unrealistic event payload.
  describe('peg-in', () => {
    const context = {
      amountInWeis: '500000000000000000',
      rskRecipient: '0x2D623170Cb518434af6c02602334610f194818c1',
    };

docker-compose.yml:17

  • api is configured to wait for localstack to become healthy even when Atlas events are disabled (Noop publisher). This makes LocalStack a hard runtime dependency for local docker-compose usage and can prevent the API/daemon from starting if LocalStack is down, undermining the kill switch’s purpose (ship dark with no extra infra).
    depends_on:
      pp-api-db:
        condition: service_healthy
      localstack:
        condition: service_healthy

Copilot AI review requested due to automatic review settings September 1, 2026 15:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 43 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

docker-compose.yml:17

  • The api service now hard-depends on localstack being healthy. Since LocalStack is only needed for Atlas SQS publishing / integration testing, this makes docker compose up fail or slow down for developers who just want the API + Mongo stack. Consider making LocalStack optional (or gating it behind a compose profile) and not blocking API startup on it.
    depends_on:
      pp-api-db:
        condition: service_healthy
      localstack:
        condition: service_healthy

Comment thread src/services/atlas/sqs-atlas-event-publisher.ts
Comment thread src/services/atlas/noop-atlas-event-publisher.ts
@ronaldsg20
ronaldsg20 marked this pull request as ready for review September 2, 2026 14:26
@ronaldsg20
ronaldsg20 changed the base branch from v4.1.0 to main September 2, 2026 14:45
    Emits one Atlas SWAP event per native peg-out transition to an SQS FIFO
    queue, so peg-out volume, duration and rejection reasons become visible to
    the analytics side.
Emits Atlas SWAP events for native peg-in, keyed by btcTxId: LOCKED publishes
swap.created, and each rejection publishes swap.created followed by
swap.rejected. The daemon observes only Rootstock, so the deposit on Bitcoin is
never seen and swap.pending has no trigger; swap.completed is left out for now,
which means a successful peg-in stays PENDING on the analytics side.

PeginAtlasEventBuilder reads the amount and addresses from the Bridge logs,
since PeginStatusDataModel persists neither. Bridge rejection reasons are
passed through as PEGIN_REJECTION_<n> / PEGIN_UNREFUNDABLE_<n> rather than
translated, so no made-up semantics reach the analytics database. The peg-in
logs report satoshis directly, unlike the peg-out logs, whose amount is in
weis, so the shared satoshisToDecimalString helper is used with no conversion.
A review of the peg-in emission added in the previous commit found seven
problems. All seven are closed here, so the events a peg-in publishes now carry
the amount, the reason and the outcome that were missing or wrong.

Bridge rejection reasons are translated to the names of the rskj enums instead
of travelling as PEGIN_REJECTION_<n> / PEGIN_UNREFUNDABLE_<n>. The two logs
carry different enums in the same position — reason 3 means
LEGACY_PEGIN_UNDETERMINED_SENDER in rejected_pegin and INVALID_AMOUNT in
unrefundable_pegin — so the new atlas-pegin-reasons table branches on the event
name first and by number second, which is the confusion the raw numbers
invited. The error_code always names the rejected_pegin reason, the root cause
present in both branches, and the raw numbers of both logs travel in
error_message so a value this build cannot name is still recoverable from the
event. INVALID_AMOUNT is reported as validation, not protocol_violation.
…ents

- docker-compose.yml — api no longer depends_on localstack, so compose
  starts with the feature kill-switched off and LocalStack down
- sqs-atlas-event-publisher.ts — assertQueueUrlConfigured() fails the
  daemon at startup when ATLAS_EVENTS_ENABLED is on without a queue url,
  instead of failing every send and losing the events with no retry
- noop-atlas-event-publisher.ts — publish() no longer rejects if the
  logger throws, honouring the AtlasEventPublisher contract
- atlas-swap-event.schema.json — description covers peg-in and peg-out
- ENV_VARIABLES.md — swap_id is originatingRskTxHash for peg-out and
  btcTxId for peg-in; document the new queue url validation
- 6 unit tests for the queue url validation and the noop contract
Copilot AI review requested due to automatic review settings September 3, 2026 13:55
@ronaldsg20 ronaldsg20 changed the title feat(Daemon-SQS): publish Atlas SWAP events for native peg-out feat(Daemon-SQS): publish Atlas SWAP events for native peg-in and peg-out Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces a new cross-cutting event publication pipeline (SQS/DI/CI/schema) plus production-impacting peg-out persistence changes that warrant final human review.

Review details
  • Files reviewed: 42/43 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment on lines +133 to +137
destination_tx_hash: pegout.btcTxHash,
output_amount: this.toDecimalAmount(received),
output_amount_usd: null,
fee: this.toDecimalAmount(requested - received),
duration_ms: this.durationMs(pegout, context),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real problem, fixed in dd2bf63. The decimalAmount pattern is ^[0-9]+\.[0-9]{8}$ — no minus sign — so a received above requested produced "-0.00005000" and an event Atlas would reject outright.

Extracted a feeSatoshis() helper that clamps at zero and logs a warn carrying originatingRskTxHash, requested and received. Clamping silently would have hidden the very thing that caused it (bad output-to-pegout matching, or a corrupted status row), so the event stays valid and in the dataset while the warning is what says the numbers behind it cannot be trusted — the same degrade-but-be-noisy approach atlas-pegin-reasons.ts takes for unknown Bridge reasons.

New unit test covers an output paying more than was requested: fee comes out "0.00000000" and the event validates.

Comment on lines +56 to +64
app
.bind(ConstantsBindings.ATLAS_EVENTS_ENABLED)
.to(isAtlasEventsEnabled());

// The kill switch decides the transport, never the callers: the processors
// always depend on the AtlasEventPublisher interface.
const atlasEventPublisher: Constructor<AtlasEventPublisher> = isAtlasEventsEnabled()
? SqsAtlasEventPublisher
: NoopAtlasEventPublisher;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dd2bf63isAtlasEventsEnabled() is now evaluated once into a local const that feeds both the ATLAS_EVENTS_ENABLED binding and the publisher choice, with a comment saying why. Agreed it also reads better as one decision applied twice rather than two independent reads of process.env.

Comment thread src/services/pegin-data.processor.ts Outdated
Comment on lines +147 to +150
await events.reduce(async (promise, event) => {
await promise;
await this.atlasEventPublisher.publish(event, 'pegin');
}, Promise.resolve());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and changed in dd2bf63 — it is a plain for...of now, with a comment recording why the loop is sequential (the queue orders by MessageGroupId, so swap.created has to be sent before the outcome that follows it).

Worth recording why the reduce was there: for...of trips two airbnb rules in this repo, no-restricted-syntax and no-await-in-loop, so the lint count goes from 204 to 206 warnings (still 0 errors). I deliberately did not silence them with disable directives — the sibling pegout-data.processor.ts writes the same sequential await loop in two places (:145, :477) and simply accepts both warnings, and matching that is more honest than two eslint-disable lines on a three-line loop. Readability was the point of the comment, and disables would have undercut it.

Comment on lines +167 to 170
// getId() is the canonical big-endian txid; getHash() is the internal
// little-endian hash, which no explorer or Bitcoin node resolves.
newPegoutStatus.btcTxHash = parsedBtcTransaction.getId();
newPegoutStatus.isNewestStatus = true;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the inconsistency is real, but deferring it to its own PR rather than folding it in here.

The three write paths are:

  • :169parsedBtcTransaction.getId(), unprefixed hex (RELEASE_BTC)
  • :263 / :374 — the Bridge log bytes32, 0x-prefixed

Two reasons not to normalize it in this PR:

  1. The target format is a genuine choice, not an obvious one. Bare hex is canonical for a Bitcoin txid and is what explorers resolve — the whole point of the getId() change. But swap_id is already deliberately 0x-prefixed in both flows per the normalization documented in ENV_VARIABLES.md, so "0x everywhere" is defensible too. Picking one deserves its own discussion.
  2. Byte order is unverified. The getId() fix was about bitcoinjs-libs little-endian getHash(). Whether rskj emits the canonical big-endian txid in release_requested/batch_pegout_created, or the internal order, I have not confirmed against rskj. If the two differ in byte order then stripping 0x` would be purely cosmetic and would leave a real mismatch that now looks consistent — strictly worse than the current state.

Scope is contained in the meantime: btcTxHash is not part of the public API surface (absent from pegout-status.model.ts, the controllers and pegout-status.service.ts), so it reaches only Mongo and the Atlas destination_tx_hash, which the schema constrains to a non-empty string — both formats validate. Nothing is emitting an invalid event today.

@alexjavabraz
alexjavabraz requested a lite review from Copilot September 3, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The peg-out event builder can emit schema-invalid values (negative fee and negative expected_confirmations) under certain inputs, which should be guarded to keep emitted events contract-compliant.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

src/services/atlas/pegout-atlas-event.builder.ts:136

  • fee is derived as requested - received with no guard. If valueInSatoshisToBeReceived is ever greater than valueRequestedInSatoshis (bad/misaligned data, parsing issues, etc.), this becomes negative and produces a decimal string that violates the JSON Schema (no negatives), potentially breaking downstream consumers. Clamp the computed fee to a minimum of 0 satoshis before formatting.
    src/services/pegin-data.processor.ts:150
  • Using Array.reduce(async ...) for sequential publishing is harder to read and easier to get wrong than a simple for...of loop. A straightforward loop preserves the current behavior (publish in order, stop on the first thrown error and fall into the surrounding catch) with less cognitive overhead.
  • Files reviewed: 42/43 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +163 to +166
private static expectedConfirmations(): number {
const configured = parseInt(process.env.RSK_PEGOUT_MINIMUM_CONFIRMATIONS ?? '', 10);
return Number.isFinite(configured) ? configured : 0;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in dd2bf63. Number.isFinite(-1) is true, so RSK_PEGOUT_MINIMUM_CONFIRMATIONS=-1 went straight through into an expected_confirmations the schema types as {"type": "integer", "minimum": 0}. A negative value is now treated exactly like an unset or unparseable one and falls back to 0:

const configured = parseInt(process.env.RSK_PEGOUT_MINIMUM_CONFIRMATIONS ?? "", 10);
if (!Number.isFinite(configured) || configured < 0) {
  return 0;
}

New unit test asserts the fallback for -1 and -4000 and validates the resulting event against the JSON Schema.

…e DI wiring

- pegout-atlas-event.builder.ts — expected_confirmations rejects negative
  RSK_PEGOUT_MINIMUM_CONFIRMATIONS the same way it rejects an unset one,
  and feeSatoshis() clamps a negative fee to zero with a warn: the schema's
  decimalAmount pattern admits no minus sign, so either would have emitted
  an event Atlas rejects
- pegin-data.processor.ts — sequential publish loop reads as a for...of
  instead of an async reduce
- dependency-injection-handler.ts — evaluate the kill switch once
- 2 unit tests covering both guards against the JSON Schema
Copilot AI review requested due to automatic review settings September 6, 2026 23:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Dockerfile runs Node via a shell without exec, which can interfere with signal handling and graceful shutdown in container environments.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

Dockerfile:19

  • The container CMD runs Node through sh -c without exec, so PID 1 is the shell. In orchestrated environments this can prevent SIGTERM (and other signals) from reaching Node reliably, which can break graceful shutdown and increase the risk of abrupt termination during deploys.
  • Files reviewed: 42/43 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants