Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ MAX_FEE_AMOUNT_ALLOWED=5000000

#Dust value (Satoshi)
BURN_DUST_VALUE=2000

# Atlas SWAP events
ATLAS_EVENTS_ENABLED=false
ATLAS_SQS_QUEUE_URL='http://localhost:4566/000000000000/atlas-swap-events.fifo'
ATLAS_SQS_ENDPOINT='http://localhost:4566'
AWS_REGION='us-east-1'
AWS_ACCESS_KEY_ID='test'
AWS_SECRET_ACCESS_KEY='test'
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ dist/
coverage/
.eslintrc.js
__tests__/
mongo-init.js
mongo-init.js
ci/
tools/
48 changes: 48 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,51 @@ jobs:
npm run test:all
npm run eslint
npm run only-coverage

integration-tests:
runs-on: ubuntu-latest
services:
localstack:
image: localstack/localstack:3
ports: ['4566:4566']
env:
SERVICES: sqs
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup NodeJS
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Generate .env file
run: |
cp .env.test .env

- name: Install dependencies
run: npm ci

# Service containers do not run LocalStack's init/ready.d hooks, so the
# queue is created explicitly instead of reusing ci/localstack-init.
- name: Create SQS queue
run: node ci/create-atlas-queue.js
env:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_REGION: us-east-1
ATLAS_SQS_ENDPOINT: http://localhost:4566

- name: Run integration tests
run: npm run integration-test
env:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_REGION: us-east-1
ATLAS_SQS_ENDPOINT: http://localhost:4566
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ COPY --chown=node:node . ./

RUN npm run build

CMD ["node", "."]
# API and daemon are deployed as separate processes. APP_MODE keeps the
# default behaviour (both in one process) while letting the orchestrator
# select API or DAEMON explicitly instead of relying on an omitted flag.
ENV APP_MODE=ALL
CMD ["sh", "-c", "node . --appmode=$APP_MODE"]
76 changes: 75 additions & 1 deletion ENV_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This table was created to guide and centralize the **environment variables** nec
|FEE_PER_KB_SLOW_MIN |100 |'Fee per kb slow' |
|BURN_DUST_VALUE |2000 |'Burn dust value' |
|BTC_CONFIRMATIONS |100 |'testnet or mainnet' |
|NETWORK |`testnet or mainnet` |'testnet or mainnet' |
|NETWORK |`testnet or mainnet` |'testnet or mainnet. Required: the daemon refuses to start with any other value' |
|BLOCKBOOK_URL | |'Blockbook url' |
|MAX_AMOUNT_ALLOWED_IN_SATOSHI | |'Pegin Pegout max allowed in satoshis' |
|LOG_FORMAT |`json or pretty` |'Log output format. Defaults to json' |
Expand All @@ -37,6 +37,80 @@ This table was created to guide and centralize the **environment variables** nec
|BACKOFFICE_API_PASSWORD | |'Backoffice service account password. Secret β€” never commit'|
|BACKOFFICE_FLAGS_CACHE_TTL_MS |60000 |'How long retrieved flags are cached before re-fetching'|
|BACKOFFICE_HTTP_TIMEOUT_MS |2000 |'Timeout for each backoffice HTTP request'|
|ATLAS_EVENTS_ENABLED |`false` |'Kill switch for Atlas SWAP event publication. Only the literal `true` enables it'|
|ATLAS_SQS_QUEUE_URL | |'URL of the SQS FIFO queue the Atlas events are published to. Required when `ATLAS_EVENTS_ENABLED=true`; the daemon aborts at startup without it'|
|AWS_REGION |`us-east-1` |'AWS region of the Atlas SQS queue' |
|ATLAS_SQS_ENDPOINT |`http://localhost:4566` |'Custom SQS endpoint. Local development and tests only (LocalStack); leave empty in deployments'|

### Atlas SWAP events

While `ATLAS_EVENTS_ENABLED=true`, the daemon publishes Atlas SWAP events to the
SQS FIFO queue at `ATLAS_SQS_QUEUE_URL` as it processes Bridge transactions.
**Only the daemon publishes**: the publisher is registered by
`configureDaemonDependencies` and is simply not bound in the API process.

Peg-out, one event per transition: `swap.created` (RECEIVED), `swap.pending`
(WAITING_FOR_CONFIRMATION), `swap.completed` (RELEASE_BTC) and `swap.rejected`
(REJECTED). `WAITING_FOR_SIGNATURE` publishes nothing: it is an internal
federation sub-state with no equivalent in the v1.0 schema.

Peg-in, keyed by `btcTxId`, two events per outcome: `LOCKED` publishes
`swap.created` and `swap.completed`, and a rejection publishes `swap.created`
followed by `swap.rejected`. `swap.pending` has no trigger, because the daemon
observes only Rootstock and never sees the deposit on Bitcoin.

The `swap.completed` of a peg-in carries `duration_ms: null` β€” the Bitcoin
broadcast time is unknown, and a zero would drag the average duration down β€” and
`fee: "0.00000000"`, because the Bridge credits the whole amount sent. Its
`destination_tx_hash` is the Rootstock transaction that credited the RBTC.

Rejection reasons are translated to the names of the rskj enums
(`RejectedPeginReason`, `NonRefundablePeginReason`) in
`models/atlas/atlas-pegin-reasons.ts`, and the raw numbers of both logs travel in
`error_message`. The `error_code` always names the `rejected_pegin` reason, the
root cause present in every branch; the exception is a rejection the Bridge
followed with no refund branch at all, reported as
`PEGIN_REJECTED_NO_REFUND_BRANCH`. **This table has to stay aligned with rskj**:
a value added there falls back to `UNKNOWN` with a `warn`, which degrades well
but only if someone reads the warning.

`swap_id` and `wallet_address` are normalized β€” 0x-prefixed and lowercase β€” in
both flows, so one transaction cannot reach Atlas under two spellings. Bitcoin
addresses are left alone, since base58 is case sensitive.

The `swap_id` identifies the swap on the chain the funds come from: a peg-out
carries its `originatingRskTxHash`, a peg-in its `btcTxId`. The queue's
`MessageGroupId` is that same `swap_id`, so the transitions of one swap stay
ordered while different swaps are processed in parallel. The queue must have
content based deduplication **disabled**: `MessageDeduplicationId` is the
`event_id`.

The network travels in the chain ids (`rootstock_testnet` / `bitcoin_testnet`),
derived from `NETWORK`. Because a wrong network would silently contaminate the
analytics database, `NETWORK` is validated when the daemon starts and the daemon
aborts if it is neither `mainnet` nor `testnet`.

`ATLAS_SQS_QUEUE_URL` is validated the same way, and only while the switch is
on: a blank url would not disable publication, it would fail every send and lose
the events with no retry, so the daemon aborts at startup rather than running
blind. With `ATLAS_EVENTS_ENABLED` off the variable is not read at all.

Publication happens after the status has been written to Mongo and never fails
the caller: if SQS is unreachable the failure is logged at error level and block
processing continues. Events lost in that window are not recovered.

Every publication, successful or not, logs one line carrying
`metric: 'atlas_events_published_total'` with `status`, `flow`, `eventType` and
the running `total`. **That field name is the contract with the log aggregator**
β€” it is what an alert on lost events queries, so it must not be renamed for
style. The counter makes the loss above visible; it does not fix it, and
idempotency by `btcTxId` means a re-sync will not retry a peg-in it already
recorded.

Credentials come from the standard AWS SDK chain (an IAM role in deployments,
`test`/`test` against LocalStack). `docker compose up` starts a LocalStack
container that creates the queue from `ci/localstack-init`; from inside the
compose network the endpoint host is `localstack`, not `localhost`.

### Backoffice feature flags

Expand Down
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,20 @@ The running application serves an interactive REST Explorer at `/explorer` and a
## Testing

```sh
npm run unit-test # dist/__tests__/**/*.unit.js
npm run acceptance-test # dist/__tests__/**/*.acceptance.js
npm run test:all # both suites
npm run coverage # nyc report (after a test run)
npm run unit-test # dist/__tests__/**/*.unit.js
npm run acceptance-test # dist/__tests__/**/*.acceptance.js
npm run test:all # both suites
npm run integration-test # dist/__tests__/integration/**/*.integration.js (needs LocalStack)
npm run coverage # nyc report (after a test run)
```

`integration-test` is deliberately left out of `test:all` so it does not slow
the fast unit cycle; it runs in its own CI job. It needs the LocalStack SQS
queue up:

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

## Fix code style and formatting issues
Expand Down Expand Up @@ -107,6 +117,7 @@ The project includes Docker Compose configuration for running both the API and M

- **API Service**: Runs on port 3000, connects to MongoDB using service name `pp-api-db`
- **MongoDB Service**: Runs on ports 27017-27019, automatically initializes with user and database from environment variables
- **LocalStack Service**: Runs on port 4566 and creates the `atlas-swap-events.fifo` SQS queue from `ci/localstack-init`, so the Atlas SWAP events have somewhere to go locally

### Start Database Only (Development)

Expand Down Expand Up @@ -135,6 +146,23 @@ The API will be accessible at `http://localhost:3000` and will automatically con

**Note**: When running in Docker, the API uses `pp-api-db` as the MongoDB host (configured in docker-compose.yml). For local development without Docker, use `localhost`.

### Atlas SWAP events locally

To watch the daemon publish Atlas events against LocalStack, add to your `.env`:

```
ATLAS_EVENTS_ENABLED=true
ATLAS_SQS_ENDPOINT=http://localhost:4566
ATLAS_SQS_QUEUE_URL=http://localhost:4566/000000000000/atlas-swap-events.fifo
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
```

From inside the compose network the host is `localstack`, not `localhost`. The
kill switch starts off in every environment; see
[`ENV_VARIABLES.md`](./ENV_VARIABLES.md) for what each event carries.

## Other useful commands

- `npm run openapi-spec`: Generate OpenAPI spec into a file
Expand Down
35 changes: 35 additions & 0 deletions ci/create-atlas-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env node
/**
* Creates the Atlas SWAP events FIFO queue.
*
* LocalStack's `init/ready.d` hooks (see `ci/localstack-init`) do not run when
* LocalStack is started as a GitHub Actions service container, so the queue is
* created explicitly there. Written against the AWS SDK already in the
* dependency tree rather than the AWS CLI, so it behaves the same on a hosted
* runner and under `act`.
*/
const {SQSClient, CreateQueueCommand} = require('@aws-sdk/client-sqs');

const region = process.env.AWS_REGION || 'us-east-1';
const endpoint = process.env.ATLAS_SQS_ENDPOINT || 'http://localhost:4566';
const queueName = process.env.ATLAS_SQS_QUEUE_NAME || 'atlas-swap-events.fifo';

async function main() {
const client = new SQSClient({region, endpoint});
try {
const {QueueUrl} = await client.send(
new CreateQueueCommand({
QueueName: queueName,
Attributes: {FifoQueue: 'true', ContentBasedDeduplication: 'false'},
}),
);
console.log(QueueUrl);
} finally {
client.destroy();
}
}

main().catch(error => {
console.error(error.message);
process.exit(1);
});
6 changes: 6 additions & 0 deletions ci/localstack-init/01-create-queue.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail

awslocal sqs create-queue \
--queue-name atlas-swap-events.fifo \
--attributes FifoQueue=true,ContentBasedDeduplication=false
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ services:
timeout: 5s
retries: 5
start_period: 10s
localstack:
image: localstack/localstack:3
container_name: pp-api-localstack
ports:
- "4566:4566"
environment:
SERVICES: sqs
AWS_DEFAULT_REGION: us-east-1
DEBUG: 0
volumes:
- ./ci/localstack-init:/etc/localstack/init/ready.d
networks:
- pp-api-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
networks:
pp-api-network:
name: pp-api-network
Expand Down
Loading
Loading