Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
59 changes: 59 additions & 0 deletions .github/workflows/publish-dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Publish DEV image

on:
push:
branches: [develop]
paths-ignore:
- '**.md'
workflow_dispatch:

permissions:
contents: read
actions: write

concurrency:
group: publish-dev
cancel-in-progress: false

env:
DOCKER_TAGS: dfxswiss/opencryptopay-api:beta

jobs:
publish:
name: Build and push beta
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- name: Require develop
if: github.ref != 'refs/heads/develop'
run: |
echo "::error::This workflow only publishes develop as beta"
exit 1

- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/arm64
tags: ${{ env.DOCKER_TAGS }}
provenance: false
sbom: false
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/openCryptoPay/api
cache-from: type=gha
cache-to: type=gha,mode=max
59 changes: 59 additions & 0 deletions .github/workflows/publish-prd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Publish PRD image

on:
push:
branches: [main]
paths-ignore:
- '**.md'
workflow_dispatch:

permissions:
contents: read
actions: write

concurrency:
group: publish-prd
cancel-in-progress: false

env:
DOCKER_TAGS: dfxswiss/opencryptopay-api:latest

jobs:
publish:
name: Build and push latest
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- name: Require main
if: github.ref != 'refs/heads/main'
run: |
echo "::error::This workflow only publishes main as latest"
exit 1

- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/arm64
tags: ${{ env.DOCKER_TAGS }}
provenance: false
sbom: false
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/openCryptoPay/api
cache-from: type=gha
cache-to: type=gha,mode=max
31 changes: 21 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ The map API keeps the same gates as the 21.gifts API.

## Branches

| Branch | Purpose |
| --------- | -------------------------------------------------------- |
| `develop` | Integration branch. Open pull requests here. |
| `main` | Production. Moves only through the release pull request. |
| Branch | Purpose | Image |
| --------- | -------------------------------------------------------- | ----------------------------------- |
| `develop` | Integration branch. Open pull requests here. | `dfxswiss/opencryptopay-api:beta` |
| `main` | Production. Moves only through the release pull request. | `dfxswiss/opencryptopay-api:latest` |

- Land work on `develop` through a feature branch and a pull request. Do not push `develop` or `main` directly.
- A push to `develop` runs CI on that commit. The release pull request shows that same CI run.
- This repository does not publish a container image.
- A push to `develop` publishes `dfxswiss/opencryptopay-api:beta`, except a push that changes only Markdown, and runs CI on that commit. The release pull request shows that same CI run. A manual run publishes only when it is started on `develop`.
- A push to `main` publishes `dfxswiss/opencryptopay-api:latest`, except a push that changes only Markdown. A manual run publishes only when it is started on `main`.
- `auto-release-pr.yaml` opens `Release: develop -> main` when `develop` is ahead of `main`.
- Never force-push, and never amend a published commit.

Expand All @@ -35,7 +35,18 @@ Do not lower a coverage threshold to land a change. A genuinely unreachable bran

## CI / CD

| Workflow | Trigger | Action |
| ---------------------- | ----------------------------------- | ----------------------------------------------------------------------- |
| `ci.yaml` | Pull request, and push to `develop` | Typecheck, lint, handbook, e2e-check, test (100% coverage), build, e2e |
| `auto-release-pr.yaml` | Push to `develop` | Open the release pull request (`develop` → `main`) when there is a diff |
| Workflow | Trigger | Action |
| ---------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `ci.yaml` | Pull request, and push to `develop` | Typecheck, lint, handbook, e2e-check, test (100% coverage), build, e2e |
| `publish-dev.yaml` | Push to `develop`, except Markdown-only, or a manual run on `develop` | Build and push `dfxswiss/opencryptopay-api:beta` |
| `publish-prd.yaml` | Push to `main`, except Markdown-only, or a manual run on `main` | Build and push `dfxswiss/opencryptopay-api:latest` |
| `auto-release-pr.yaml` | Push to `develop` | Open the release pull request (`develop` → `main`) when there is a diff |

Images are `linux/arm64`. The publish workflows push the image and do not deploy it.

Publish workflows need these Actions secrets:

| Secret | Purpose |
| ----------------- | --------------------------------------------------------------------------- |
| `DOCKER_USERNAME` | Docker Hub username for the push |
| `DOCKER_PASSWORD` | Docker Hub access token with read and write on `dfxswiss/opencryptopay-api` |
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# OpenCryptoPay place API.
#
# Build:
# docker build -t opencryptopay/api:beta .
# docker build -t opencryptopay/api:latest .
# docker build -t dfxswiss/opencryptopay-api:beta .
# docker build -t dfxswiss/opencryptopay-api:latest .

FROM oven/bun:1.3-alpine AS deps
WORKDIR /app
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenCryptoPay API

Map API. `GET /map/places` is the public list of places, each with a tech provider. `POST /map/places` adds a place once. `PUT /map/places` updates a place. `DELETE /map/places` removes a place.
Map API. `GET /map/places` is the public list of places, each with a tech provider; `?origin=` limits that list to one origin. The list can also be limited by country, shopName (`SPAR` or `others`), blockchain, and asset; country is a stored column (the DFX country symbol). A pin with support rows matches only those rows. A pin with none uses a hardcoded payment-link catalog: `21.gifts` is Lightning and BTC, and other pins use the DFX payment-link networks and assets. `GET /map/filters` lists shop names, stored countries, and that catalog plus any stored values. `POST /map/places` adds a place once. `PUT /map/places` updates a place. `DELETE /map/places` removes a place.

The same `origin` and `externalId` returns the first pin and does not submit it again. When `BTCMAP_ACCESS_TOKEN` is set, a new pin is posted once to `https://api.btcmap.org/v4/place-submissions`. A missing token stores the pin and skips that call.

Expand All @@ -20,8 +20,12 @@ The process listens on port `3000` unless `PORT` is set. The database file is `d

## Branches

Pull requests target `develop`. `main` moves only through the release pull request from `develop`. This repository does not publish a container image.
Pull requests target `develop`. A push there publishes `dfxswiss/opencryptopay-api:beta`, except a push that changes only Markdown. `main` publishes `dfxswiss/opencryptopay-api:latest` the same way and moves only through the release pull request from `develop`. A manual run publishes only when it is started on that branch.

## Checks

`bun run typecheck`, `bun run lint`, `bun run handbook:check`, `bun run e2e:check`, `bun run test:coverage`, `bun run build`, and `bun run e2e`. Coverage on `src` is 100 percent. `src/index.ts` is the process entry and is exercised by the HTTP end-to-end run.

## Product notes

Notes that are not specific to this API are in [docs/product](docs/product/README.md). [Where place maps are shown](docs/product/maps.md) is the first of those notes.
18 changes: 16 additions & 2 deletions docs/handbook/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@

## Endpoint: GET /map/places

- **Purpose:** Public map list, newest first. Each item is `id`, `origin`, `name`, `lat`, `lon`, `category`, and `techProvider`. The caller id and payment methods are not included.
- **Errors:** 400 `{ error: "Invalid limit" }` when `limit` is not an integer from 1 to 1000. Omitted `limit` means 1000.
- **Purpose:** Public map list, newest first. Each item is `id`, `origin`, `name`, `lat`, `lon`, `category`, `techProvider`, `country`, `shopName`, `supports`, and `activity`. `country` is the stored DFX country symbol or null. `shopName` is the stored brand or null. `supports` is the stored blockchain and asset pairs, sorted. `activity` is `within7Days` when the stored instant is at most 7×24h before the request clock (exactly 7 days stays in this bucket; a future instant is `within7Days`), `within30Days` when it is older than 7×24h and at most 30×24h (exactly 30 days stays in this bucket; one millisecond later is `none`), or `none` when there is no stored instant, the instant is unparseable, or it is older than 30×24h. The caller id and payment methods are not included. Omitted `origin` is every pin. A present `origin` must be the same token as a stored origin. Optional `country`, `shopName` (`SPAR` or `others`), `blockchain`, and `asset` further restrict the store read. All predicates are AND and run before `limit`. `country` is a stored column, not inferred from an address. `others` includes a pin with no brand. A pin that already has support rows matches only those rows. A pin with none matches the hardcoded payment-link catalog: origin `21gifts` or tech provider `21.gifts` matches Lightning and BTC only, and every other pin matches a catalog network, asset, or pair.
- **Errors:** 400 `{ error: "Invalid limit" }` when `limit` is not an integer from 1 to 1000. Omitted `limit` means 1000. 400 `{ error: "Place origin is invalid" }` when `origin` is present but not a valid origin token. 400 `{ error: "Place country is invalid" }` when `country` is present but not an assigned ISO 3166-1 alpha-2 DFX country symbol. 400 `{ error: "Place shop name is invalid" }` when `shopName` is present and not `SPAR` or `others`. 400 `{ error: "Place support is invalid" }` when `blockchain` is present but not a payment-link or stored payment-network name, or when `asset` is present but not a ticker, a prefixed ticker, or a payment-link catalog name.
- **Used by:** The OpenCryptoPay map.
- **Auth:** none. Browser calls are allowed from any origin.

## Endpoint: GET /map/filters

- **Purpose:** Public list of filter values for the map. `shopNames` is always exactly `["SPAR", "others"]`. `countries` are the distinct stored ISO country codes, sorted. `blockchains` and `assets` are the hardcoded payment-link catalog plus distinct stored values, sorted. `?blockchain=` limits the top-level `assets` to that chain and still returns every blockchain. `techProviders` lists the full hardcoded offer for `DFX.swiss` and for `21.gifts` (Lightning and BTC only) and is not narrowed by `?blockchain=`.
- **Errors:** 400 `{ error: "Place support is invalid" }` when `blockchain` is present but not a payment-link or stored payment-network name.
- **Used by:** The OpenCryptoPay map filter UI.
- **Auth:** none. Browser calls are allowed from any origin.

## Endpoint: POST /map/places

- **Purpose:** Create one map pin. The same `origin` and `externalId` returns the first row, including its tech provider, and does not call BTC Map again. A new row is posted once when `BTCMAP_ACCESS_TOKEN` is set. An omitted tech provider is stored as `DFX.swiss`.
Expand All @@ -34,3 +41,10 @@
- **Errors:** 503 `{ error: "Place ingest is not configured" }` when the ingest token is unset or blank. 401 `{ error: "Unauthorized" }` when the bearer does not match. 400 with the validator message for a bad body.
- **Used by:** Trusted callers that take a shop off the public map.
- **Auth:** `Authorization: Bearer` must equal `OCP_PLACE_INGEST_TOKEN`. The JSON body is `origin` and `externalId`.

## Endpoint: POST /map/places/transactions

- **Purpose:** Record a transaction time on an existing pin. The JSON body is `origin`, `externalId`, and optional `occurredAt`. The response is `{ activity }` classified at the request clock, not at `occurredAt`. This call does not create a pin and does not call BTC Map. An older or equal stored instant is kept. Browser CORS `allowMethods` stays `GET` and `OPTIONS`; this call is server-to-server.
- **Errors:** 503 `{ error: "Place ingest is not configured" }` when the ingest token is unset or blank. 401 `{ error: "Unauthorized" }` when the bearer does not match. 400 with the validator message for a bad body, including `Place transaction time is invalid`. 404 `{ error: "Place not found" }` when the pair is missing.
- **Used by:** Trusted callers that report a shop transaction.
- **Auth:** `Authorization: Bearer` must equal `OCP_PLACE_INGEST_TOKEN`. Browser CORS `allowMethods` stays GET and OPTIONS.
Loading
Loading