diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml new file mode 100644 index 00000000..1684e1dd --- /dev/null +++ b/.github/workflows/load-tests.yml @@ -0,0 +1,106 @@ +name: Load Tests + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + determine-service-matrix: + runs-on: ubuntu-latest + outputs: + services: ${{ steps.determine-matrix.outputs.changes }} + steps: + - name: Check Out Repo + uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: determine-matrix + with: + # Note, 'common' is for future use--when code is factored out into common libraries, + # but it's set up here so that the workflow logic could be tested with it present + filters: | + common: + - '*' + - '.github/**' + account: + - '*' + - '.github/**' + - 'services/account/**' + graph: + - '*' + - '.github/**' + - 'services/graph/**' + content-publishing: + - '*' + - '.github/**' + - 'services/content-publishing/**' + content-watcher: + - '*' + - '.github/**' + - 'services/content-watcher/**' + + build: + name: '[${{ matrix.service }}] Load Test' + runs-on: ubuntu-latest + needs: determine-service-matrix + strategy: + fail-fast: false + matrix: + service: ${{ fromJson(needs.determine-service-matrix.outputs.services) }} + exclude: + - service: common + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + cache-dependency-path: tools/ci-k6/package-lock.json + - name: Install dependencies + working-directory: tools/ci-k6 + run: npm ci + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{secrets.DOCKERHUB_USERNAME}} + password: ${{secrets.DOCKERHUB_TOKEN}} + + - name: Start Frequency + run: | + docker compose -f docker-compose.yaml -f docker-compose-k6.${{ matrix.service }}.yaml up -d frequency + sleep 5 + - name: Generate Provider and Capacity + working-directory: tools/ci-k6 + run: npm run main + + # Just start the services we need... + - name: Start All Services + run: docker compose -f docker-compose.yaml -f docker-compose-k6.${{ matrix.service }}.yaml up -d + + - name: Wait for API to be ready + uses: cygnetdigital/wait_for_response@v2.0.0 + with: + url: 'http://localhost:3000/readyz' + responseCode: '200' + timeout: 120000 + interval: 2000 + + - name: Setup K6 + uses: grafana/setup-k6-action@v1 + - name: Run k6 Tests + run: | + for test_file in services/${{ matrix.service }}/k6-test/*.k6.js; do + k6 run --no-color --quiet --no-usage-report "$test_file" || exit 1 + done + - name: Stop Docker Compose + if: always() + run: docker compose -f docker-compose.yaml -f docker-compose-k6.${{ matrix.service }}.yaml down diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e64fd398..852c09c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ -name: "Release" -run-name: "Cut Release ${{ github.event.release.tag_name || github.event.inputs.release-tag}}" +name: 'Release' +run-name: 'Cut Release ${{ github.event.release.tag_name || github.event.inputs.release-tag}}' concurrency: group: ${{github.workflow}}-${{github.ref}} cancel-in-progress: true @@ -9,12 +9,12 @@ on: workflow_dispatch: inputs: release-tag: - description: "Release tag ([servicename-]v#.#.#[-rc#])" + description: 'Release tag ([servicename-]v#.#.#[-rc#])' required: true env: FULL_TAG: ${{ github.event.release.tag_name || github.event.inputs.release-tag}} - DOCKER_HUB_PROFILE: amplicalabs + DOCKER_HUB_PROFILE: projectlibertylabs ALL_SERVICES: '["account", "content-publishing", "content-watcher", "graph"]' jobs: @@ -33,7 +33,7 @@ jobs: run: | valid=false test_run=false - ENTIRE_TAG_RE="^(([[:alpha:]]+)-)?(.*)$" + ENTIRE_TAG_RE="^(([[:alpha:]\-]+)-)?(.*)$" VERSION_TAG_RE="^v[\.]?([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-[[:alpha:][:digit:]\.]+)*)$" TAG="${{ github.event.release.tag_name }}" echo "Running with tag: ${TAG}" @@ -107,13 +107,13 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v3 with: - username: ${{secrets.DOCKERHUB_USERNAME_FC}} - password: ${{secrets.DOCKERHUB_TOKEN_FC}} + username: ${{secrets.DOCKERHUB_USERNAME}} + password: ${{secrets.DOCKERHUB_TOKEN}} - name: Build and Push account-service-service Image uses: docker/build-push-action@v5 with: - context: services/${{ matrix.service }} + context: . platforms: linux/amd64 push: ${{ needs.set_variables.outputs.test_run != 'true'}} - file: services/account/Dockerfile + file: Docker/Dockerfile.${{ matrix.service }} tags: ${{ steps.cp-tags.outputs.tags }} diff --git a/.gitignore b/.gitignore index c30d5ee1..8e28caab 100644 --- a/.gitignore +++ b/.gitignore @@ -77,7 +77,7 @@ web_modules/ .yarn-integrity # dotenv environment variable files -.env +.env* .env.* # parcel-bundler cache (https://parceljs.org/) diff --git a/services/account/Dockerfile b/Docker/Dockerfile.account similarity index 50% rename from services/account/Dockerfile rename to Docker/Dockerfile.account index cb2ede76..9f0582c8 100644 --- a/services/account/Dockerfile +++ b/Docker/Dockerfile.account @@ -1,30 +1,37 @@ # Use a multi-stage build for efficiency FROM node:20 AS builder -WORKDIR /app +WORKDIR /build/packages + +COPY packages ./ + +WORKDIR /build/services/account -COPY package*.json ./ +COPY services/account/package* ./ RUN npm ci -COPY . . +COPY services/account ./ # Build the application -RUN npm run build +RUN npm run build && \ + rm -rf node_modules && \ + npm ci --omit=dev # Production stage FROM node:20 +ENV NODE_ENV=production + WORKDIR /app -COPY --from=builder /app/dist ./dist -COPY package*.json ./ -COPY ./lua ./lua -COPY ./scripts/docker-entrypoint.sh ./ +COPY --from=builder /build/services/account/dist ./dist/ +COPY services/account/package*.json ./ +COPY services/account/lua ./lua/ +COPY services/account/scripts/docker-entrypoint.sh ./ +COPY --from=builder /build/services/account/node_modules ./node_modules/ RUN chmod +x ./docker-entrypoint.sh -RUN npm ci --omit=dev - # We want jq and curl in the final image, but we don't need the support files RUN apt-get update && \ apt-get install -y jq curl tini && \ diff --git a/Docker/Dockerfile.content-publishing b/Docker/Dockerfile.content-publishing new file mode 100644 index 00000000..e7104a9e --- /dev/null +++ b/Docker/Dockerfile.content-publishing @@ -0,0 +1,45 @@ +# Use a multi-stage build for efficiency +FROM node:20 AS builder + +WORKDIR /build/packages + +COPY packages ./ + +WORKDIR /build/services/content-publishing + +COPY services/content-publishing/package* ./ + +RUN npm ci + +COPY services/content-publishing ./ + +# Build the application +RUN npm run build && \ + rm -rf node_modules && \ + npm ci --omit=dev + +# Production stage +FROM node:20 + +ENV NODE_ENV=production + +WORKDIR /app + +COPY --from=builder /build/services/content-publishing/dist ./dist/ +COPY services/content-publishing/package*.json ./ +COPY services/content-publishing/lua ./lua/ +COPY services/content-publishing/scripts/docker-entrypoint.sh ./ +COPY --from=builder /build/services/content-publishing/node_modules ./node_modules/ +RUN chmod +x ./docker-entrypoint.sh + +# We want jq and curl in the final image, but we don't need the support files +RUN apt-get update && \ + apt-get install -y jq curl tini && \ + apt-get clean && \ + rm -rf /usr/share/doc /usr/share/man /usr/share/zsh + +EXPOSE 3000 + +ENV START_PROCESS="api" + +ENTRYPOINT ["/usr/bin/tini", "--", "./docker-entrypoint.sh"] diff --git a/Docker/Dockerfile.content-watcher b/Docker/Dockerfile.content-watcher new file mode 100644 index 00000000..8f060d31 --- /dev/null +++ b/Docker/Dockerfile.content-watcher @@ -0,0 +1,40 @@ +# Use a multi-stage build for efficiency +FROM node:20 AS builder + +WORKDIR /build/packages + +COPY packages ./ + +WORKDIR /build/services/content-watcher + +COPY services/content-watcher/package* ./ + +RUN npm ci + +COPY services/content-watcher ./ + +# Build the application +RUN npm run build && \ + rm -rf node_modules && \ + npm ci --omit=dev + +# Production stage +FROM node:20 + +ENV NODE_ENV=production + +WORKDIR /app + +COPY --from=builder /build/services/content-watcher/dist ./dist/ +COPY services/content-watcher/package*.json ./ +COPY --from=builder /build/services/content-watcher/node_modules ./node_modules/ + +# We want jq and curl in the final image, but we don't need the support files +RUN apt-get update && \ + apt-get install -y jq curl tini && \ + apt-get clean && \ + rm -rf /usr/share/doc /usr/share/man /usr/share/zsh + +EXPOSE 3000 + +ENTRYPOINT [ "/usr/bin/tini", "--", "node", "dist/apps/api/main.js" ] diff --git a/Docker/Dockerfile.dev b/Docker/Dockerfile.dev new file mode 100644 index 00000000..0c2d6852 --- /dev/null +++ b/Docker/Dockerfile.dev @@ -0,0 +1,7 @@ +FROM node:20 + +WORKDIR /app + +EXPOSE 3000 + +VOLUME [ "/app", "/app/services/account/node_modules", "/app/services/graph/node_modules", "/app/services/content-publishing/node_modules", "/app/services/content-watcher/node_modules" ] diff --git a/Docker/Dockerfile.graph b/Docker/Dockerfile.graph new file mode 100644 index 00000000..d32d4c33 --- /dev/null +++ b/Docker/Dockerfile.graph @@ -0,0 +1,45 @@ +# Use a multi-stage build for efficiency +FROM node:20 AS builder + +WORKDIR /build/packages + +COPY packages ./ + +WORKDIR /build/services/graph + +COPY services/graph/package* ./ + +RUN npm ci + +COPY services/graph ./ + +# Build the application +RUN npm run build && \ + rm -rf node_modules && \ + npm ci --omit=dev + +# Production stage +FROM node:20 + +ENV NODE_ENV=production + +WORKDIR /app + +COPY --from=builder /build/services/graph/dist ./dist/ +COPY services/graph/package*.json ./ +COPY services/graph/lua ./lua/ +COPY services/graph/scripts/docker-entrypoint.sh ./ +COPY --from=builder /build/services/graph/node_modules ./node_modules/ +RUN chmod +x ./docker-entrypoint.sh + +# We want jq and curl in the final image, but we don't need the support files +RUN apt-get update && \ + apt-get install -y jq curl tini && \ + apt-get clean && \ + rm -rf /usr/share/doc /usr/share/man /usr/share/zsh + +EXPOSE 3000 + +ENV START_PROCESS="api" + +ENTRYPOINT ["/usr/bin/tini", "--", "./docker-entrypoint.sh", "prod"] diff --git a/Makefile b/Makefile index d60bbbe5..939ee40f 100644 --- a/Makefile +++ b/Makefile @@ -64,4 +64,27 @@ $(FORMAT_TARGETS): @( cd services/$(@:format-%=%) ; npm run format ) $(DOCKER_BUILD_TARGETS): - @( cd services/$(@:docker-build-%=%) ; npm run docker:build ) + @docker build -t $(@:docker-build-%=%)-service -f Docker/Dockerfile.$(@:docker-build-%=%) . + +docker-build: $(DOCKER_BUILD_TARGETS) + +start-account-api: + @( cd services/account ; npm i ; npm run start:api:watch ) + +start-account-worker: + @( cd services/account ; npm i ; npm run start:worker:watch ) + +start-content-publishing-api: + @( cd services/content-publishing ; npm i ; npm run start:api:watch ) + +start-content-publishing-worker: + @( cd services/content-publishing ; npm i ; npm run start:worker:watch ) + +start-content-watcher: + @( cd services/content-watcher ; npm i ; npm run start:watch ) + +start-graph-api: + @( cd services/graph ; npm i ; npm run start:api:watch ) + +start-graph-worker: + @( cd services/graph ; npm i ; npm run start:worker:watch ) diff --git a/README.md b/README.md index c9be0828..19fb77f1 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,13 @@ Clone: To run all Gateway services, execute the following command: ```sh - docker compose docker-compose.all.yaml + ./start-gateway.sh +``` + +To stop the Gateway services, execute the following command: + +```sh + ./stop-gateway.sh ``` To build the Gateway Documentation: diff --git a/docker-compose-k6.account.yaml b/docker-compose-k6.account.yaml new file mode 100644 index 00000000..f935d6c6 --- /dev/null +++ b/docker-compose-k6.account.yaml @@ -0,0 +1,71 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json + +services: + account-service-api: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.account + command: ['api'] + ports: + - 3000:3000 + volumes: !reset [] + depends_on: !override + redis: + condition: service_started + ipfs: + condition: service_healthy + account-service-webhook: + condition: service_healthy + environment: + PROVIDER_BASE_URL: 'http://account-service-webhook:3001/webhooks/account-service' + WEBHOOK_BASE_URL: 'http://account-service-webhook:3001/webhooks' + + account-service-worker: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.account + command: ['worker'] + volumes: !reset [] + depends_on: !override + - redis + - ipfs + environment: + PROVIDER_BASE_URL: 'http://account-service-webhook:3001/webhooks/account-service' + WEBHOOK_BASE_URL: 'http://account-service-webhook:3001/webhooks' + + account-service-webhook: + image: rust:1.80.0 + volumes: + - ./services/account/rust-webhook-server:/app + ports: + - '3001:3001' + command: sh -c "cd /app && cargo run" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://127.0.0.1:3001/webhooks/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - amplica-gateway + + gateway-base: + profiles: + - skip + content-publishing-service-worker: + profiles: + - skip + content-publishing-service-api: + profiles: + - skip + content-watcher-service: + profiles: + - skip + graph-service-api: + profiles: + - skip + graph-service-worker: + profiles: + - skip diff --git a/docker-compose-k6.content-publishing.yaml b/docker-compose-k6.content-publishing.yaml new file mode 100644 index 00000000..233eebd5 --- /dev/null +++ b/docker-compose-k6.content-publishing.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json + +services: + content-publishing-service-api: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.content-publishing + command: ['api'] + ports: + - 3000:3000 + volumes: !reset [] + depends_on: !override + - redis + - ipfs + + content-publishing-service-worker: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.content-publishing + command: ['worker'] + volumes: !reset [] + depends_on: !override + - redis + - ipfs + + gateway-base: + profiles: + - skip + account-service-worker: + profiles: + - skip + account-service-api: + profiles: + - skip + content-watcher-service: + profiles: + - skip + graph-service-api: + profiles: + - skip + graph-service-worker: + profiles: + - skip diff --git a/docker-compose-k6.content-watcher.yaml b/docker-compose-k6.content-watcher.yaml new file mode 100644 index 00000000..aa4cf5de --- /dev/null +++ b/docker-compose-k6.content-watcher.yaml @@ -0,0 +1,36 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json + +services: + content-watcher-service: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.content-watcher + ports: + - 3000:3000 + volumes: !reset [] + depends_on: !override + - redis + - ipfs + + gateway-base: + profiles: + - skip + content-publishing-service-worker: + profiles: + - skip + content-publishing-service-api: + profiles: + - skip + account-service-worker: + profiles: + - skip + account-service-api: + profiles: + - skip + graph-service-api: + profiles: + - skip + graph-service-worker: + profiles: + - skip diff --git a/docker-compose-k6.graph.yaml b/docker-compose-k6.graph.yaml new file mode 100644 index 00000000..8fabf202 --- /dev/null +++ b/docker-compose-k6.graph.yaml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json + +services: + graph-service-api: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.graph + command: ['api'] + ports: + - 3000:3000 + volumes: !reset [] + depends_on: !override + - redis + + graph-service-worker: + pull_policy: never + build: + context: . + dockerfile: ./Docker/Dockerfile.graph + command: ['worker'] + volumes: !reset [] + depends_on: !override + - redis + + gateway-base: + profiles: + - skip + content-publishing-service-worker: + profiles: + - skip + content-publishing-service-api: + profiles: + - skip + content-watcher-service: + profiles: + - skip + account-service-api: + profiles: + - skip + account-service-worker: + profiles: + - skip diff --git a/docker-compose.yaml b/docker-compose.yaml index d01d70b5..107e8fa4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,3 +1,50 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json +x-common-environment: &common-environment + FREQUENCY_URL: ${FREQUENCY_URL:-ws://frequency:9944} + FREQUENCY_HTTP_URL: ${FREQUENCY_HTTP_URL:-http://localhost:9944} + REDIS_URL: 'redis://redis:6379' + PROVIDER_ID: ${PROVIDER_ID:-1} + PROVIDER_ACCOUNT_SEED_PHRASE: ${PROVIDER_ACCOUNT_SEED_PHRASE:-//Alice} + WEBHOOK_BASE_URL: 'http://social-app-template-backend:3001/webhooks' + WEBHOOK_FAILURE_THRESHOLD: 3 + WEBHOOK_RETRY_INTERVAL_SECONDS: 10 + HEALTH_CHECK_MAX_RETRIES: 4 + HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS: 10 + HEALTH_CHECK_SUCCESS_THRESHOLD: 10 + CAPACITY_LIMIT: '{"type":"percentage", "value":80}' + SIWF_URL: 'https://amplicalabs.github.io/siwf/ui' + SIWF_DOMAIN: 'localhost' + IPFS_ENDPOINT: ${IPFS_ENDPOINT:-http://ipfs:5001} + IPFS_GATEWAY_URL: ${IPFS_GATEWAY_URL:-https://ipfs.io/ipfs/[CID]} + IPFS_BASIC_AUTH_USER: ${IPFS_BASIC_AUTH_USER:-""} + IPFS_BASIC_AUTH_SECRET: ${IPFS_BASIC_AUTH_SECRET:-""} + QUEUE_HIGH_WATER: 1000 + CHAIN_ENVIRONMENT: dev + DEBUG: true + +x-content-publishing-env: &content-publishing-env + START_PROCESS: api + FILE_UPLOAD_MAX_SIZE_IN_BYTES: 2000000000 + ASSET_EXPIRATION_INTERVAL_SECONDS: 300 + BATCH_INTERVAL_SECONDS: 12 + BATCH_MAX_COUNT: 1000 + ASSET_UPLOAD_VERIFICATION_DELAY_SECONDS: 5 + +x-content-watcher-env: &content-watcher-env + STARTING_BLOCK: 759882 + BLOCKCHAIN_SCAN_INTERVAL_SECONDS: 6 + WEBHOOK_FAILURE_THRESHOLD: 4 + +x-graph-service-env: &graph-service-env + DEBOUNCE_SECONDS: 10 + GRAPH_ENVIRONMENT_TYPE: Mainnet + RECONNECTION_SERVICE_REQUIRED: false + +x-account-service-env: &account-service-env + BLOCKCHAIN_SCAN_INTERVAL_SECONDS: 1 + TRUST_UNFINALIZED_BLOCKS: true + PROVIDER_BASE_URL: 'http://social-app-template-backend:3001/webhooks/account-service' + services: redis: image: redis:latest @@ -9,15 +56,15 @@ services: - redis_data:/data/redis frequency: - image: dsnp/instant-seal-node-with-deployed-schemas:latest + image: frequencychain/standalone-node:v1.13.0-rc3 # We need to specify the platform because it's the only image # built by Frequency at the moment, and auto-pull won't work otherwise platform: linux/amd64 # Uncomment SEALING_MODE and SEALING_INTERVAL if you want to use interval sealing. # Other options you may want to add depending on your test scenario. - # environment: - # - SEALING_MODE=interval - # - SEALING_INTERVAL=3 + environment: + - SEALING_MODE=interval + - SEALING_INTERVAL=12 # - CREATE_EMPTY_BLOCKS=true # The 'command' may contain additional CLI options to the Frequency node, # such as: @@ -29,99 +76,137 @@ services: - amplica-gateway volumes: - chainstorage:/data + profiles: + - local-node - kubo_ipfs: + ipfs: image: ipfs/kubo:latest ports: - 4001:4001 - - 5001:5001 - - 8080:8080 + - 127.0.0.1:5001:5001 + - 127.0.0.1:8080:8080 networks: - amplica-gateway volumes: - - ipfs_data:/data/ipfs + - ipfs_data:${IPFS_VOLUME:-/data/ipfs} + + gateway-base: + pull_policy: never + image: gateway-dev:latest + build: + context: . + dockerfile: Docker/Dockerfile.dev + tags: + - gateway-dev:latest content-publishing-service-api: - image: amplicalabs/content-publishing-service:latest - platform: linux/amd64 + image: gateway-dev:latest ports: - - 3001:3000 - env_file: - - path: .env.common - required: true - - path: .env.content-publishing-service - required: false + - ${SERVICE_PORT_0:-3010}:3000 + command: make start-content-publishing-api environment: - - START_PROCESS=api + <<: [*common-environment, *content-publishing-env] + volumes: + - ./:/app + - content_publishing_node_cache:/app/services/content-publishing/node_modules depends_on: - redis - - frequency - - kubo_ipfs + - ipfs + - gateway-base networks: - amplica-gateway content-publishing-service-worker: - image: amplicalabs/content-publishing-service:latest - platform: linux/amd64 - env_file: - - path: .env.common - required: true - - path: .env.content-publishing-service - required: false + image: gateway-dev:latest + command: make start-content-publishing-worker environment: - - START_PROCESS=worker + <<: [*common-environment, *content-publishing-env] + volumes: + - ./:/app + - content_publishing_node_cache:/app/services/content-publishing/node_modules depends_on: - redis - - frequency - - kubo_ipfs + - ipfs + - gateway-base networks: - amplica-gateway - graph-service: - image: amplicalabs/graph-service:latest - platform: linux/amd64 + content-watcher-service: + image: gateway-dev:latest ports: - - 3002:3000 - env_file: - - path: .env.common - required: true - - path: .env.graph-service - required: false + - ${SERVICE_PORT_1:-3011}:3000 + command: make start-content-watcher + environment: + <<: [*common-environment, *content-watcher-env] + volumes: + - ./:/app + - content_watcher_node_cache:/app/services/content-watcher/node_modules depends_on: - redis - - frequency + - ipfs + - gateway-base + networks: + - amplica-gateway + + graph-service-api: + image: gateway-dev:latest + ports: + - ${SERVICE_PORT_2:-3012}:3000 + command: make start-graph-api + environment: + <<: [*common-environment, *graph-service-env] + volumes: + - ./:/app + - graph_node_cache:/app/services/graph/node_modules + depends_on: + - redis + - gateway-base + networks: + - amplica-gateway + + graph-service-worker: + image: gateway-dev:latest + command: make start-graph-worker + environment: + <<: [*common-environment, *graph-service-env] + volumes: + - ./:/app + - graph_node_cache:/app/services/graph/node_modules + depends_on: + - redis + - gateway-base networks: - amplica-gateway account-service-api: - image: amplicalabs/account-service:latest - platform: linux/amd64 + image: gateway-dev:latest ports: - - 3003:3000 - command: api - env_file: - - path: .env.common - required: true - - path: .env.account-service - required: false + - ${SERVICE_PORT_3:-3013}:3000 + environment: + <<: [*common-environment, *account-service-env] + command: make start-account-api + volumes: + - ./:/app + - account_node_cache:/app/services/account/node_modules depends_on: - redis - - frequency + - ipfs + - gateway-base networks: - amplica-gateway account-service-worker: - image: amplicalabs/account-service:latest - platform: linux/amd64 - command: worker - env_file: - - path: .env.common - required: true - - path: .env.account-service - required: false + image: gateway-dev:latest + command: make start-account-worker + environment: + <<: [*common-environment, *account-service-env] + volumes: + - ./:/app + - account_node_cache:/app/services/account/node_modules depends_on: - redis - - frequency + - ipfs + - gateway-base networks: - amplica-gateway @@ -131,6 +216,10 @@ volumes: chainstorage: external: false redis_data: + account_node_cache: + graph_node_cache: + content_publishing_node_cache: + content_watcher_node_cache: networks: amplica-gateway: diff --git a/environment/env.account-service.template b/environment/env.account-service.template deleted file mode 100644 index 2c662ff4..00000000 --- a/environment/env.account-service.template +++ /dev/null @@ -1,38 +0,0 @@ -# Copy this file to "/.env.account-service", and then tweak values for local development -# Values in this file will override the same-named environnent variables in `.env.common.docker` for the account-service - -# Base URL for provider webhook endpoints -# PROVIDER_BASE_URL=https://some-provider/api/v1.0.0 - -# An optional bearer token authentication to the provider webhook -# PROVIDER_ACCESS_TOKEN=some-token - -# Number of failures allowing in the provider webhook before the service is marked down -# WEBHOOK_FAILURE_THRESHOLD=3 - -# Number of seconds between provider webhook retry attempts when failing -# WEBHOOK_RETRY_INTERVAL_SECONDS=10 - -# Number of `/health` endpoint failures allowed before marking the provider webhook service down -# HEALTH_CHECK_MAX_RETRIES=4 - -# Number of seconds to retry provider webhook `/health` endpoint when failing -# HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS=10 - -# Minimum number of consecutive successful calls to the provider webhook -# `/health` endpoint before it is marked up again -# HEALTH_CHECK_SUCCESS_THRESHOLD=10 - -# Maximum amount of provider capacity this app is allowed to use (per epoch) -# type: 'percentage' | 'amount' -# value: number (may be percentage, ie '80', or absolute amount of capacity) -# CAPACITY_LIMIT='{"type":"percentage", "value":80}' - -# URL for the Sign-In With Frequency UI -# SIWF_URL=https://amplicalabs.github.io/siwf/ui - -# Domain for the Sign-in with Frequency login payload -# SIWF_DOMAIN=localhost - -# Enable debug mode for development -# DEBUG=true diff --git a/environment/env.common.template b/environment/env.common.template deleted file mode 100644 index 138687c9..00000000 --- a/environment/env.common.template +++ /dev/null @@ -1,49 +0,0 @@ -# Copy this file to ".env.dev" and ".env.docker.dev", and then tweak values for local development - -# Blockchain node address -FREQUENCY_URL=ws://0.0.0.0:9944 - -# Specifies the provider ID -PROVIDER_ID=1 - -# Base URL for provider webhook endpoints -PROVIDER_BASE_URL=https://some-provider/api/v1.0.0 - -# Redis URL -REDIS_URL=redis://0.0.0.0:6379 - -# An optional bearer token authentication to the provider webhook -PROVIDER_ACCESS_TOKEN=some-token - -# Seed phrase for provider MSA control key -PROVIDER_ACCOUNT_SEED_PHRASE='come finish flower cinnamon blame year glad tank domain hunt release fatigue' - -# Number of failures allowing in the provider webhook before the service is marked down -WEBHOOK_FAILURE_THRESHOLD=3 - -# Number of seconds between provider webhook retry attempts when failing -WEBHOOK_RETRY_INTERVAL_SECONDS=10 - -# Number of `/health` endpoint failures allowed before marking the provider webhook service down -HEALTH_CHECK_MAX_RETRIES=4 - -# Number of seconds to retry provider webhook `/health` endpoint when failing -HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS=10 - -# Minimum number of consecutive successful calls to the provider webhook -# `/health` endpoint before it is marked up again -HEALTH_CHECK_SUCCESS_THRESHOLD=10 - -# Maximum amount of provider capacity this app is allowed to use (per epoch) -# type: 'percentage' | 'amount' -# value: number (may be percentage, ie '80', or absolute amount of capacity) -CAPACITY_LIMIT='{"type":"percentage", "value":80}' - -# URL for the Sign-In With Frequency UI -SIWF_URL=https://amplicalabs.github.io/siwf/ui - -# Domain for the Sign-in with Frequency login payload -SIWF_DOMAIN=localhost - -# Enable debug mode for development -DEBUG=false diff --git a/environment/env.content-publishing-service.template b/environment/env.content-publishing-service.template deleted file mode 100644 index 6ab4d47b..00000000 --- a/environment/env.content-publishing-service.template +++ /dev/null @@ -1,42 +0,0 @@ -# Copy this file to "/.env.content-publishing-service", and then tweak values for local development -# Values in this file will override the same-named environnent variables in `.env.common.docker` for the content-publishing-service - -# URL to IPFS endpoint -# IPFS_ENDPOINT="https://ipfs.infura.io:5001" -IPFS_ENDPOINT="http://kubo_ipfs:5001" - -# If using Infura, put Project ID here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_USER= - -# If using Infura, put auth token here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_SECRET= - -# IPFS gateway URL. '[CID]' is a token that will be replaced with an actual content ID -# IPFS_GATEWAY_URL="https://ipfs.io/ipfs/[CID]" -IPFS_GATEWAY_URL="http://kubo_ipfs:8080/ipfs/[CID]" - -CAPACITY_LIMIT='{"type":"percentage", "value":80}' - -# Environment for mapping announcement type to schema ID (use 'dev' for e2e tests) -# Possible values: dev, rococo, testnet, mainnet -CHAIN_ENVIRONMENT=dev - -# Max file size allowed for asset upload -FILE_UPLOAD_MAX_SIZE_IN_BYTES=2000000000 - -# Number of seconds to keep completed asset entrie in the cache -# before expiring them -ASSET_EXPIRATION_INTERVAL_SECONDS=300 - -# Number of seconds between content publishing batches. This is so that -# the service waits a reasonable amount of time for additional content to publishing -# before submitting a batch--it represents a trade-off between maximum batch fullness -# and minimal wait time for published content. -BATCH_INTERVAL_SECONDS=12 - -# Maximum number of items that can be submitted in a single batch -BATCH_MAX_COUNT=1000 - -# Base delay in seconds used for exponential backoff while waiting for -# uploaded assets to be verified available before publishing a content notice. -ASSET_UPLOAD_VERIFICATION_DELAY_SECONDS=5 diff --git a/environment/env.content-watcher-service.template b/environment/env.content-watcher-service.template deleted file mode 100644 index 8ecb729d..00000000 --- a/environment/env.content-watcher-service.template +++ /dev/null @@ -1,33 +0,0 @@ -# Copy this file to "/.env.content-watcher-service", and then tweak values for local development -# Values in this file will override the same-named environnent variables in `.env.common.docker` for the content-watcher-service - -# URL to IPFS endpoint -# IPFS_ENDPOINT="https://ipfs.infura.io:5001" -IPFS_ENDPOINT="http://kubo_ipfs:5001" - -# If using Infura, put Project ID here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_USER= - -# If using Infura, put auth token here, or leave blank for Kubo RPC -IPFS_BASIC_AUTH_SECRET= - -# IPFS gateway URL. '[CID]' is a token that will be replaced with an actual content ID -# IPFS_GATEWAY_URL="https://ipfs.io/ipfs/[CID]" -IPFS_GATEWAY_URL="http://kubo_ipfs:8080/ipfs/[CID]" - -# Block number from which the service will start scanning the chain -STARTING_BLOCK=1 - -# How many minutes to delay between successive scans of the chain -# for new accounts (after end of chain is reached) -BLOCKCHAIN_SCAN_INTERVAL_MINUTES=1 - -# Max number of jobs allowed on the queue before -# blockchain scan will be paused to allow queue to drain -QUEUE_HIGH_WATER=1000 - -# Number of retry attempts if a registered webhook call fails -WEBHOOK_FAILURE_THRESHOLD=4 - -# Number of seconds between webhook retry attempts when failing -WEBHOOK_RETRY_INTERVAL_SECONDS=10 diff --git a/environment/env.graph-service.template b/environment/env.graph-service.template deleted file mode 100644 index 60879ccb..00000000 --- a/environment/env.graph-service.template +++ /dev/null @@ -1,85 +0,0 @@ -# Copy this file to "/.env.graph-service", and then tweak values for local development -# Values in this file will override the same-named environnent variables in `.env.common.docker` for the graph-service - -# Max number of jobs allowed on the 'graphUpdateQueue' before -# blockchain scan will be paused to allow queue to drain -QUEUE_HIGH_WATER=1000 - -# Number of seconds to retain pending graph updates in the Redis cache to avoid redundant fetches from the chain -DEBOUNCE_SECONDS=10 - -# Maximum amount of provider capacity this app is allowed to use (per epoch) -# type: 'percentage' | 'amount' -# value: number (may be percentage, ie '80', or absolute amount of capacity) -CAPACITY_LIMIT='{"type":"percentage", "value":80}' - -# Graph environment type. This can be 'Dev' or 'Rococo', 'TestnetPaseo', or 'Mainnet'. -GRAPH_ENVIRONMENT_TYPE=Dev - -# [NOTE]: The following config is only used for Dev environments. -# Add the graph environment config in JSON format only used for Dev environments. -# Be careful to escape any inner quotes as this is in a .env file. -GRAPH_ENVIRONMENT_DEV_CONFIG='{ - "sdkMaxStaleFriendshipDays": 100, - "maxPageId": 100, - "dsnpVersions": [ - "1.0" - ], - "maxGraphPageSizeBytes": 100, - "maxKeyPageSizeBytes": 100, - "schemaMap": { - "1": { - "dsnpVersion": "1.0", - "connectionType": "follow", - "privacyType": "public" - }, - "2": { - "dsnpVersion": "1.0", - "connectionType": "follow", - "privacyType": "private" - }, - "3": { - "dsnpVersion": "1.0", - "connectionType": "friendship", - "privacyType": "private" - } - }, - "graphPublicKeySchemaId": 4 - } - ' -PROVIDER_ACCOUNT_SEED_PHRASE="//Ferdie" - -# Whether to instantiate/activate reconnection-service features -RECONNECTION_SERVICE_REQUIRED=false - -### The following are only applicable if RECONNECTION_SERVICE_REQUIRED is 'true' - -# Base URL for provider webhook endpoints -# PROVIDER_BASE_URL=https://some-provider/api/v1.0.0 - -# An optional bearer token authentication to the provider webhook -# PROVIDER_ACCESS_TOKEN=some-token - -# Number of connection/page to request when requesting provider connections from webhook -C# ONNECTIONS_PER_PROVIDER_RESPONSE_PAGE=100 - - -# How many minutes to delay between successive scans of the chain -# for new accounts (after end of chain is reached) -# BLOCKCHAIN_SCAN_INTERVAL_MINUTES=1 - -# Number of failures allowing in the provider webhook before the service is marked down -# WEBHOOK_FAILURE_THRESHOLD=3 - -# Minimum number of consecutive successful calls to the provider webhook -# `/health` endpoint before it is marked up again -# HEALTH_CHECK_SUCCESS_THRESHOLD=10 - -# Number of seconds between provider webhook retry attempts when failing -# WEBHOOK_RETRY_INTERVAL_SECONDS=10 - -# Number of seconds to retry provider webhook `/health` endpoint when failing -# HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS=10 - -# Number of `/health` endpoint failures allowed before marking the provider webhook service down -# HEALTH_CHECK_MAX_RETRIES=4 diff --git a/services/account/apps/api/src/controllers/v1/accounts-v1.controller.ts b/services/account/apps/api/src/controllers/v1/accounts-v1.controller.ts index 38950e6f..f0772780 100644 --- a/services/account/apps/api/src/controllers/v1/accounts-v1.controller.ts +++ b/services/account/apps/api/src/controllers/v1/accounts-v1.controller.ts @@ -40,12 +40,41 @@ export class AccountsControllerV1 { * @returns A promise that resolves to an Account object => {msaId, handle}. * @throws An error if the account cannot be found. */ - async getAccount(@Param('msaId') msaId: string): Promise { + async getAccountForMsa(@Param('msaId') msaId: string): Promise { try { this.logger.debug(`Received request to get account with msaId: ${msaId}`); - return await this.accountsService.getAccount(msaId); + const account = await this.accountsService.getAccount(msaId); + if (account) return account; + throw new HttpException('Failed to find the account', HttpStatus.NOT_FOUND); } catch (error) { this.logger.error(error); + if (error instanceof HttpException) throw error; + throw new HttpException('Failed to find the account', HttpStatus.BAD_REQUEST); + } + } + + @Get('account/:publicKey') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Fetch an account given a public key.' }) + @ApiOkResponse({ description: 'Found account', type: AccountResponse }) + /** + * Gets an account. + * @param queryParams - The query parameters for creating the account. + * @returns A promise that resolves to an Account object => {msaId, handle}. + * @throws An error if the msaId or account cannot be found. + */ + async getAccountForPublicKey(@Param('publicKey') publicKey: string): Promise { + try { + this.logger.debug(`Received request to get account with publicKey: ${publicKey}`); + const response = await this.accountsService.getMsaIdForPublicKey(publicKey); + if (response?.msaId) { + const account = await this.accountsService.getAccount(response.msaId); + if (account) return account; + } + throw new HttpException('Failed to find the account', HttpStatus.NOT_FOUND); + } catch (error) { + this.logger.error(error); + if (error instanceof HttpException) throw error; throw new HttpException('Failed to find the account', HttpStatus.BAD_REQUEST); } } @@ -58,7 +87,7 @@ export class AccountsControllerV1 { async postSignInWithFrequency(@Body() walletLoginRequest: WalletLoginRequestDto): Promise { try { this.logger.debug(`Received Sign-In With Frequency request: ${JSON.stringify(walletLoginRequest)}`); - return await this.accountsService.signInWithFrequency(walletLoginRequest); + return this.accountsService.signInWithFrequency(walletLoginRequest); } catch (error) { const errorMessage = 'Failed to Sign In With Frequency'; this.logger.error(`${errorMessage}: ${error}`); diff --git a/services/account/apps/api/src/metadata.ts b/services/account/apps/api/src/metadata.ts index 3e7f4e25..5f2d6760 100644 --- a/services/account/apps/api/src/metadata.ts +++ b/services/account/apps/api/src/metadata.ts @@ -4,13 +4,13 @@ export default async () => { ['../../../libs/common/src/types/dtos/wallet.login.request.dto']: await import( '../../../libs/common/src/types/dtos/wallet.login.request.dto' ), + ['../../../libs/common/src/types/dtos/accounts.response.dto']: await import( + '../../../libs/common/src/types/dtos/accounts.response.dto' + ), ['@polkadot/types-codec/primitive/U32']: await import('@polkadot/types-codec/primitive/U32'), ['../../../libs/common/src/types/dtos/wallet.login.config.response.dto']: await import( '../../../libs/common/src/types/dtos/wallet.login.config.response.dto' ), - ['../../../libs/common/src/types/dtos/accounts.response.dto']: await import( - '../../../libs/common/src/types/dtos/accounts.response.dto' - ), ['../../../libs/common/src/types/dtos/wallet.login.response.dto']: await import( '../../../libs/common/src/types/dtos/wallet.login.response.dto' ), @@ -91,6 +91,24 @@ export default async () => { }, }, ], + [ + import('../../../libs/common/src/types/dtos/accounts.response.dto'), + { + HandleResponseDTO: { + base_handle: { required: true, type: () => String }, + canonical_base: { required: true, type: () => String }, + suffix: { required: true, type: () => Number }, + }, + AccountResponse: { + msaId: { required: true, type: () => String }, + handle: { + required: false, + type: () => t['../../../libs/common/src/types/dtos/accounts.response.dto'].HandleResponseDTO, + }, + }, + MsaIdResponse: { msaId: { required: true, type: () => String } }, + }, + ], [ import('../../../libs/common/src/types/dtos/transaction.response.dto'), { TransactionResponse: { referenceId: { required: true, type: () => String } } }, @@ -105,15 +123,6 @@ export default async () => { }, }, ], - [ - import('../../../libs/common/src/types/dtos/accounts.response.dto'), - { - AccountResponse: { - msaId: { required: true, type: () => String }, - handle: { required: false, type: () => Object }, - }, - }, - ], [ import('../../../libs/common/src/types/dtos/wallet.login.config.response.dto'), { @@ -148,7 +157,12 @@ export default async () => { type: t['../../../libs/common/src/types/dtos/wallet.login.config.response.dto'] .WalletLoginConfigResponse, }, - getAccount: { type: t['../../../libs/common/src/types/dtos/accounts.response.dto'].AccountResponse }, + getAccountForMsa: { + type: t['../../../libs/common/src/types/dtos/accounts.response.dto'].AccountResponse, + }, + getAccountForPublicKey: { + type: t['../../../libs/common/src/types/dtos/accounts.response.dto'].AccountResponse, + }, postSignInWithFrequency: { type: t['../../../libs/common/src/types/dtos/wallet.login.response.dto'].WalletLoginResponse, }, @@ -175,7 +189,7 @@ export default async () => { changeHandle: { type: t['../../../libs/common/src/types/dtos/transaction.response.dto'].TransactionResponse, }, - getHandle: { type: Object }, + getHandle: { type: t['../../../libs/common/src/types/dtos/accounts.response.dto'].HandleResponseDTO }, }, }, ], diff --git a/services/account/apps/api/src/services/accounts.service.ts b/services/account/apps/api/src/services/accounts.service.ts index 40ad0490..c75bba3f 100644 --- a/services/account/apps/api/src/services/accounts.service.ts +++ b/services/account/apps/api/src/services/accounts.service.ts @@ -6,7 +6,7 @@ import { ConfigService } from '#lib/config/config.service'; import { EnqueueService } from '#lib/services/enqueue-request.service'; import { WalletLoginRequestDto, PublishSIWFSignupRequest } from '#lib/types/dtos/wallet.login.request.dto'; import { WalletLoginResponse } from '#lib/types/dtos/wallet.login.response.dto'; -import { AccountResponse } from '#lib/types/dtos/accounts.response.dto'; +import { AccountResponse, MsaIdResponse } from '#lib/types/dtos/accounts.response.dto'; import { WalletLoginConfigResponse } from '#lib/types/dtos/wallet.login.config.response.dto'; @Injectable() @@ -21,7 +21,7 @@ export class AccountsService { this.logger = new Logger(this.constructor.name); } - async getAccount(msaId: string): Promise { + async getAccount(msaId: string): Promise { try { const isValidMsaId = await this.blockchainService.isValidMsaId(msaId); if (isValidMsaId) { @@ -33,13 +33,29 @@ export class AccountsService { this.logger.log(`Failed to get handle for msaId: ${msaId}`); return { msaId }; } - throw new Error(`Invalid msaId: ${msaId}`); + this.logger.log(`Invalid msaId: ${msaId}`); + return null; } catch (e) { this.logger.error(`Error during get account request: ${e}`); throw new Error('Failed to get account'); } } + async getMsaIdForPublicKey(publicKey: string): Promise { + try { + const msaId = await this.blockchainService.publicKeyToMsaId(publicKey); + if (msaId) { + this.logger.debug(`Found msaId: ${msaId} for public key: ${publicKey}`); + return { msaId }; + } + this.logger.debug(`Did not find msaId for public key: ${publicKey}`); + return null; + } catch (e) { + this.logger.error(`Error during get msaId request: ${e}`); + throw new Error('Failed to get msaId'); + } + } + async getSIWFConfig(): Promise { let response: WalletLoginConfigResponse; try { diff --git a/services/account/dev.Dockerfile b/services/account/dev.Dockerfile deleted file mode 100644 index 80d919d3..00000000 --- a/services/account/dev.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM node:20 - -WORKDIR /app - -EXPOSE 3000 - -VOLUME [ "/app" ] - -ENTRYPOINT [ "./scripts/docker-entrypoint.sh", "watch" ] diff --git a/services/account/docker-compose.yaml b/services/account/docker-compose.yaml deleted file mode 100644 index e906a0f2..00000000 --- a/services/account/docker-compose.yaml +++ /dev/null @@ -1,106 +0,0 @@ -x-account-service-env: &account-service-env - API_PORT: 3000 - BLOCKCHAIN_SCAN_INTERVAL_SECONDS: 3 - TRUST_UNFINALIZED_BLOCKS: false - FREQUENCY_URL: 'ws://frequency:9944' - FREQUENCY_HTTP_URL: 'http://frequency:9944' - PROVIDER_ID: 1 - PROVIDER_BASE_URL: 'http://host.docker.internal:3001/webhooks/account-service' - REDIS_URL: 'redis://redis:6379' - PROVIDER_ACCESS_TOKEN: 'some-token' - PROVIDER_ACCOUNT_SEED_PHRASE: '//Alice' - WEBHOOK_FAILURE_THRESHOLD: 3 - WEBHOOK_RETRY_INTERVAL_SECONDS: 10 - HEALTH_CHECK_MAX_RETRIES: 4 - HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS: 10 - HEALTH_CHECK_SUCCESS_THRESHOLD: 10 - CAPACITY_LIMIT: '{"type":"percentage", "value":80}' - SIWF_URL: 'https://amplicalabs.github.io/siwf/ui' - SIWF_DOMAIN: localhost - DEBUG: false - -services: - redis: - image: redis:latest - ports: - - 6379:6379 - volumes: - - redis_data:/data/redis - networks: - - account-service - - frequency: - image: dsnp/instant-seal-node-with-deployed-schemas:latest - # We need to specify the platform because it's the only image - # built by Frequency at the moment, and auto-pull won't work otherwise - platform: linux/amd64 - # Uncomment SEALING_MODE and SEALING_INTERVAL if you want to use interval sealing. - # Other options you may want to add depending on your test scenario. - environment: - - SEALING_MODE=interval - - SEALING_INTERVAL=1 - # - CREATE_EMPTY_BLOCKS=true - # Uncomment below if you want to let the chain run and keep all of the historical blocks - # command: --state-pruning=archive - command: --offchain-worker=always --enable-offchain-indexing=true - ports: - - 9944:9944 - networks: - - account-service - volumes: - - chainstorage:/data - - account-service-base: - pull_policy: never - image: account-service:latest - environment: - <<: *account-service-env - build: - context: . - dockerfile: dev.Dockerfile - tags: - - account-service:latest - volumes: - - ./:/app - - api: - pull_policy: never - image: account-service:latest - environment: - <<: *account-service-env - command: ['api'] - ports: - - 3000:3000 - volumes: - - ./:/app - depends_on: - - account-service-base - - redis - - frequency - networks: - - account-service - restart: on-failure - - worker: - pull_policy: never - image: account-service:latest - environment: - <<: *account-service-env - command: ['worker'] - volumes: - - ./:/app - depends_on: - - account-service-base - - redis - - frequency - networks: - - account-service - restart: on-failure - -volumes: - redis_data: - chainstorage: - external: false - -networks: - account-service: diff --git a/services/account/docs/account_service_arch.drawio b/services/account/docs/account_service_arch.drawio index 318fc2e9..9fc05541 100644 --- a/services/account/docs/account_service_arch.drawio +++ b/services/account/docs/account_service_arch.drawio @@ -10,7 +10,7 @@ - + diff --git a/services/account/docs/index.html b/services/account/docs/index.html new file mode 100644 index 00000000..f66aa172 --- /dev/null +++ b/services/account/docs/index.html @@ -0,0 +1,400 @@ + + + + + + Account Service + + + + + + + + + +

Account Service (1.0)

Download OpenAPI specification:Download

Account Service API

+

v1/accounts

Get the Sign-In With Frequency Configuration

Responses

Response samples

Content type
application/json
{
  • "providerId": "string",
  • "siwfUrl": "string",
  • "frequencyRpcUrl": "string"
}

Request to sign in with Frequency

Request Body schema: application/json
required
object

The wallet login request information

+
object (SignUpResponseDto)

Responses

Request samples

Content type
application/json
{
  • "signIn": {
    },
  • "signUp": {
    }
}

Response samples

Content type
application/json
{
  • "referenceId": "string",
  • "msaId": "string",
  • "publicKey": "string"
}

Fetch an account given an msaId.

path Parameters
msaId
required
string

Responses

Response samples

Content type
application/json
{
  • "msaId": "string",
  • "handle": {
    }
}

Fetch an account given a public key.

path Parameters
publicKey
required
string

Responses

Response samples

Content type
application/json
{
  • "msaId": "string",
  • "handle": {
    }
}

v1/delegation

Get the delegation information associated with an msaId.

path Parameters
msaId
required
string

Responses

v1/handles

Request to create a new handle for an account

Request Body schema: application/json
required
accountId
required
string
required
object (HandlePayload)
proof
required
string

Responses

Request samples

Content type
application/json
{
  • "accountId": "string",
  • "payload": {
    },
  • "proof": "string"
}

Request to change a handle

Request Body schema: application/json
required
accountId
required
string
required
object (HandlePayload)
proof
required
string

Responses

Request samples

Content type
application/json
{
  • "accountId": "string",
  • "payload": {
    },
  • "proof": "string"
}

Fetch a handle given an msaId.

path Parameters
msaId
required
string

Responses

v1/keys

add new control keys for an MSA ID

Request Body schema: application/json
required
msaOwnerAddress
required
string
msaOwnerSignature
required
string
newKeyOwnerSignature
required
string
required
object (KeysRequestPayload)

Responses

Request samples

Content type
application/json
{
  • "msaOwnerAddress": "string",
  • "msaOwnerSignature": "string",
  • "newKeyOwnerSignature": "string",
  • "payload": {
    }
}

Fetch public keys given an msaId.

path Parameters
msaId
required
string

Responses

health

Check the health status of the service

Responses

Check the live status of the service

Responses

Check the ready status of the service

Responses

+ + + + diff --git a/services/account/k6-test/README.md b/services/account/k6-test/README.md index 72e4023f..c3792a97 100644 --- a/services/account/k6-test/README.md +++ b/services/account/k6-test/README.md @@ -1,7 +1,7 @@ # Generated k6 script -The `account-service-load.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. -The `health-check.js` file contains a simple health check script that can be used to check the health of the service. +The `account-service-load.k6.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. +The `health-check.k6.js` file contains a simple health check script that can be used to check the health of the service. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. @@ -22,5 +22,11 @@ To run the script, you need to have k6 installed. You can download it from [here To run the script, execute the following command: ```bash -k6 run health-check.js +k6 run health-check.k6.js ``` + +## Generating Test Data + +So that k6 doesn't have to interact with the chain, we can generate data sepatately and then use it for the tests. + +`npm run generate:signup` for example will re-generate the `signups.gen.js` file with 100 generated valid signup payloads. diff --git a/services/account/k6-test/account-service-load.js b/services/account/k6-test/account-service-load.k6.skip.js similarity index 74% rename from services/account/k6-test/account-service-load.js rename to services/account/k6-test/account-service-load.k6.skip.js index a719f742..c484afcb 100644 --- a/services/account/k6-test/account-service-load.js +++ b/services/account/k6-test/account-service-load.k6.skip.js @@ -36,63 +36,7 @@ const SLEEP_DURATION = 0.1; // Global variables should be initialized. export default function () { - group('health', () => { - // Request No. 1: ApiController_health - // eslint-disable-next-line no-lone-blocks - { - const url = `${BASE_URL}/healthz`; - const request = http.get(url); - - check(request, { - 'Service is healthy': (r) => r.status === 200, - }); - } - }); - - group('/v1/accounts/siwf', () => { - // Request No. 1: AccountsController_getSIWFConfig - { - const url = `${BASE_URL}/v1/accounts/siwf`; - const request = http.get(url); - - check(request, { - 'Returned SIWF Configuration data': (r) => r.status === 200, - }); - - sleep(SLEEP_DURATION); - } - - // Request No. 2: AccountsController_postSignInWithFrequency - { - const url = `${BASE_URL}/v1/accounts/siwf`; - // Use the SIWF sample Sign Up request body for a new user. - const body = { - signUp: { - extrinsics: [ - { - pallet: 'msa', - extrinsicName: 'createSponsoredAccountWithDelegation', - encodedExtrinsic: - '0xed01043c01b01b4dcafc8a8e73bff98e7558249f53cd0e0e64fa6b8f0159f0913d4874d9360176644186458bad3b00bbd0ac21e6c9bd5a8bed9ced7a772d11a9aac025b47f6559468808e272696f596a02af230951861027c0dc30f7163ecf316838a0723483010000000000000014000000000000000000004d000000', - }, - { - pallet: 'handles', - extrinsicName: 'claimHandle', - encodedExtrinsic: - '0xb901044200b01b4dcafc8a8e73bff98e7558249f53cd0e0e64fa6b8f0159f0913d4874d93601225508ae2da9804c60660a150277eb32b2a0f6b9c8f6e07dd6cad799cb31ae1dfb43896f488e9c0b7ec8b530d930b3f9b690683f2765d5def3fee3fc6540d58714656e6464794d000000', - }, - ], - }, - }; - const params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } }; - const request = http.post(url, JSON.stringify(body), params); - - check(request, { - 'Signed in successfully': (r) => r.status === 201, - }); - } - }); - + // Needs 255 generated accounts group('/v1/accounts/{msaId}', () => { const msaId = randomIntBetween(2, 256); @@ -107,6 +51,7 @@ export default function () { } }); + // Needs 255 generated accounts group('/v1/delegation/{msaId}', () => { const msaId = randomIntBetween(2, 256); @@ -121,6 +66,7 @@ export default function () { } }); + // Needs generation group('/v1/keys/add', () => { // Request No. 1: KeysController_addKey // eslint-disable-next-line no-lone-blocks @@ -152,6 +98,7 @@ export default function () { }); }); + // Needs 255 generated accounts with KEYS group('/v1/keys/{msaId}', () => { const msaId = randomIntBetween(2, 256); @@ -166,6 +113,7 @@ export default function () { } }); + // Needs generation group('/v1/handles', () => { // Request No. 1: HandlesController_createHandle // eslint-disable-next-line no-lone-blocks @@ -196,6 +144,7 @@ export default function () { } }); + // Needs generation group('/v1/handles/change', () => { // Request No. 1: HandlesController_changeHandle // eslint-disable-next-line no-lone-blocks @@ -226,6 +175,7 @@ export default function () { } }); + // Needs 1 generated accounts group('/v1/handles/{msaId}', () => { const msaId = '2'; diff --git a/services/account/k6-test/generate/helpers/chain.mjs b/services/account/k6-test/generate/helpers/chain.mjs new file mode 100644 index 00000000..39931215 --- /dev/null +++ b/services/account/k6-test/generate/helpers/chain.mjs @@ -0,0 +1,26 @@ +// Only way to silence PolkadotJS API warnings we don't want +console.warn = () => {}; + +import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; +import { mnemonicGenerate } from '@polkadot/util-crypto'; +import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; + +export async function getApi() { + const api = await ApiPromise.create({ + // Using mainnet as: 1. Nothing here would use local only metadata, 2. We aren't actually submitting. + provider: new WsProvider('wss://0.rpc.frequency.xyz'), + }); + await api.isReady; + return api; +} + +export function createKey() { + const mnemonic = mnemonicGenerate(); + const keyring = new Keyring({ type: 'sr25519' }); + const keypair = keyring.createFromUri(mnemonic); + return keypair; +} + +export function signPayloadSr25519(keys, data) { + return { Sr25519: u8aToHex(keys.sign(u8aWrapBytes(data.toU8a()))) }; +} diff --git a/services/account/k6-test/generate/signups.mjs b/services/account/k6-test/generate/signups.mjs new file mode 100644 index 00000000..d02ecdea --- /dev/null +++ b/services/account/k6-test/generate/signups.mjs @@ -0,0 +1,69 @@ +import fs from 'fs/promises'; +import { Bytes } from '@polkadot/types'; +import { getApi, createKey, signPayloadSr25519 } from './helpers/chain.mjs'; + +const GENERATE_COUNT = 10; + +const signup = (api) => { + const keypair = createKey(); + const expiration = 100; // Will this fail when running against longer chains? + + const addProviderData = api.registry.createType('PalletMsaAddProvider', { + authorizedMsaId: 1, + schemaIds: [1], + expiration, + }); + const createTx = api.tx.msa.createSponsoredAccountWithDelegation( + keypair.address, + signPayloadSr25519(keypair, addProviderData), + addProviderData, + ); + const claimHandlePayload = api.registry.createType('CommonPrimitivesHandlesClaimHandlePayload', { + baseHandle: new Bytes(api.registry, keypair.address.substring(0, 8)), + expiration, + }); + const claimTx = api.tx.handles.claimHandle( + keypair.address, + signPayloadSr25519(keypair, claimHandlePayload), + claimHandlePayload, + ); + + return { + signUp: { + extrinsics: [ + { + pallet: 'msa', + extrinsicName: 'createSponsoredAccountWithDelegation', + encodedExtrinsic: createTx.toHex(), + }, + { + pallet: 'handles', + extrinsicName: 'claimHandle', + encodedExtrinsic: claimTx.toHex(), + }, + ], + }, + }; +}; + +const main = async () => { + const api = await getApi(); + const generated = []; + for (let i = 0; i < GENERATE_COUNT; i++) { + generated.push(signup(api)); + } + // Write the generated array to './signups.gen.js' + const fileContent = ` + // GENERATED FILE! Regenerate using npm run generate:signups + export const signups = ${JSON.stringify(generated, null, 2)}; + `; + + try { + await fs.writeFile('./signups.gen.js', fileContent, 'utf8'); + console.log('Successfully wrote to ./signups.gen.js'); + } catch (err) { + console.error('Error writing to file:', err); + } +}; + +main().catch(console.error).finally(process.exit); diff --git a/services/account/k6-test/health-check.js b/services/account/k6-test/health-check.k6.js similarity index 95% rename from services/account/k6-test/health-check.js rename to services/account/k6-test/health-check.k6.js index cacce951..c7eb063b 100644 --- a/services/account/k6-test/health-check.js +++ b/services/account/k6-test/health-check.k6.js @@ -31,7 +31,7 @@ const SLEEP_DURATION = 0.1; export default function () { // Request No. 1: ApiController_health // eslint-disable-next-line no-lone-blocks - const url = `${BASE_URL}/api/health`; + const url = `${BASE_URL}/healthz`; const request = http.get(url); check(request, { diff --git a/services/account/k6-test/new-sign-up.js b/services/account/k6-test/new-sign-up.k6.js similarity index 65% rename from services/account/k6-test/new-sign-up.js rename to services/account/k6-test/new-sign-up.k6.js index b5414b48..20c2e575 100644 --- a/services/account/k6-test/new-sign-up.js +++ b/services/account/k6-test/new-sign-up.k6.js @@ -2,7 +2,8 @@ /* eslint-disable func-names */ /* * Account Service - * Account Service API + * Account Service API: New Sign Ups + * NOTE: This test MUST run on a clean chain and cannot be re-run without running: npm run generate:signups * * OpenAPI spec version: 1.0 * @@ -14,6 +15,8 @@ import http from 'k6/http'; import { group, check, sleep } from 'k6'; +import exec from 'k6/execution'; +import { signups } from './signups.gen.js'; export const options = { // scenarios: { @@ -43,38 +46,39 @@ const CALLBACK_URL = 'http://localhost:3001/webhooks/account-service'; const BASE_URL = 'http://localhost:3000'; // Sleep duration between successive requests. const SLEEP_DURATION = 0.1; -const BLOCKTIME_SECONDS = 12; +const BLOCKTIME_SECONDS = 13; // Add 1 second for additional loop buffer // Global variables should be initialized. -function checkCallback() { +function checkCallback(referenceId) { const res = http.get(CALLBACK_URL); console.log('Callback response:', res.status, res.body); check(res, { 'callback received': (r) => r.status === 201, 'callback contains expected data': (r) => { const json = JSON.parse(r.body); - return json.referenceId === 'DekKx1F_WYOWCQFXE3vgwqXP4U4'; + return json.referenceId === referenceId; }, }); } -export function setup() { +export async function setup() { // Let's make sure the service is healthy before starting the test. console.log('Checking service health...'); - const res = http.get(`${BASE_URL}/api/health`); + const res = http.get(`${BASE_URL}/healthz`); console.log('Service health check status:', res.status); if (res.status !== 200) { console.error('Service is not healthy! Terminating test...'); return false; } - return true; + + return { signUpBody: signups[exec.vu.iterationInInstance] }; } -export default function () { - group('/accounts/siwf', () => { +export default function (setupData) { + group('/v1/accounts/siwf', () => { // Request No. 1: AccountsController_getSIWFConfig { - const url = `${BASE_URL}/accounts/siwf`; + const url = `${BASE_URL}/v1/accounts/siwf`; const request = http.get(url); check(request, { @@ -85,39 +89,25 @@ export default function () { } // Request No. 2: AccountsController_postSignInWithFrequency + let referenceId; { - const url = `${BASE_URL}/accounts/siwf`; + const url = `${BASE_URL}/v1/accounts/siwf`; // Use the SIWF sample Sign Up request body for a new user. - const body = { - signUp: { - extrinsics: [ - { - pallet: 'msa', - extrinsicName: 'createSponsoredAccountWithDelegation', - encodedExtrinsic: - '0xed01043c01b01b4dcafc8a8e73bff98e7558249f53cd0e0e64fa6b8f0159f0913d4874d9360176644186458bad3b00bbd0ac21e6c9bd5a8bed9ced7a772d11a9aac025b47f6559468808e272696f596a02af230951861027c0dc30f7163ecf316838a0723483010000000000000014000000000000000000004d000000', - }, - { - pallet: 'handles', - extrinsicName: 'claimHandle', - encodedExtrinsic: - '0xb901044200b01b4dcafc8a8e73bff98e7558249f53cd0e0e64fa6b8f0159f0913d4874d93601225508ae2da9804c60660a150277eb32b2a0f6b9c8f6e07dd6cad799cb31ae1dfb43896f488e9c0b7ec8b530d930b3f9b690683f2765d5def3fee3fc6540d58714656e6464794d000000', - }, - ], - }, - }; + const body = setupData.signUpBody; const params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } }; const request = http.post(url, JSON.stringify(body), params); check(request, { 'Signed in successfully': (r) => r.status === 201, + 'Has referenceId': (r) => !!JSON.parse(r.body).referenceId, }); + referenceId = JSON.parse(request.body).referenceId; } // The front end will poll the server for the account status. // We'll wait here for a block to be finalized. sleep(BLOCKTIME_SECONDS); - console.log('Block finalized. Checking callback...'); - checkCallback(); + console.log('Block finalized. Checking callback...', { referenceId }); + checkCallback(referenceId); }); } diff --git a/services/account/k6-test/package-lock.json b/services/account/k6-test/package-lock.json index c41fefa7..b8f75166 100644 --- a/services/account/k6-test/package-lock.json +++ b/services/account/k6-test/package-lock.json @@ -7,16 +7,857 @@ "": { "name": "k6-scripts", "version": "1.0.0", - "license": "ISC", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "^12.3.1", + "@polkadot/types": "^12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2" + }, "devDependencies": { "@types/k6": "^0.51.0" } }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz", + "integrity": "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA==", + "optional": true + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1.tgz", + "integrity": "sha512-gmVDUP8LpCH0BXewbzqXF2sdHddq1H1q+XrAW2of+KZj4woQkIGBRGTJHeBEVHe30EB+UejR1N2dT4PO/RvDdg==", + "optional": true + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1.tgz", + "integrity": "sha512-GCI78BHDzXAF/L2pZD6Aod/yl82adqQ7ftNmKg51ixRL02JpWUA+SpUKTJE5MY1p8kiJJIo09P2um24SiJHxNA==", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.0.1", + "@polkadot-api/utils": "0.0.1" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.1.0.tgz", + "integrity": "sha512-GBCGDRztKorTLna/unjl/9SWZcRmvV58o9jwU2Y038VuPXZcr01jcw/1O3x+yeAuwyGzbucI/mLTDa1QoEml3A==", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.0.1", + "@polkadot-api/substrate-bindings": "0.0.1", + "@polkadot-api/substrate-client": "0.0.1", + "@polkadot-api/utils": "0.0.1" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1.tgz", + "integrity": "sha512-bAe7a5bOPnuFVmpv7y4BBMRpNTnMmE0jtTqRUw/+D8ZlEHNVEJQGr4wu3QQCl7k1GnSV1wfv3mzIbYjErEBocg==", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.0.1", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.0.1.tgz", + "integrity": "sha512-9Bg9SGc3AwE+wXONQoW8GC00N3v6lCZLW74HQzqB6ROdcm5VAHM4CB/xRzWSUF9CXL78ugiwtHx3wBcpx4H4Wg==", + "optional": true + }, + "node_modules/@polkadot-api/utils": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.0.1.tgz", + "integrity": "sha512-3j+pRmlF9SgiYDabSdZsBSsN5XHbpXOAce1lWj56IEEaFZVjsiCaxDOA7C9nCcgfVXuvnbxqqEGQvnY+QfBAUw==", + "optional": true + }, + "node_modules/@polkadot/api": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-12.3.1.tgz", + "integrity": "sha512-VCrtadJRJttya5NhZ8slkD/UQyOZv4qABjagQMaG1eTZpn5k1wskmDUGdHrZZpYO5jBPewnCgaN8+LPKO2qiOw==", + "dependencies": { + "@polkadot/api-augment": "12.3.1", + "@polkadot/api-base": "12.3.1", + "@polkadot/api-derive": "12.3.1", + "@polkadot/keyring": "^13.0.2", + "@polkadot/rpc-augment": "12.3.1", + "@polkadot/rpc-core": "12.3.1", + "@polkadot/rpc-provider": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/types-augment": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/types-create": "12.3.1", + "@polkadot/types-known": "12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-12.3.1.tgz", + "integrity": "sha512-KfofZVEUeTgLzcexdxKBT2vihazDheUoTLxbsa2ztmmw4UB/IjOL911y04pjg2obZQAI9B+oCyxJXyCfzauWEg==", + "dependencies": { + "@polkadot/api-base": "12.3.1", + "@polkadot/rpc-augment": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/types-augment": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-12.3.1.tgz", + "integrity": "sha512-vNbxXNjn4APfXg+ui99gurX2Jzns+eezmWranxoGXT7q0mme1zAtWus5t4e+qe1qRjDNZZYPruF7YJA8dL5k8A==", + "dependencies": { + "@polkadot/rpc-core": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/util": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-12.3.1.tgz", + "integrity": "sha512-2MbK1h4GcKEdSgDKKYI28iZESw0VOm0kekV6YGQflZNWe84jJOn2rohP8pACseUjQjwWDgbPPBvTlRZTk7zdAw==", + "dependencies": { + "@polkadot/api": "12.3.1", + "@polkadot/api-augment": "12.3.1", + "@polkadot/api-base": "12.3.1", + "@polkadot/rpc-core": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/keyring": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.0.2.tgz", + "integrity": "sha512-NeLbhyKDT5W8LI9seWTZGePxNTOVpDhv2018HSrEDwJq9Ie0C4TZhUf3KNERCkSveuThXjfQJMs+1CF33ZXPWw==", + "dependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/util-crypto": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/util-crypto": "13.0.2" + } + }, + "node_modules/@polkadot/networks": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-13.0.2.tgz", + "integrity": "sha512-ABAL+vug/gIwkdFEzeh87JoJd0YKrxSYg/HjUrZ+Zis2ucxQEKpvtCpJ34ku+YrjacBfVqIAkkwd3ZdIPGq9aQ==", + "dependencies": { + "@polkadot/util": "13.0.2", + "@substrate/ss58-registry": "^1.46.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-12.3.1.tgz", + "integrity": "sha512-/tZLl5IuQ4vdGlUAbd8x3ShZ35XDSeyknKHCC+9kIrM/+KIyoCYBob2RXP9uqX8m516AWkXUrjsSO6DFPBpRGg==", + "dependencies": { + "@polkadot/rpc-core": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-12.3.1.tgz", + "integrity": "sha512-bNo26P20nRpLfANTK4sWEakxvqBJpKwAp/Gt7KlxoGgoTUbWFapyGKScFxp/pblycEziEbC+ZjkLMkaWaqi69g==", + "dependencies": { + "@polkadot/rpc-augment": "12.3.1", + "@polkadot/rpc-provider": "12.3.1", + "@polkadot/types": "12.3.1", + "@polkadot/util": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-12.3.1.tgz", + "integrity": "sha512-Tg1Oj/1ldivqwnnOWepcNHEHYgpOBffxlrZMEXH1XX6D3AZaUhAWbatizyisydpuMbknTQ9FGYSnb9rOc2QBJw==", + "dependencies": { + "@polkadot/keyring": "^13.0.2", + "@polkadot/types": "12.3.1", + "@polkadot/types-support": "12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "@polkadot/x-fetch": "^13.0.2", + "@polkadot/x-global": "^13.0.2", + "@polkadot/x-ws": "^13.0.2", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.10" + } + }, + "node_modules/@polkadot/types": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-12.3.1.tgz", + "integrity": "sha512-4MkTF1znpgp9mZc/ZZPdFe7/5it9v+EJmzXc/DEOX9kVWs2BuKOWopzOFyO3reVUUB+v7dxfSOArSsxkMUcuoA==", + "dependencies": { + "@polkadot/keyring": "^13.0.2", + "@polkadot/types-augment": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/types-create": "12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-12.3.1.tgz", + "integrity": "sha512-I3ggJt7W3UOScP6WA6PNmNsmpCfZtXkRJvSJkX7bi2LsSm/iF0xo0KdpQK02dHu7nGRFD9O5cSoVawzZJifGLA==", + "dependencies": { + "@polkadot/types": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-12.3.1.tgz", + "integrity": "sha512-yZ4exsQI+eVkE/fZNuJBOajAgOH/YncKWOOf0N4lc6iq28oYp22DCAXc50Ym372l4HO+uhC9QdMPH9EiWwT2pQ==", + "dependencies": { + "@polkadot/util": "^13.0.2", + "@polkadot/x-bigint": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-12.3.1.tgz", + "integrity": "sha512-Jf9BByWB64FPW3lM5/Mcc/foyPJ3L9t0QwHVHaYWaonZt6l7SPX71rQmD7twJiTj9q1d1WidDKg/TtRDNbm1yA==", + "dependencies": { + "@polkadot/types-codec": "12.3.1", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-12.3.1.tgz", + "integrity": "sha512-G8t0uiIW1iu3KwylHDPnqdHxg5qwBxzPZQJvsjnGx2qBUk2VqXditKWcNFLEwCTnJPL95t2AzEO711lS99WRbg==", + "dependencies": { + "@polkadot/networks": "^13.0.2", + "@polkadot/types": "12.3.1", + "@polkadot/types-codec": "12.3.1", + "@polkadot/types-create": "12.3.1", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-12.3.1.tgz", + "integrity": "sha512-TwL5M5HkZ4jQGKekD+qJFLba7UXWASfwlPy2OpKj0LOnnmq4tudXgN13UFdQ7HoOmisPhr+vYo9vteYeBZ0jTA==", + "dependencies": { + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.0.2.tgz", + "integrity": "sha512-/6bS9sfhJLhs8QuqWaR1eRapzfDdGC5XAQZEPL9NN5sTTA7HxWos8rVleai0UERm8QUMabjZ9rK9KpzbXl7ojg==", + "dependencies": { + "@polkadot/x-bigint": "13.0.2", + "@polkadot/x-global": "13.0.2", + "@polkadot/x-textdecoder": "13.0.2", + "@polkadot/x-textencoder": "13.0.2", + "@types/bn.js": "^5.1.5", + "bn.js": "^5.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.0.2.tgz", + "integrity": "sha512-woUsJJ6zd/caL7U+D30a5oM/+WK9iNI00Y8aNUHSj6Zq/KPzK9uqDBaLGWwlgrejoMQkxxiU2X0f2LzP15AtQg==", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.0.2", + "@polkadot/util": "13.0.2", + "@polkadot/wasm-crypto": "^7.3.2", + "@polkadot/wasm-util": "^7.3.2", + "@polkadot/x-bigint": "13.0.2", + "@polkadot/x-randomvalues": "13.0.2", + "@scure/base": "^1.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.3.2.tgz", + "integrity": "sha512-AJEXChcf/nKXd5Q/YLEV5dXQMle3UNT7jcXYmIffZAo/KI394a+/24PaISyQjoNC0fkzS1Q8T5pnGGHmXiVz2g==", + "dependencies": { + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.3.2.tgz", + "integrity": "sha512-+neIDLSJ6jjVXsjyZ5oLSv16oIpwp+PxFqTUaZdZDoA2EyFRQB8pP7+qLsMNk+WJuhuJ4qXil/7XiOnZYZ+wxw==", + "dependencies": { + "@polkadot/wasm-bridge": "7.3.2", + "@polkadot/wasm-crypto-asmjs": "7.3.2", + "@polkadot/wasm-crypto-init": "7.3.2", + "@polkadot/wasm-crypto-wasm": "7.3.2", + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.2.tgz", + "integrity": "sha512-QP5eiUqUFur/2UoF2KKKYJcesc71fXhQFLT3D4ZjG28Mfk2ZPI0QNRUfpcxVQmIUpV5USHg4geCBNuCYsMm20Q==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.2.tgz", + "integrity": "sha512-FPq73zGmvZtnuJaFV44brze3Lkrki3b4PebxCy9Fplw8nTmisKo9Xxtfew08r0njyYh+uiJRAxPCXadkC9sc8g==", + "dependencies": { + "@polkadot/wasm-bridge": "7.3.2", + "@polkadot/wasm-crypto-asmjs": "7.3.2", + "@polkadot/wasm-crypto-wasm": "7.3.2", + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.2.tgz", + "integrity": "sha512-15wd0EMv9IXs5Abp1ZKpKKAVyZPhATIAHfKsyoWCEFDLSOA0/K0QGOxzrAlsrdUkiKZOq7uzSIgIDgW8okx2Mw==", + "dependencies": { + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.3.2.tgz", + "integrity": "sha512-bmD+Dxo1lTZyZNxbyPE380wd82QsX+43mgCm40boyKrRppXEyQmWT98v/Poc7chLuskYb6X8IQ6lvvK2bGR4Tg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.0.2.tgz", + "integrity": "sha512-h2jKT/UaxiEal8LhQeH6+GCjO7GwEqVAD2SNYteCOXff6yNttqAZYJuHZsndbVjVNwqRNf8D5q/zZkD0HUd6xQ==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-13.0.2.tgz", + "integrity": "sha512-B/gf9iriUr6za/Ui7zIFBfHz7UBZ68rJEIteWHx1UHRCZPcLqv+hgpev6xIGrkfFljI0/lI7IwtN2qy6HYzFBg==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "node-fetch": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-global": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.0.2.tgz", + "integrity": "sha512-OoNIXLB5y8vIKpk4R+XmpDPhipNXWSUvEwUnpQT7NAxNLmzgMq1FhbrwBWWPRNHPrQonp7mqxV/X+v5lv1HW/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.0.2.tgz", + "integrity": "sha512-SGj+L0H/7TWZtSmtkWlixO4DFzXDdluI0UscN2h285os2Ns8PnmBbue+iJ8PVSzpY1BOxd66gvkkpboPz+jXFQ==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.0.2.tgz", + "integrity": "sha512-mauglOkTJxLGmLwLc3J5Jlq/W+SHP53eiy3F8/8JxxfnXrZKgWoQXGpvXYPjFnMZj0MzDSy/6GjyGWnDCgdQFA==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.0.2.tgz", + "integrity": "sha512-Lq08H2OnVXj97uaOwg7tcmRS7a4VJYkHEeWO4FyEMOk6P6lU6W8OVNjjxG0se9PCEgmyZPUDbJI//1ynzP4cXw==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-13.0.2.tgz", + "integrity": "sha512-nC5e2eY5D5ZR5teQOB7ib+dWLbmNws86cTz3BjKCalSMBBIn6i3V9ElgABpierBmnSJe9D94EyrH1BxdVfDxUg==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scure/base": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@substrate/connect": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.10.tgz", + "integrity": "sha512-DIyQ13DDlXqVFnLV+S6/JDgiGowVRRrh18kahieJxhgvzcWicw5eLc6jpfQ0moVVLBYkO7rctB5Wreldwpva8w==", + "deprecated": "versions below 1.x are no longer maintained", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.4", + "@substrate/light-client-extension-helpers": "^0.0.6", + "smoldot": "2.0.22" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.0.0.tgz", + "integrity": "sha512-nKu8pDrE3LNCEgJjZe1iGXzaD6OSIDD4Xzz/yo4KO9mQ6LBvf49BVrt4qxBFGL6++NneLiWUZGoh+VSd4PyVIg==", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.2.2.tgz", + "integrity": "sha512-gOGrXSWA2d/3kf8Yco00VlHZl48smzAGW5Z9MDxMht98hRpT2yEEN4N5QdoEKMI4ihDW8goXGzmp79D0hFPpuA==", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-0.0.6.tgz", + "integrity": "sha512-girltEuxQ1BvkJWmc8JJlk4ZxnlGXc/wkLcNguhY+UoDEMBK0LsdtfzQKIfrIehi4QdeSBlFEFBoI4RqPmsZzA==", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.1", + "@polkadot-api/json-rpc-provider-proxy": "0.0.1", + "@polkadot-api/observable-client": "0.1.0", + "@polkadot-api/substrate-client": "0.0.1", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.4", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.49.0.tgz", + "integrity": "sha512-leW6Ix4LD7XgvxT7+aobPWSw+WvPcN2Rxof1rmd0mNC5t2n99k1N7UNEvz7YEFSOUeHWmKIY7F5q8KeIqYoHfA==" + }, + "node_modules/@types/bn.js": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/k6": { "version": "0.51.0", "resolved": "https://registry.npmjs.org/@types/k6/-/k6-0.51.0.tgz", "integrity": "sha512-xelcvFGPI4VYrV5ozADmRuFQBKmDqDRzxfHVuCDD1/firZiSQvTP0pntxHuYUSkRyL8I83kvABXUlnLYNT2VuA==", "dev": true + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scale-ts": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.0.tgz", + "integrity": "sha512-Ja5VCjNZR8TGKhUumy9clVVxcDpM+YFjAnkMuwQy68Hixio3VRRvWdE3g8T/yC+HXA0ZDQl2TGyUmtmbcVl40Q==", + "optional": true + }, + "node_modules/smoldot": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.22.tgz", + "integrity": "sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g==", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/services/account/k6-test/package.json b/services/account/k6-test/package.json index cc6985a3..aeb9ac01 100644 --- a/services/account/k6-test/package.json +++ b/services/account/k6-test/package.json @@ -4,12 +4,19 @@ "description": "The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs.", "main": "account-service-load.js", "scripts": { + "generate:signups": "node generate/signups.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", - "license": "ISC", + "license": "Apache-2.0", "devDependencies": { "@types/k6": "^0.51.0" + }, + "dependencies": { + "@polkadot/api": "^12.3.1", + "@polkadot/types": "^12.3.1", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2" } } diff --git a/services/account/k6-test/signups.gen.js b/services/account/k6-test/signups.gen.js new file mode 100644 index 00000000..04b3511d --- /dev/null +++ b/services/account/k6-test/signups.gen.js @@ -0,0 +1,165 @@ + + // GENERATED FILE! Regenerate using npm run generate:signups + export const signups = [ + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c0112314c23e4db5a72860c155340c795790090388f101e67d7b9770cc8b556ec6c017adc6b9662b45039b188b880e581a9c2e8e8416622ce2933d5495497c4e4a25d5c708380563daee96c079a764a4e79578b95419fa8147890693e46d40f34768f010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc50104420012314c23e4db5a72860c155340c795790090388f101e67d7b9770cc8b556ec6c0188346a1fdba5e63a91ce2de8e2e45d16c4af6476e2bed5469fc03565fbe0a973ae6257db65772322c4aeaa0494a91dcc1010d801452df284d03b3c2ad7fd168f203543555a4e327a6764000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c01788dce66a1b7bbd4e223f5a1ba10d93fdaaddefc151f6b221dd5e0ea564f2a7401963bb2e86a9ced2d2bee262027e7e177aab080ca5a597caab07f052c74e79353a79cf6914462096e98135bd5b3f46143bdbccc54dcd3eb1048a497b10f7f2286010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc501044200788dce66a1b7bbd4e223f5a1ba10d93fdaaddefc151f6b221dd5e0ea564f2a74013465e0e8814ecb2269a200a1e479826d6a9aba644b21d7f4c1a5eea3b80eb12214a5bccd2bf8ff92f356141eface7b9a41eac1b56afacdeb3d6cce596ecdc98d2035456e6d6a44436264000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c014e84760e864f18fa4aa8089f8cfa3347ce8533ccd898c386730d9bded366026201306663ea0dd22fff1df12db8a3dd19ecf94fede4568fe497f86d0a29471d3970cc45a11c3e8f1298b76ee3901ee1be5c01005beb2d880323be79f1652a279786010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc5010442004e84760e864f18fa4aa8089f8cfa3347ce8533ccd898c386730d9bded3660262012e4c32568af35395bdbfa19eade46fac0b7f26aa7ac2dabc1c589d0cc6e0b954e24c29611e17e112197e70a9b6f92ba2daf1ce1ce2056e538c7fbe84d8d5458420354471657759785664000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c010e554132a77274bde2071d06fe76108a01cfe701507b03c5f838595669cb217401b82c0f221789f8e194e1a712be81dec0f1b6d960a59286620b28bb6ecc531f2fd055247d4c3bf417a61c171f35abe126bb8263f7012b3485991bc898fec1548a010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc5010442000e554132a77274bde2071d06fe76108a01cfe701507b03c5f838595669cb21740156dd50096330a95b31f4c78aa698c5d85bcdb8a0df829291cfee59780e53cc5ebdf7c80d53e3abfc9ef8d949034719f657b4e508bcef5f96bad3fd3f8100e886203543505672544d3464000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c01205029209aded136c5b24887ea1ad4565ee60e438db6f5dcaca404d9fcc9085b012498e3447196cb2f93a8bd9e591b2733d8e48dfe6b54c89b5c87b17a21e73712714f734316d8e1668e580892a72de8713ae2e8dcecb5611ccf8215911dc52f8d010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc501044200205029209aded136c5b24887ea1ad4565ee60e438db6f5dcaca404d9fcc9085b01acc9b16de4f13cae4d0c249fb0c57f758923b26e25bba390d51585080c31506ce8b331421804bf93f051e5857b2cc459c01b898582336b68827c58f4fa4a0f802035436f3543674b4b64000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c014a7e9342f32196f3c7aa36154b0db60ad9047c697504edb5dd1abc1d947a3523011cd40bd17ee79bd8052927e17dabba10f691f21fc5da864ed36bf05d15fcdb4ae6ba78e770d63e05b19acd9d5f96a98a4f6a031887b1fd4a7cc5069c2aee918b010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc5010442004a7e9342f32196f3c7aa36154b0db60ad9047c697504edb5dd1abc1d947a352301b2eddcbc1e76154c75cea0799a5980ce73e2a525a71e7f23ab1b2eae31db8728b1c3c4060de5b7a02665de7bb54a719c1c63c2e35edc09ce7294659ef40e1c832035446b503132697664000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c01d6c88bf28f9299ff371871695eebf3602678ff3e8060b8668cae634709bf0902014c8b7c372a92560116a3b477938ca5ccbd9667f82de9144cfbc95de0d2ae06687c1e145acb401e3e8ae151fe6e33b2d66724cca9c7db2ec6fa525f26fef89d82010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc501044200d6c88bf28f9299ff371871695eebf3602678ff3e8060b8668cae634709bf09020172686f0a2ecb34fbb019c05fe4f32a69c08d584208fc8d6e695836116a9c8005f89a664f22aee2658428fe1ac4d67183142eada0bfdbd77958cb4dd58488d387203547764b674d506764000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c01f8b4e06f5c5ae437e8485ae9ec19edb3036f2f94c9a88e67a4c0b747a0512f720170886a75b4032f33e95d1ef9641742f65127d6f8863a9bb5bea007a54d65572e148731654954fd764cc7fa770d0c8f5d8fc73b1393afe9f12ac7efa2b3efce87010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc501044200f8b4e06f5c5ae437e8485ae9ec19edb3036f2f94c9a88e67a4c0b747a0512f7201feb5612ba688d4bb10eadd84b6b1be075376e1cdb812019715ec9c92bf4bb8339dda3899627c17149a9039e8c67cc8cbaa0c1f8a7d1f85eb4e172606a274968d203548676f5473697064000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c012855dc9998ad16b1eab804424d214da7acf87c14b8b27ed33bb2f838cb06547801aa9d429155da56378e2902b40a2ad7dcb5f3002102284547ef3e614eddf5c87215ccc29c7276ba32811337bef430829f46e15d3b6fc32c18133c9cc19c69ec89010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc5010442002855dc9998ad16b1eab804424d214da7acf87c14b8b27ed33bb2f838cb065478017c2675d392d7ba33813a51560811674ac42966ac21032527fa5a350cb1674b791d2cbd429664ce7cf8a18923264dab05aa42223e1398d33631cdc3a81e2d008a20354379624838414364000000" + } + ] + } + }, + { + "signUp": { + "extrinsics": [ + { + "pallet": "msa", + "extrinsicName": "createSponsoredAccountWithDelegation", + "encodedExtrinsic": "0xcd01043c016044d8001ffea105dab1583161003d65e2bd9c11d5a1cb777efa1df6877080340112c71f897faf2dd9f11b71d2f49563aba27d9f774907d02a5d1112097b110233291c0e86c454e2add1004f7c88c149ede1786154bbffc3ce4b80b2efac8cfe80010000000000000004010064000000" + }, + { + "pallet": "handles", + "extrinsicName": "claimHandle", + "encodedExtrinsic": "0xc5010442006044d8001ffea105dab1583161003d65e2bd9c11d5a1cb777efa1df687708034014ccc9ea7af7132083482f69262818e8cd7224d475932f14188b69afbad9f8e2f67649c8e4cbe9090d6ac9fda8cd934151735a5cd5dc4c460c4e58f475c00da8020354545767553587164000000" + } + ] + } + } +]; + \ No newline at end of file diff --git a/services/account/libs/common/src/types/dtos/accounts.response.dto.ts b/services/account/libs/common/src/types/dtos/accounts.response.dto.ts index 68c77b45..db5fcd2d 100644 --- a/services/account/libs/common/src/types/dtos/accounts.response.dto.ts +++ b/services/account/libs/common/src/types/dtos/accounts.response.dto.ts @@ -22,3 +22,9 @@ export class AccountResponse { @IsOptional() handle?: HandleResponseDTO; } + +export class MsaIdResponse { + @ApiProperty() + @IsNotEmpty() + msaId: string; +} diff --git a/services/account/package-lock.json b/services/account/package-lock.json index 41237915..34556cf0 100644 --- a/services/account/package-lock.json +++ b/services/account/package-lock.json @@ -90,6 +90,7 @@ } }, "../../packages/ts-config": { + "name": "@amplica-labs/ts-config", "version": "1.0.0", "dev": true, "license": "Apache-2.0" diff --git a/services/account/package.json b/services/account/package.json index a30964a0..0ab27268 100644 --- a/services/account/package.json +++ b/services/account/package.json @@ -21,19 +21,14 @@ "start:worker:prod": "node dist/apps/worker/main.js", "start:worker:dev": "set -a ; . .env ; nest start worker", "start:worker:debug": "set -a ; . .env ; nest start worker --debug=9230 --watch", - "docker:build": "docker build -t account-service .", - "docker:build:dev": "docker-compose build", - "docker:run": " build -t account-service-deploy . ; docker run -p 6379:6379 --env-file .env account-service-deploy", - "docker:run:dev": "docker-compose up -d ; docker-compose -f docker-compose.dev.yaml logs", - "docker:stop:dev": "docker-compose stop", "clean": "rm -Rf dist", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "pretest": "cp env.template .env", "test": "jest --coverage --verbose", "test:e2e": "set -a ; . .env ; jest --verbose --silent --runInBand --testRegex \".e2e-spec.ts\" --detectOpenHandles --forceExit", "test:e2e:verbose": "set -a ; . .env ; jest --verbose --runInBand --testRegex \".e2e-spec.ts\" --detectOpenHandles --forceExit", - "test:k6:account-service": "k6 run k6-test/account-service-load.js", - "test:k6:new-sign-up": "k6 run k6-test/new-sign-up.js", + "test:k6:account-service": "k6 run k6-test/account-service-load.k6.js", + "test:k6:new-sign-up": "k6 run k6-test/new-sign-up.k6.js", "test:k6:health-check": "k6 run k6-test/health-check.js", "test:k6": "npm run test:k6:account-service ; npm run test:k6:new-sign-up ; npm run test:k6:health-check" }, diff --git a/services/account/rust-webhook-server/src/main.rs b/services/account/rust-webhook-server/src/main.rs index 5605ef5c..b6f7eade 100644 --- a/services/account/rust-webhook-server/src/main.rs +++ b/services/account/rust-webhook-server/src/main.rs @@ -22,7 +22,7 @@ const WEBHOOK_ENDPOINT: &str = "/account-service"; #[actix_web::main] async fn main() -> Result<(), impl Error> { - const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); + const HOST: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); const PORT: u16 = 3001; env_logger::init(); println!( diff --git a/services/account/swagger.json b/services/account/swagger.json index d0f7799a..3fd24a60 100644 --- a/services/account/swagger.json +++ b/services/account/swagger.json @@ -55,7 +55,7 @@ }, "/v1/accounts/{msaId}": { "get": { - "operationId": "AccountsControllerV1_getAccount", + "operationId": "AccountsControllerV1_getAccountForMsa", "summary": "Fetch an account given an msaId.", "parameters": [ { @@ -84,6 +84,37 @@ ] } }, + "/v1/accounts/account/{publicKey}": { + "get": { + "operationId": "AccountsControllerV1_getAccountForPublicKey", + "summary": "Fetch an account given a public key.", + "parameters": [ + { + "name": "publicKey", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Found account", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountResponse" + } + } + } + } + }, + "tags": [ + "v1/accounts" + ] + } + }, "/v1/delegation/{msaId}": { "get": { "operationId": "DelegationControllerV1_getDelegation", @@ -359,7 +390,7 @@ "type": "string" }, "signature": { - "type": "object" + "type": "string" } }, "required": [ @@ -399,7 +430,7 @@ "type": "string" }, "encodedExtrinsic": { - "type": "object" + "type": "string" } }, "required": [ @@ -489,7 +520,7 @@ "$ref": "#/components/schemas/HandlePayload" }, "proof": { - "type": "object" + "type": "string" } }, "required": [ @@ -524,10 +555,10 @@ "type": "string" }, "msaOwnerSignature": { - "type": "object" + "type": "string" }, "newKeyOwnerSignature": { - "type": "object" + "type": "string" }, "payload": { "$ref": "#/components/schemas/KeysRequestPayload" diff --git a/services/content-publishing/.env.docker.dev b/services/content-publishing/.env.docker.dev deleted file mode 100644 index 02bc678b..00000000 --- a/services/content-publishing/.env.docker.dev +++ /dev/null @@ -1,51 +0,0 @@ -# Copy this file to ".env.dev" and ".env.docker.dev", and then tweak values for local development - -# URL to IPFS endpoint -# IPFS_ENDPOINT="https://ipfs.infura.io:5001" -IPFS_ENDPOINT="http://127.0.0.1:5001" - -# If using Infura, put Project ID here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_USER= - -# If using Infura, put auth token here, or leave blank for Kubo RPC -IPFS_BASIC_AUTH_SECRET= - -# IPFS gateway URL. '[CID]' is a token that will be replaced with an actual content ID -# IPFS_GATEWAY_URL="https://ipfs.io/ipfs/[CID]" -IPFS_GATEWAY_URL="http://127.0.0.1:8080/ipfs/[CID]" - -# Blockchain node address -FREQUENCY_URL=ws://0.0.0.0:9944 - -PROVIDER_ID=1 -# Redis URL -REDIS_URL=redis://0.0.0.0:6379 -PROVIDER_ACCOUNT_SEED_PHRASE="//Alice" -CAPACITY_LIMIT='{"type":"percentage", "value":80}' - -# Port that the application REST endpoints listen on -API_PORT=3000 - -# Environment for mapping announcement type to schema ID (use 'dev' for e2e tests) -# Possible values: dev, rococo, testnet, mainnet -CHAIN_ENVIRONMENT=dev - -# Max file size allowed for asset upload -FILE_UPLOAD_MAX_SIZE_IN_BYTES=2000000000 - -# Number of seconds to keep completed asset entrie in the cache -# before expiring them -ASSET_EXPIRATION_INTERVAL_SECONDS=300 - -# Number of seconds between content publishing batches. This is so that -# the service waits a reasonable amount of time for additional content to publishing -# before submitting a batch--it represents a trade-off between maximum batch fullness -# and minimal wait time for published content. -BATCH_INTERVAL_SECONDS=12 - -# Maximum number of items that can be submitted in a single batch -BATCH_MAX_COUNT=1000 - -# Base delay in seconds used for exponential backoff while waiting for -# uploaded assets to be verified available before publishing a content notice. -ASSET_UPLOAD_VERIFICATION_DELAY_SECONDS=5 diff --git a/services/content-publishing/Dockerfile b/services/content-publishing/Dockerfile deleted file mode 100644 index d7901660..00000000 --- a/services/content-publishing/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Use a multi-stage build for efficiency -FROM node:20 AS builder - -WORKDIR /app - -COPY package*.json ./ - -RUN npm install - -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node:20 - -WORKDIR /app - -COPY --from=builder /app/dist ./dist -COPY package*.json ./ -COPY ./lua ./lua -COPY ./scripts/docker-entrypoint.sh ./ -RUN chmod +x ./docker-entrypoint.sh - -RUN npm ci --omit=dev - -# We want jq and curl in the final image, but we don't need the support files -RUN apt-get update && \ - apt-get install -y jq curl tini && \ - apt-get clean && \ - rm -rf /usr/share/doc /usr/share/man /usr/share/zsh - -EXPOSE 3000 - -ENV START_PROCESS="api" - -ENTRYPOINT ["/usr/bin/tini", "--", "./docker-entrypoint.sh"] diff --git a/services/content-publishing/README.md b/services/content-publishing/README.md index e92a46da..b45229bf 100644 --- a/services/content-publishing/README.md +++ b/services/content-publishing/README.md @@ -167,27 +167,16 @@ Use the provided [env.template](./env.template) file to create an initial enviro ```sh cp env.template .env - cp env.template .env.docker.dev ``` 2. Configure the environment variable values according to your environment. -### Setup - -Clone this repository to your desired folder: - -Example commands: - -```sh - git clone git@github.com:AmplicaLabs/content-publishing-service.git - cd content-publishing-service -``` - ### Install Install NPM Dependencies: ```sh + cd services/content-publishing npm install ``` diff --git a/services/content-publishing/apps/api/src/api.service.ts b/services/content-publishing/apps/api/src/api.service.ts index 46868078..b1aae1e3 100644 --- a/services/content-publishing/apps/api/src/api.service.ts +++ b/services/content-publishing/apps/api/src/api.service.ts @@ -6,15 +6,8 @@ import { BulkJobOptions } from 'bullmq/dist/esm/interfaces'; import { InjectRedis } from '@songkeys/nestjs-redis'; import Redis from 'ioredis'; import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util'; -import { - AnnouncementTypeDto, - RequestTypeDto, - AnnouncementResponseDto, - AssetIncludedRequestDto, - isImage, - UploadResponseDto, -} from '#libs/dtos'; -import { IRequestJob, IAssetMetadata, IAssetJob } from '#libs/interfaces'; +import { AnnouncementTypeDto, RequestTypeDto, AnnouncementResponseDto, AssetIncludedRequestDto, isImage, UploadResponseDto, AttachmentType } from '#libs/dtos'; +import { IRequestJob, IAssetMetadata, IAssetJob, IAssetTypeInfo } from '#libs/interfaces'; import { REQUEST_QUEUE_NAME, ASSET_QUEUE_NAME } from '#libs/queues/queue.constants'; import { calculateIpfsCID } from '#libs/utils/ipfs'; import { getAssetMetadataKey, getAssetDataKey, STORAGE_EXPIRE_UPPER_LIMIT_SECONDS } from '#libs/utils/redis'; @@ -35,7 +28,7 @@ export class ApiService { announcementType: AnnouncementTypeDto, dsnpUserId: string, content: RequestTypeDto, - assetToMimeType?: Map, + assetToMimeType?: IRequestJob['assetToMimeType'], ): Promise { const data = { content, @@ -60,12 +53,10 @@ export class ApiService { }; } - async validateAssetsAndFetchMetadata(content: AssetIncludedRequestDto): Promise | undefined> { + async validateAssetsAndFetchMetadata(content: AssetIncludedRequestDto): Promise { const checkingList: { onlyImage: boolean; referenceId: string }[] = []; if (content.profile) { - content.profile.icon?.forEach((reference) => - checkingList.push({ onlyImage: true, referenceId: reference.referenceId }), - ); + content.profile.icon?.forEach((reference) => checkingList.push({ onlyImage: true, referenceId: reference.referenceId })); } else if (content.content) { content.content.assets?.forEach((asset) => asset.references?.forEach((reference) => @@ -77,19 +68,15 @@ export class ApiService { ); } - const redisResults = await Promise.all( - checkingList.map((obj) => this.redis.get(getAssetMetadataKey(obj.referenceId))), - ); + const redisResults = await Promise.all(checkingList.map((obj) => this.redis.get(getAssetMetadataKey(obj.referenceId)))); const errors: string[] = []; const map = new Map(); redisResults.forEach((res, index) => { if (res === null) { - errors.push( - `${content.profile ? 'profile.icon' : 'content.assets'}.referenceId ${checkingList[index].referenceId} does not exist!`, - ); + errors.push(`${content.profile ? 'profile.icon' : 'content.assets'}.referenceId ${checkingList[index].referenceId} does not exist!`); } else { const metadata: IAssetMetadata = JSON.parse(res); - map[checkingList[index].referenceId] = metadata.mimeType; + map[checkingList[index].referenceId] = { mimeType: metadata.mimeType, attachmentType: metadata.type }; // checks if attached asset is an image if (checkingList[index].onlyImage && !isImage(metadata.mimeType)) { @@ -114,20 +101,29 @@ export class ApiService { const jobs: any[] = []; files.forEach((f, index) => { // adding data and metadata to the transaction - dataTransaction = dataTransaction.setex( - getAssetDataKey(references[index]), - STORAGE_EXPIRE_UPPER_LIMIT_SECONDS, - f.buffer, - ); - metadataTransaction = metadataTransaction.setex( - getAssetMetadataKey(references[index]), - STORAGE_EXPIRE_UPPER_LIMIT_SECONDS, - JSON.stringify({ - ipfsCid: references[index], - mimeType: f.mimetype, - createdOn: Date.now(), - } as IAssetMetadata), - ); + dataTransaction = dataTransaction.setex(getAssetDataKey(references[index]), STORAGE_EXPIRE_UPPER_LIMIT_SECONDS, f.buffer); + const type = ((m) => { + switch (m) { + case 'image': + return AttachmentType.IMAGE; + case 'audio': + return AttachmentType.AUDIO; + case 'video': + return AttachmentType.VIDEO; + default: + throw new Error('Invalid MIME type'); + } + })(f.mimetype.split('/')[0]); + + const assetCache: IAssetMetadata = { + ipfsCid: references[index], + mimeType: f.mimetype, + createdOn: Date.now(), + type: type, + }; + + metadataTransaction = metadataTransaction.setex(getAssetMetadataKey(references[index]), STORAGE_EXPIRE_UPPER_LIMIT_SECONDS, JSON.stringify(assetCache)); + // adding asset job to the jobs jobs.push({ name: `Asset Job - ${references[index]}`, diff --git a/services/content-publishing/apps/api/src/metadata.ts b/services/content-publishing/apps/api/src/metadata.ts index bf1f6e63..82bb0bd6 100644 --- a/services/content-publishing/apps/api/src/metadata.ts +++ b/services/content-publishing/apps/api/src/metadata.ts @@ -1,8 +1,93 @@ /* eslint-disable */ export default async () => { - const t = { - ["../../../libs/common/src/dtos/activity.dto"]: await import("../../../libs/common/src/dtos/activity.dto"), - ["../../../libs/common/src/dtos/announcement.dto"]: await import("../../../libs/common/src/dtos/announcement.dto") - }; - return { "@nestjs/swagger": { "models": [[import("../../../libs/common/src/dtos/common.dto"), { "DsnpUserIdParam": { userDsnpId: { required: true, type: () => String } }, "AnnouncementResponseDto": { referenceId: { required: true, type: () => String } }, "UploadResponseDto": { assetIds: { required: true, type: () => [String] } }, "FilesUploadDto": { files: { required: true, type: () => [Object] } } }], [import("../../../libs/common/src/dtos/activity.dto"), { "LocationDto": { name: { required: true, type: () => String, minLength: 1 }, accuracy: { required: false, type: () => Number, minimum: 0, maximum: 100 }, altitude: { required: false, type: () => Number }, latitude: { required: false, type: () => Number }, longitude: { required: false, type: () => Number }, radius: { required: false, type: () => Number, minimum: 0 }, units: { required: false, enum: t["../../../libs/common/src/dtos/activity.dto"].UnitTypeDto } }, "AssetReferenceDto": { referenceId: { required: true, type: () => String, minLength: 1 }, height: { required: false, type: () => Number, minimum: 1 }, width: { required: false, type: () => Number, minimum: 1 }, duration: { required: false, type: () => String, pattern: "DURATION_REGEX" } }, "TagDto": { type: { required: true, enum: t["../../../libs/common/src/dtos/activity.dto"].TagTypeDto }, name: { required: false, type: () => String, minLength: 1 }, mentionedId: { required: false, type: () => String } }, "AssetDto": { type: { required: true, enum: t["../../../libs/common/src/dtos/activity.dto"].AttachmentTypeDto }, references: { required: false, type: () => [t["../../../libs/common/src/dtos/activity.dto"].AssetReferenceDto] }, name: { required: false, type: () => String, minLength: 1 }, href: { required: false, type: () => String, minLength: 1 } }, "BaseActivityDto": { name: { required: false, type: () => String }, tag: { required: false, type: () => [t["../../../libs/common/src/dtos/activity.dto"].TagDto] }, location: { required: false, type: () => t["../../../libs/common/src/dtos/activity.dto"].LocationDto } }, "NoteActivityDto": { content: { required: true, type: () => String, minLength: 1 }, published: { required: true, type: () => String }, assets: { required: false, type: () => [t["../../../libs/common/src/dtos/activity.dto"].AssetDto] } }, "ProfileActivityDto": { icon: { required: false, type: () => [t["../../../libs/common/src/dtos/activity.dto"].AssetReferenceDto] }, summary: { required: false, type: () => String }, published: { required: false, type: () => String } } }], [import("../../../libs/common/src/dtos/announcement.dto"), { "BroadcastDto": { content: { required: true, type: () => t["../../../libs/common/src/dtos/activity.dto"].NoteActivityDto } }, "ReplyDto": { inReplyTo: { required: true, type: () => String }, content: { required: true, type: () => t["../../../libs/common/src/dtos/activity.dto"].NoteActivityDto } }, "TombstoneDto": { targetContentHash: { required: true, type: () => String }, targetAnnouncementType: { required: true, enum: t["../../../libs/common/src/dtos/announcement.dto"].ModifiableAnnouncementTypeDto } }, "UpdateDto": { targetContentHash: { required: true, type: () => String }, targetAnnouncementType: { required: true, enum: t["../../../libs/common/src/dtos/announcement.dto"].ModifiableAnnouncementTypeDto }, content: { required: true, type: () => t["../../../libs/common/src/dtos/activity.dto"].NoteActivityDto } }, "ReactionDto": { emoji: { required: true, type: () => String, minLength: 1, pattern: "DSNP_EMOJI_REGEX" }, apply: { required: true, type: () => Number, minimum: 0, maximum: 255 }, inReplyTo: { required: true, type: () => String } }, "ProfileDto": { profile: { required: true, type: () => t["../../../libs/common/src/dtos/activity.dto"].ProfileActivityDto } } }]], "controllers": [[import("./controllers/health.controller"), { "HealthController": { "healthz": {}, "livez": {}, "readyz": {} } }]] } }; -}; \ No newline at end of file + const t = { + ['../../../libs/common/src/dtos/activity.dto']: await import('../../../libs/common/src/dtos/activity.dto'), + ['../../../libs/common/src/dtos/announcement.dto']: await import('../../../libs/common/src/dtos/announcement.dto'), + }; + return { + '@nestjs/swagger': { + models: [ + [ + import('../../../libs/common/src/dtos/common.dto'), + { + DsnpUserIdParam: { userDsnpId: { required: true, type: () => String } }, + AnnouncementResponseDto: { referenceId: { required: true, type: () => String } }, + UploadResponseDto: { assetIds: { required: true, type: () => [String] } }, + FilesUploadDto: { files: { required: true, type: () => [Object] } }, + }, + ], + [ + import('../../../libs/common/src/dtos/activity.dto'), + { + LocationDto: { + name: { required: true, type: () => String, minLength: 1 }, + accuracy: { required: false, type: () => Number, minimum: 0, maximum: 100 }, + altitude: { required: false, type: () => Number }, + latitude: { required: false, type: () => Number }, + longitude: { required: false, type: () => Number }, + radius: { required: false, type: () => Number, minimum: 0 }, + units: { required: false, enum: t['../../../libs/common/src/dtos/activity.dto'].UnitTypeDto }, + }, + AssetReferenceDto: { + referenceId: { required: true, type: () => String, minLength: 1 }, + height: { required: false, type: () => Number, minimum: 1 }, + width: { required: false, type: () => Number, minimum: 1 }, + duration: { required: false, type: () => String, pattern: 'DURATION_REGEX' }, + }, + TagDto: { + type: { required: true, enum: t['../../../libs/common/src/dtos/activity.dto'].TagTypeDto }, + name: { required: false, type: () => String, minLength: 1 }, + mentionedId: { required: false, type: () => String }, + }, + AssetDto: { + references: { required: false, type: () => [t['../../../libs/common/src/dtos/activity.dto'].AssetReferenceDto] }, + name: { required: false, type: () => String, minLength: 1 }, + href: { required: false, type: () => String, minLength: 1 }, + }, + BaseActivityDto: { + name: { required: false, type: () => String }, + tag: { required: false, type: () => [t['../../../libs/common/src/dtos/activity.dto'].TagDto] }, + location: { required: false, type: () => t['../../../libs/common/src/dtos/activity.dto'].LocationDto }, + }, + NoteActivityDto: { + content: { required: true, type: () => String, minLength: 1 }, + published: { required: true, type: () => String }, + assets: { required: false, type: () => [t['../../../libs/common/src/dtos/activity.dto'].AssetDto] }, + }, + ProfileActivityDto: { + icon: { required: false, type: () => [t['../../../libs/common/src/dtos/activity.dto'].AssetReferenceDto] }, + summary: { required: false, type: () => String }, + published: { required: false, type: () => String }, + }, + }, + ], + [ + import('../../../libs/common/src/dtos/announcement.dto'), + { + BroadcastDto: { content: { required: true, type: () => t['../../../libs/common/src/dtos/activity.dto'].NoteActivityDto } }, + ReplyDto: { + inReplyTo: { required: true, type: () => String }, + content: { required: true, type: () => t['../../../libs/common/src/dtos/activity.dto'].NoteActivityDto }, + }, + TombstoneDto: { + targetContentHash: { required: true, type: () => String }, + targetAnnouncementType: { required: true, enum: t['../../../libs/common/src/dtos/announcement.dto'].ModifiableAnnouncementTypeDto }, + }, + UpdateDto: { + targetContentHash: { required: true, type: () => String }, + targetAnnouncementType: { required: true, enum: t['../../../libs/common/src/dtos/announcement.dto'].ModifiableAnnouncementTypeDto }, + content: { required: true, type: () => t['../../../libs/common/src/dtos/activity.dto'].NoteActivityDto }, + }, + ReactionDto: { + emoji: { required: true, type: () => String, minLength: 1, pattern: 'DSNP_EMOJI_REGEX' }, + apply: { required: true, type: () => Number, minimum: 0, maximum: 255 }, + inReplyTo: { required: true, type: () => String }, + }, + ProfileDto: { profile: { required: true, type: () => t['../../../libs/common/src/dtos/activity.dto'].ProfileActivityDto } }, + }, + ], + ], + controllers: [[import('./controllers/health.controller'), { HealthController: { healthz: {}, livez: {}, readyz: {} } }]], + }, + }; +}; diff --git a/services/content-publishing/apps/worker/src/publisher/nonce.service.ts b/services/content-publishing/apps/worker/src/publisher/nonce.service.ts index f23f35c7..a9255451 100644 --- a/services/content-publishing/apps/worker/src/publisher/nonce.service.ts +++ b/services/content-publishing/apps/worker/src/publisher/nonce.service.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import { ConfigService } from '#libs/config'; import { BlockchainService } from '#libs/blockchain/blockchain.service'; import { createKeys } from '#libs/blockchain/create-keys'; -import { NUMBER_OF_NONCE_KEYS_TO_CHECK, getNonceKey } from '#libs/utils/redis'; +import { NUMBER_OF_NONCE_KEYS_TO_CHECK, NONCE_KEY_EXPIRE_SECONDS, getNonceKey } from '#libs/utils/redis'; @Injectable() export class NonceService implements OnApplicationBootstrap { diff --git a/services/content-publishing/apps/worker/src/request_processor/dsnp.announcement.processor.ts b/services/content-publishing/apps/worker/src/request_processor/dsnp.announcement.processor.ts index 296fcd0f..f7a4064b 100644 --- a/services/content-publishing/apps/worker/src/request_processor/dsnp.announcement.processor.ts +++ b/services/content-publishing/apps/worker/src/request_processor/dsnp.announcement.processor.ts @@ -24,8 +24,9 @@ import { TombstoneDto, ModifiableAnnouncementTypeDto, TagTypeDto, - AttachmentTypeDto, + AttachmentType, AssetDto, + TagDto, } from '#libs/dtos'; import { IRequestJob, @@ -43,14 +44,7 @@ import { ProfileAnnouncement, createProfile, } from '#libs/interfaces'; -import { - BROADCAST_QUEUE_NAME, - REPLY_QUEUE_NAME, - REACTION_QUEUE_NAME, - UPDATE_QUEUE_NAME, - PROFILE_QUEUE_NAME, - TOMBSTONE_QUEUE_NAME, -} from '#libs/queues/queue.constants'; +import { BROADCAST_QUEUE_NAME, REPLY_QUEUE_NAME, REACTION_QUEUE_NAME, UPDATE_QUEUE_NAME, PROFILE_QUEUE_NAME, TOMBSTONE_QUEUE_NAME } from '#libs/queues/queue.constants'; import { calculateDsnpHash } from '#libs/utils/ipfs'; import { IpfsService } from '#libs/utils/ipfs.client'; @@ -132,16 +126,8 @@ export class DsnpAnnouncementProcessor { private async queueUpdate(data: IRequestJob) { const updateDto = data.content as UpdateDto; - const updateAnnouncementType: AnnouncementType = await this.getAnnouncementTypeFromModifiableAnnouncementType( - updateDto.targetAnnouncementType, - ); - const update = await this.processUpdate( - updateDto, - updateAnnouncementType, - updateDto.targetContentHash ?? '', - data.dsnpUserId, - data.assetToMimeType, - ); + const updateAnnouncementType: AnnouncementType = await this.getAnnouncementTypeFromModifiableAnnouncementType(updateDto.targetAnnouncementType); + const update = await this.processUpdate(updateDto, updateAnnouncementType, updateDto.targetContentHash ?? '', data.dsnpUserId, data.assetToMimeType); await this.updateQueue.add(`Update Job - ${data.id}`, update, { jobId: data.id, removeOnFail: false, @@ -160,9 +146,7 @@ export class DsnpAnnouncementProcessor { private async queueTombstone(data: IRequestJob) { const tombStoneDto = data.content as TombstoneDto; - const announcementType: AnnouncementType = await this.getAnnouncementTypeFromModifiableAnnouncementType( - tombStoneDto.targetAnnouncementType, - ); + const announcementType: AnnouncementType = await this.getAnnouncementTypeFromModifiableAnnouncementType(tombStoneDto.targetAnnouncementType); const tombstone = createTombstone(data.dsnpUserId, announcementType, tombStoneDto.targetContentHash ?? ''); await this.tombstoneQueue.add(`Tombstone Job - ${data.id}`, tombstone, { jobId: data.id, @@ -171,9 +155,7 @@ export class DsnpAnnouncementProcessor { }); } - private async getAnnouncementTypeFromModifiableAnnouncementType( - modifiableAnnouncementType: ModifiableAnnouncementTypeDto, - ): Promise { + private async getAnnouncementTypeFromModifiableAnnouncementType(modifiableAnnouncementType: ModifiableAnnouncementTypeDto): Promise { this.logger.debug(`Getting announcement type from modifiable announcement type`); switch (modifiableAnnouncementType) { case ModifiableAnnouncementTypeDto.BROADCAST: @@ -185,13 +167,10 @@ export class DsnpAnnouncementProcessor { } } - public async prepareNote(noteContent: any, assetToMimeType?: Map): Promise<[string, string, string]> { - this.logger.debug(`Preparing note`); + public async prepareNote(noteContent: BroadcastDto | ReplyDto | UpdateDto, assetToMimeType?: IRequestJob['assetToMimeType']): Promise<[string, string, string]> { + this.logger.debug(`Preparing note of type: ${typeof noteContent.content}`); const tags: ActivityContentTag[] = this.prepareTags(noteContent?.content.tag); - const attachments: ActivityContentAttachment[] = await this.prepareAttachments( - noteContent.content.assets, - assetToMimeType, - ); + const attachments: ActivityContentAttachment[] = await this.prepareAttachments(noteContent.content.assets, assetToMimeType); const note = createNote(noteContent.content.content ?? '', new Date(noteContent.content.published ?? ''), { name: noteContent.content.name, @@ -205,16 +184,22 @@ export class DsnpAnnouncementProcessor { return [cid, ipfsUrl, hash]; } - private prepareTags(tagData?: any[]): ActivityContentTag[] { + private prepareTags(tagData?: TagDto[]): ActivityContentTag[] { this.logger.debug(`Preparing tags`); const tags: ActivityContentTag[] = []; if (tagData) { tagData.forEach((tag) => { switch (tag.type) { case TagTypeDto.Hashtag: + if (!tag.name) { + throw new Error(`Tag name is required`); + } tags.push({ name: tag.name }); break; case TagTypeDto.Mention: + if (!tag.mentionedId) { + throw new Error(`Mentioned ID is required`); + } tags.push({ name: tag.name, type: 'Mention', @@ -229,33 +214,38 @@ export class DsnpAnnouncementProcessor { return tags; } - private async prepareAttachments( - assetData?: any[], - assetToMimeType?: Map, - ): Promise { + private async prepareAttachments(assetData?: AssetDto[], assetToMimeType?: IRequestJob['assetToMimeType']): Promise { const attachments: ActivityContentAttachment[] = []; if (assetData) { const promises = assetData.map(async (asset) => { - switch (asset.type) { - case AttachmentTypeDto.LINK: - attachments.push(this.prepareLinkAttachment(asset)); - break; - case AttachmentTypeDto.IMAGE: - attachments.push(await this.prepareImageAttachment(asset, assetToMimeType)); - break; - case AttachmentTypeDto.VIDEO: - attachments.push(await this.prepareVideoAttachment(asset, assetToMimeType)); - break; - case AttachmentTypeDto.AUDIO: - attachments.push(await this.prepareAudioAttachment(asset, assetToMimeType)); - break; - default: - throw new Error(`Unsupported attachment type ${typeof asset.type}`); + if (asset.references) { + const assetPromises = asset.references.map(async (reference) => { + if (!assetToMimeType) { + throw new Error(`asset ${reference.referenceId} should have a mimeTypes`); + } + const { attachmentType } = assetToMimeType[reference.referenceId]; + switch (attachmentType) { + case AttachmentType.LINK: + attachments.push(this.prepareLinkAttachment(asset)); + break; + case AttachmentType.IMAGE: + attachments.push(await this.prepareImageAttachment(asset, assetToMimeType)); + break; + case AttachmentType.VIDEO: + attachments.push(await this.prepareVideoAttachment(asset, assetToMimeType)); + break; + case AttachmentType.AUDIO: + attachments.push(await this.prepareAudioAttachment(asset, assetToMimeType)); + break; + default: + throw new Error(`Unsupported attachment type ${attachmentType}`); + } + }); + await Promise.all(assetPromises); } }); await Promise.all(promises); } - return attachments; } @@ -268,10 +258,7 @@ export class DsnpAnnouncementProcessor { }; } - private async prepareImageAttachment( - asset: AssetDto, - assetToMimeType?: Map, - ): Promise { + private async prepareImageAttachment(asset: AssetDto, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { const imageLinks: ActivityContentImageLink[] = []; if (asset.references) { const promises = asset.references.map(async (reference) => { @@ -301,10 +288,7 @@ export class DsnpAnnouncementProcessor { }; } - private async prepareVideoAttachment( - asset: AssetDto, - assetToMimeType?: Map, - ): Promise { + private async prepareVideoAttachment(asset: AssetDto, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { const videoLinks: ActivityContentVideoLink[] = []; let duration: string | undefined = ''; @@ -338,10 +322,7 @@ export class DsnpAnnouncementProcessor { }; } - private async prepareAudioAttachment( - asset: AssetDto, - assetToMimeType?: Map, - ): Promise { + private async prepareAudioAttachment(asset: AssetDto, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { const audioLinks: ActivityContentAudioLink[] = []; let duration = ''; if (asset.references) { @@ -373,21 +354,13 @@ export class DsnpAnnouncementProcessor { }; } - private async processBroadcast( - content: BroadcastDto, - dsnpUserId: string, - assetToMimeType?: Map, - ): Promise { + private async processBroadcast(content: BroadcastDto, dsnpUserId: string, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { this.logger.debug(`Processing broadcast`); const [cid, ipfsUrl, hash] = await this.prepareNote(content, assetToMimeType); return createBroadcast(dsnpUserId, ipfsUrl, hash); } - private async processReply( - content: ReplyDto, - dsnpUserId: string, - assetToMimeType?: Map, - ): Promise { + private async processReply(content: ReplyDto, dsnpUserId: string, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { this.logger.debug(`Processing reply for ${content.inReplyTo}`); const [cid, ipfsUrl, hash] = await this.prepareNote(content, assetToMimeType); return createReply(dsnpUserId, ipfsUrl, hash, content.inReplyTo); @@ -403,23 +376,16 @@ export class DsnpAnnouncementProcessor { targetAnnouncementType: AnnouncementType, targetContentHash: string, dsnpUserId: string, - assetToMimeType?: Map, + assetToMimeType?: IRequestJob['assetToMimeType'], ): Promise { this.logger.debug(`Processing update`); const [cid, ipfsUrl, hash] = await this.prepareNote(content, assetToMimeType); return createUpdate(dsnpUserId, ipfsUrl, hash, targetAnnouncementType, targetContentHash); } - private async processProfile( - content: ProfileDto, - dsnpUserId: string, - assetToMimeType?: Map, - ): Promise { + private async processProfile(content: ProfileDto, dsnpUserId: string, assetToMimeType?: IRequestJob['assetToMimeType']): Promise { this.logger.debug(`Processing profile`); - const attachments: ActivityContentImageLink[] = await this.prepareProfileIconAttachments( - content.profile.icon ?? [], - assetToMimeType, - ); + const attachments: ActivityContentImageLink[] = await this.prepareProfileIconAttachments(content.profile.icon ?? [], assetToMimeType); const profileActivity: ActivityContentProfile = { '@context': 'https://www.w3.org/ns/activitystreams', @@ -436,10 +402,7 @@ export class DsnpAnnouncementProcessor { return createProfile(dsnpUserId, this.formIpfsUrl(cid), hash); } - private async prepareProfileIconAttachments( - icons: any[], - assetToMimeType?: Map, - ): Promise { + private async prepareProfileIconAttachments(icons: any[], assetToMimeType?: IRequestJob['assetToMimeType']): Promise { const attachments: ActivityContentImageLink[] = []; const promises = icons.map(async (icon) => { diff --git a/services/content-publishing/dev.Dockerfile b/services/content-publishing/dev.Dockerfile deleted file mode 100644 index d5204f11..00000000 --- a/services/content-publishing/dev.Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:20 - -WORKDIR /app - -# COPY . . - -# RUN npm install - -EXPOSE 3000 - -ENV START_PROCESS="api" - -ENTRYPOINT npm run start:${START_PROCESS}:watch diff --git a/services/content-publishing/docker-compose.yaml b/services/content-publishing/docker-compose.yaml deleted file mode 100644 index 68f39cdb..00000000 --- a/services/content-publishing/docker-compose.yaml +++ /dev/null @@ -1,92 +0,0 @@ -services: - redis: - image: redis:latest - ports: - - 6379:6379 - volumes: - - redis_data:/data/redis - networks: - - content-publishing-service - - frequency: - image: dsnp/instant-seal-node-with-deployed-schemas:latest - # We need to specify the platform because it's the only image - # built by Frequency at the moment, and auto-pull won't work otherwise - platform: linux/amd64 - # Uncomment SEALING_MODE and SEALING_INTERVAL if you want to use interval sealing. - # Other options you may want to add depending on your test scenario. - # environment: - # - SEALING_MODE=interval - # - SEALING_INTERVAL=3 - # - CREATE_EMPTY_BLOCKS=true - # Uncomment below if you want to let the chain run and keep all of the historical blocks - # command: --state-pruning=archive - ports: - - 9944:9944 - networks: - - content-publishing-service - volumes: - - frequency_data:/data/frequency - - ipfs: - image: ipfs/kubo:latest - ports: - - 4001:4001 - - 5001:5001 - - 8080:8080 - networks: - - content-publishing-service - volumes: - - ipfs_data:/data/ipfs - - content-publishing-service-api: - pull_policy: never - image: content-publishing-service - build: - context: . - dockerfile: dev.Dockerfile - tags: - - content-publishing-service:latest - ports: - - 3000:3000 - env_file: - - .env.docker.dev - environment: - - START_PROCESS=api - volumes: - - ./:/app - depends_on: - - redis - - frequency - - ipfs - networks: - - content-publishing-service - - content-publishing-service-worker: - pull_policy: never - image: content-publishing-service - build: - context: . - dockerfile: dev.Dockerfile - tags: - - content-publishing-service:latest - env_file: - - .env.docker.dev - environment: - - START_PROCESS=worker - volumes: - - ./:/app - depends_on: - - redis - - frequency - - ipfs - networks: - - content-publishing-service - -volumes: - redis_data: - ipfs_data: - frequency_data: - -networks: - content-publishing-service: diff --git a/services/content-publishing/k6-test/README.md b/services/content-publishing/k6-test/README.md index 64a75216..5afbf52b 100644 --- a/services/content-publishing/k6-test/README.md +++ b/services/content-publishing/k6-test/README.md @@ -1,6 +1,6 @@ # Generated k6 script -The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. +The `script.k6.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. diff --git a/services/content-publishing/k6-test/health-check.k6.js b/services/content-publishing/k6-test/health-check.k6.js new file mode 100644 index 00000000..c7eb063b --- /dev/null +++ b/services/content-publishing/k6-test/health-check.k6.js @@ -0,0 +1,41 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable func-names */ +/* + * Account Service + * Account Service API + * + * OpenAPI spec version: 1.0 + * + * NOTE: This class is auto generated by OpenAPI Generator. + * https://github.com/OpenAPITools/openapi-generator + * + * Generator version: 7.7.0-SNAPSHOT + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 100, + duration: '10s', + thresholds: { + http_req_duration: ['avg<100', 'p(95)<200'], + }, + noConnectionReuse: true, +}; + +const BASE_URL = 'http://localhost:3000'; +// Sleep duration between successive requests. +const SLEEP_DURATION = 0.1; + +export default function () { + // Request No. 1: ApiController_health + // eslint-disable-next-line no-lone-blocks + const url = `${BASE_URL}/healthz`; + const request = http.get(url); + + check(request, { + 'Service is healthy': (r) => r.status === 200, + }); + sleep(SLEEP_DURATION); +} diff --git a/services/content-publishing/k6-test/script.js b/services/content-publishing/k6-test/script.k6.js similarity index 100% rename from services/content-publishing/k6-test/script.js rename to services/content-publishing/k6-test/script.k6.js diff --git a/services/content-publishing/libs/common/src/dtos/activity.dto.ts b/services/content-publishing/libs/common/src/dtos/activity.dto.ts index 053cb24c..668b6df2 100644 --- a/services/content-publishing/libs/common/src/dtos/activity.dto.ts +++ b/services/content-publishing/libs/common/src/dtos/activity.dto.ts @@ -43,7 +43,7 @@ export enum TagTypeDto { } // eslint-disable-next-line no-shadow -export enum AttachmentTypeDto { +export enum AttachmentType { LINK = 'link', IMAGE = 'image', AUDIO = 'audio', @@ -121,10 +121,7 @@ export class TagDto { } export class AssetDto { - @IsEnum(AttachmentTypeDto) - type: AttachmentTypeDto; - - @ValidateIf((o) => o.type !== AttachmentTypeDto.LINK) + @ValidateIf((o) => o.type !== AttachmentType.LINK) @ValidateNested({ each: true }) @IsArray() @ArrayNotEmpty() @@ -137,7 +134,7 @@ export class AssetDto { @MinLength(1) name?: string; - @ValidateIf((o) => o.type === AttachmentTypeDto.LINK) + @ValidateIf((o) => o.type === AttachmentType.LINK) @IsString() @MinLength(1) @IsUrl({ protocols: ['http', 'https'] }) diff --git a/services/content-publishing/libs/common/src/interfaces/asset-job.interface.ts b/services/content-publishing/libs/common/src/interfaces/asset-job.interface.ts index 3da2dc91..cf38549b 100644 --- a/services/content-publishing/libs/common/src/interfaces/asset-job.interface.ts +++ b/services/content-publishing/libs/common/src/interfaces/asset-job.interface.ts @@ -1,3 +1,5 @@ +import { AttachmentType } from '#libs/dtos'; + export interface IAssetJob { ipfsCid: string; mimeType: string; @@ -9,4 +11,5 @@ export interface IAssetMetadata { ipfsCid: string; mimeType: string; createdOn: number; + type: AttachmentType; } diff --git a/services/content-publishing/libs/common/src/interfaces/request-job.interface.ts b/services/content-publishing/libs/common/src/interfaces/request-job.interface.ts index c2822a92..98577b7d 100644 --- a/services/content-publishing/libs/common/src/interfaces/request-job.interface.ts +++ b/services/content-publishing/libs/common/src/interfaces/request-job.interface.ts @@ -1,10 +1,13 @@ -import { AnnouncementTypeDto, RequestTypeDto } from '#libs/dtos'; - +import { AnnouncementTypeDto, AttachmentType, RequestTypeDto } from '#libs/dtos'; +export interface IAssetTypeInfo { + mimeType: string; + attachmentType: AttachmentType; +} export interface IRequestJob { id: string; announcementType: AnnouncementTypeDto; dsnpUserId: string; - assetToMimeType?: Map; + assetToMimeType?: Map; content?: RequestTypeDto; dependencyAttempt: number; } diff --git a/services/content-publishing/package.json b/services/content-publishing/package.json index 34a05d60..73f8b4fb 100644 --- a/services/content-publishing/package.json +++ b/services/content-publishing/package.json @@ -22,16 +22,11 @@ "start:worker:dev": "set -a ; . .env ; nest start worker --watch", "start:api:debug": "set -a ; . .env ; nest start api --debug --watch", "start:worker:debug": "set -a ; . .env ; nest start worker --debug --watch", - "docker:build": "docker build -t content-publishing-service .", - "docker:build:dev": "docker-compose build", - "docker:run": "docker build -t content-publishing-service-deploy . ; docker run -p 6379:6379 --env-file .env content-publishing-service-deploy", - "docker:run:dev": "docker-compose up -d ; docker-compose logs -f content-publishing-service", - "docker:stop:dev": "docker-compose stop", "clean": "rm -Rf dist", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "pretest": "cp env.template .env", "test": "jest --coverage --verbose", - "test:k6:script": "k6 run k6-test/script.js", + "test:k6:script": "k6 run k6-test/script.k6.js", "test:k6:script-sm": "k6 run k6-test/script_sm_files.js", "test:k6:script-md": "k6 run k6-test/script_md_files.js", "test:k6:script-lg": "k6 run k6-test/script_lg_files.js", diff --git a/services/content-publishing/swagger.json b/services/content-publishing/swagger.json index 1fea1330..99100aec 100644 --- a/services/content-publishing/swagger.json +++ b/services/content-publishing/swagger.json @@ -322,96 +322,6 @@ "health" ] } - }, - "/dev/request/{jobId}": { - "get": { - "operationId": "DevelopmentControllerV1_requestJob", - "summary": "Get a Job given a jobId", - "description": "ONLY enabled when ENVIRONMENT=\"dev\".", - "parameters": [ - { - "name": "jobId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "dev" - ] - } - }, - "/dev/asset/{assetId}": { - "get": { - "operationId": "DevelopmentControllerV1_getAsset", - "summary": "Get an Asset given an assetId", - "description": "ONLY enabled when ENVIRONMENT=\"dev\".", - "parameters": [ - { - "name": "assetId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "2XX": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Buffer" - } - } - } - } - }, - "tags": [ - "dev" - ] - } - }, - "/dev/dummy/announcement/{queueType}/{count}": { - "post": { - "operationId": "DevelopmentControllerV1_populate", - "summary": "Create dummy announcement data", - "description": "ONLY enabled when ENVIRONMENT=\"dev\".", - "parameters": [ - { - "name": "queueType", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "count", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "" - } - }, - "tags": [ - "dev" - ] - } } }, "info": { @@ -493,15 +403,6 @@ "AssetDto": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "link", - "image", - "audio", - "video" - ] - }, "references": { "type": "array", "items": { @@ -516,10 +417,7 @@ "type": "string", "minLength": 1 } - }, - "required": [ - "type" - ] + } }, "TagDto": { "type": "object", @@ -759,10 +657,6 @@ "required": [ "profile" ] - }, - "Buffer": { - "type": "object", - "properties": {} } } } diff --git a/services/content-watcher/.env.docker.dev b/services/content-watcher/.env.docker.dev deleted file mode 100644 index 32474bf2..00000000 --- a/services/content-watcher/.env.docker.dev +++ /dev/null @@ -1,39 +0,0 @@ -# Tweak values for local development -# Content Publishing Service in Docker Compose will override these values with `.env.content-publishing-service` - -# URL to IPFS endpoint -# IPFS_ENDPOINT="https://ipfs.infura.io:5001" -IPFS_ENDPOINT="http://ipfs:5001" - -# If using Infura with auth required for read access, put Project ID here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_USER= - -# If using Infura with auth required for read access, put auth token here, or leave blank for Kubo RPC -# IPFS_BASIC_AUTH_SECRET= - -# IPFS gateway URL. '[CID]' is a token that will be replaced with an actual content ID -# IPFS_GATEWAY_URL="https://ipfs.io/ipfs/[CID]" -IPFS_GATEWAY_URL="http://ipfs:8080/ipfs/[CID]" - -# Blockchain node address -FREQUENCY_URL=ws://frequency:9944 - -# Redis URL -REDIS_URL=redis://redis:6379 - -# How many seconds to delay between successive scans of the chain -# for new content (after end of chain is reached) -BLOCKCHAIN_SCAN_INTERVAL_SECONDS=12 - -# Max number of jobs allowed on the queue before -# blockchain scan will be paused to allow queue to drain -QUEUE_HIGH_WATER=1000 - -# Number of retry attempts if a registered webhook call fails -WEBHOOK_FAILURE_THRESHOLD=4 - -# Number of seconds between webhook retry attempts when failing -WEBHOOK_RETRY_INTERVAL_SECONDS=10 - -# Port that the application REST endpoints listen on -API_PORT=3000 diff --git a/services/content-watcher/Dockerfile b/services/content-watcher/Dockerfile deleted file mode 100644 index 2922244d..00000000 --- a/services/content-watcher/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Use a multi-stage build for efficiency -FROM node:20 AS builder - -WORKDIR /app - -COPY package*.json ./ - -RUN npm ci - -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node:20 - -WORKDIR /app - -COPY --from=builder /app/dist ./dist -COPY package*.json ./ - -RUN npm ci --omit=dev - -# We want jq and curl in the final image, but we don't need the support files -RUN apt-get update && \ - apt-get install -y jq curl tini && \ - apt-get clean && \ - rm -rf /usr/share/doc /usr/share/man /usr/share/zsh - -EXPOSE 3000 - -ENTRYPOINT ["/usr/bin/tini", "--", "node", "dist/apps/api/main.js"] diff --git a/services/content-watcher/INSTALLING.md b/services/content-watcher/INSTALLING.md index 21503830..ada5f64c 100644 --- a/services/content-watcher/INSTALLING.md +++ b/services/content-watcher/INSTALLING.md @@ -9,9 +9,9 @@ The application requires a Redis server that is configured with `Append-only fil ### Standalone (complete) image -The standalone container image is meant to be a complete solution for a provider. It contains a single instance of the main application, plus a pre-configured Redis server. Simply download the latest [container image](https://hub.docker.com/r/amplicalabs/content-watcher-service/) and deploy using your favorite container management system. +The standalone container image is meant to be a complete solution for a provider. It contains a single instance of the main application, plus a pre-configured Redis server. Simply download the latest [container image](https://hub.docker.com/r/projectlibertylabs/content-watcher-service/) and deploy using your favorite container management system. ``` - docker pull amplicalabs/content-watcher-service:standalone-latest + docker pull projectlibertylabs/content-watcher-service:standalone-latest ``` The internal Redis server included in the complete image is already configured for persistence; it is simply necessary to configure your container pod to map the directory `/var/lib/redis` to a persistent storage volume. @@ -22,9 +22,9 @@ Follow the instructions below for [configuration](#configuration), with the exce ### App-only image -The app-only image is meant to be used for providers who would rather utilize a Redis instance in their own (or their cloud infrastructure provider's) external Redis instance or service. To download the latest [container image](https://hub.docker.com/r/amplicalabs/content-watcher-service/), simply: +The app-only image is meant to be used for providers who would rather utilize a Redis instance in their own (or their cloud infrastructure provider's) external Redis instance or service. To download the latest [container image](https://hub.docker.com/r/projectlibertylabs/content-watcher-service/), simply: ``` - docker pull amplicalabs/content-watcher-service:apponly-latest + docker pull projectlibertylabs/content-watcher-service:apponly-latest ``` In this case, you need to ensure that the following settings are configured in your Redis instance: ``` diff --git a/services/content-watcher/apps/api/test/app.e2e-spec.ts b/services/content-watcher/apps/api/test/app.e2e-spec.ts index 08b5c39e..5cba09fb 100644 --- a/services/content-watcher/apps/api/test/app.e2e-spec.ts +++ b/services/content-watcher/apps/api/test/app.e2e-spec.ts @@ -25,7 +25,7 @@ describe('Content Watcher E2E request verification!', () => { }); }, 15000); - it('(GET) /api/health', () => request(WATCHER_URI).get('/api/health').expect(200).expect({ status: 200 })); + it('(GET) /healthz', () => request(WATCHER_URI).get('/healthz').expect(200).expect({ status: 200 })); it('(Post) /api/resetScanner', async () => { const resetScannerDto: ResetScannerDto = { diff --git a/services/content-watcher/dev.Dockerfile b/services/content-watcher/dev.Dockerfile deleted file mode 100644 index cb942c5b..00000000 --- a/services/content-watcher/dev.Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# syntax=docker/dockerfile:1 - -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Dockerfile reference guide at -# https://docs.docker.com/go/dockerfile-reference/ - -# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 - -ARG NODE_VERSION=20 - -FROM node:${NODE_VERSION}-alpine - -WORKDIR /app - -# Run the application as a non-root user. -USER node - -# Expose the port that the application listens on. -EXPOSE 3000 - -# Run the application. -ENTRYPOINT [ "npm", "run", "start:watch" ] diff --git a/services/content-watcher/docker-compose.yaml b/services/content-watcher/docker-compose.yaml deleted file mode 100644 index dc272b92..00000000 --- a/services/content-watcher/docker-compose.yaml +++ /dev/null @@ -1,120 +0,0 @@ -x-content-watcher-environment: &content-watcher-environment - IPFS_GATEWAY_URL: 'https://ipfs:8080/ipfs/[CID]' - FREQUENCY_URL: 'ws://frequency:9944' - STARTING_BLOCK: 1 - REDIS_URL: 'redis://redis:6379' - BLOCKCHAIN_SCAN_INTERVAL_SECONDS: 12 - QUEUE_HIGH_WATER: 1000 - WEBHOOK_FAILURE_THRESHOLD: 4 - WEBHOOK_RETRY_INTERVAL_SECONDS: 10 - API_PORT: 3000 - CAPACITY_LIMIT: '{"type":"percentage", "value":80}' - IPFS_ENDPOINT: 'http://ipfs:5001' - -services: - redis: - image: redis:latest - ports: - - 6379:6379 - volumes: - - redis_data:/data/redis - networks: - - content-watcher-service - - frequency: - image: dsnp/instant-seal-node-with-deployed-schemas:latest - # We need to specify the platform because it's the only image - # built by Frequency at the moment, and auto-pull won't work otherwise - platform: linux/amd64 - # Uncomment SEALING_MODE and SEALING_INTERVAL if you want to use interval sealing. - # Other options you may want to add depending on your test scenario. - environment: - - SEALING_MODE=interval - - SEALING_INTERVAL=1 - # - CREATE_EMPTY_BLOCKS=true - # Uncomment below if you want to let the chain run and keep all of the historical blocks - # command: --state-pruning=archive - ports: - - 9944:9944 - networks: - - content-watcher-service - container_name: frequency - volumes: - - chainstorage:/data - - ipfs: - image: ipfs/kubo:latest - ports: - - 4001:4001 - - 5001:5001 - - 8080:8080 - networks: - - content-watcher-service - volumes: - - ipfs_data:/data/ipfs - - content-publishing-service-api: - image: amplicalabs/content-publishing-service:latest - # For now, this is the only platform image published. - platform: linux/amd64 - ports: - - 3001:3000 - env_file: - - .env.docker.dev - - .env.content-publishing-service - environment: - - START_PROCESS=api - depends_on: - - redis - - frequency - - ipfs - networks: - - content-watcher-service - - content-publishing-service-worker: - image: amplicalabs/content-publishing-service:latest - # For now, this is the only platform image published. - platform: linux/amd64 - env_file: - - .env.docker.dev - - .env.content-publishing-service - environment: - - START_PROCESS=worker - depends_on: - - redis - - frequency - - ipfs - networks: - - content-watcher-service - - content-watcher-service: - pull_policy: never - image: content-watcher-service - build: - context: . - dockerfile: dev.Dockerfile - tags: - - content-watcher-service:latest - environment: - <<: *content-watcher-environment - ports: - - 3000:3000 - volumes: - - ./:/app - depends_on: - - redis - - frequency - - ipfs - - content-publishing-service-api - - content-publishing-service-worker - networks: - - content-watcher-service - -volumes: - redis_data: - ipfs_data: - chainstorage: - external: false - -networks: - content-watcher-service: diff --git a/services/content-watcher/k6-test/README.md b/services/content-watcher/k6-test/README.md new file mode 100644 index 00000000..e312f96f --- /dev/null +++ b/services/content-watcher/k6-test/README.md @@ -0,0 +1,25 @@ +# Generated k6 script + +The `health-check.k6.js` file contains a simple health check script that can be used to check the health of the service. + +Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. + +If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and for more information about adding examples. + +k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check. + +[Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue. + +Each request is always followed by a 0.1 second [sleep](https://docs.k6.io/docs/sleep-t-1) to prevent the script execution from flooding the system with too many requests simultaneously. + +Note that the default iteration count and VU count is 1. So each request in each group will be executed once. For more information, see the [k6 options](https://docs.k6.io/docs/options). + +## Running the script + +To run the script, you need to have k6 installed. You can download it from [here](https://k6.io/docs/getting-started/installation). + +To run the script, execute the following command: + +```bash +k6 run health-check.k6.js +``` diff --git a/services/content-watcher/k6-test/health-check.k6.js b/services/content-watcher/k6-test/health-check.k6.js new file mode 100644 index 00000000..c7eb063b --- /dev/null +++ b/services/content-watcher/k6-test/health-check.k6.js @@ -0,0 +1,41 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable func-names */ +/* + * Account Service + * Account Service API + * + * OpenAPI spec version: 1.0 + * + * NOTE: This class is auto generated by OpenAPI Generator. + * https://github.com/OpenAPITools/openapi-generator + * + * Generator version: 7.7.0-SNAPSHOT + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 100, + duration: '10s', + thresholds: { + http_req_duration: ['avg<100', 'p(95)<200'], + }, + noConnectionReuse: true, +}; + +const BASE_URL = 'http://localhost:3000'; +// Sleep duration between successive requests. +const SLEEP_DURATION = 0.1; + +export default function () { + // Request No. 1: ApiController_health + // eslint-disable-next-line no-lone-blocks + const url = `${BASE_URL}/healthz`; + const request = http.get(url); + + check(request, { + 'Service is healthy': (r) => r.status === 200, + }); + sleep(SLEEP_DURATION); +} diff --git a/services/content-watcher/k6-test/package-lock.json b/services/content-watcher/k6-test/package-lock.json new file mode 100644 index 00000000..4d22478e --- /dev/null +++ b/services/content-watcher/k6-test/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "k6-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "k6-scripts", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/k6": "^0.51.0" + } + }, + "node_modules/@types/k6": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@types/k6/-/k6-0.51.0.tgz", + "integrity": "sha512-xelcvFGPI4VYrV5ozADmRuFQBKmDqDRzxfHVuCDD1/firZiSQvTP0pntxHuYUSkRyL8I83kvABXUlnLYNT2VuA==", + "dev": true + } + } +} diff --git a/services/content-watcher/k6-test/package.json b/services/content-watcher/k6-test/package.json new file mode 100644 index 00000000..c679dbc6 --- /dev/null +++ b/services/content-watcher/k6-test/package.json @@ -0,0 +1,15 @@ +{ + "name": "k6-scripts", + "version": "1.0.0", + "description": "The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs.", + "main": "health-check.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/k6": "^0.51.0" + } +} diff --git a/services/content-watcher/package.json b/services/content-watcher/package.json index d4357592..db9eea7b 100644 --- a/services/content-watcher/package.json +++ b/services/content-watcher/package.json @@ -14,16 +14,11 @@ "generate:metadata": "set -a ; . ./env.template ; npx ts-node apps/api/src/generate-metadata.ts", "generate:swagger-ui": "set -a ; . ./env.template ; npx --yes @redocly/cli build-docs swagger.json --output=./docs/index.html", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", - "docker:build": "docker build -t content-watcher-service .", - "docker:build:dev": "docker-compose build", - "docker:run": " build -t content-watcher-service-deploy . ; docker run -p 6379:6379 --env-file .env content-watcher-service-deploy", - "docker:run:dev": "docker-compose up -d ; docker-compose logs", - "docker:stop:dev": "docker-compose stop", "clean": "rm -Rf dist", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --coverage --verbose", "test:e2e": "set -a ; . ./.env; jest --config ./apps/api/test/jest-e2e.json --detectOpenHandles", - "test:k6": "echo 'TODO: K6 tests not implemented for content-watcher'", + "test:k6": "k6 run k6-test/health-check.k6.js", "local:init": "node scripts/chain-setup/local-chain-setup.cjs", "local:publish": "cd scripts/content-setup && npm i && npm run main", "local:webhook": "node scripts/webhook-cat.cjs", diff --git a/services/graph/Dockerfile b/services/graph/Dockerfile deleted file mode 100644 index 5c152b81..00000000 --- a/services/graph/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# Use a multi-stage build for efficiency -FROM node:20 AS builder - -WORKDIR /app - -COPY package*.json ./ - -RUN npm ci - -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node:20 - -WORKDIR /app - -COPY --from=builder /app/dist ./dist -COPY package*.json ./ -COPY ./lua ./lua -COPY ./scripts/docker-entrypoint.sh ./ -RUN chmod +x ./docker-entrypoint.sh - -RUN apt-get update && \ - apt-get install -y jq curl tini && \ - apt-get clean && \ - rm -rf /usr/share/doc /usr/share/man /usr/share/zsh - -RUN npm ci --omit=dev - -EXPOSE 3000 - -ENV START_PROCESS="api" - -ENTRYPOINT ["/usr/bin/tini", "--", "./docker-entrypoint.sh", "prod"] diff --git a/services/graph/dev.Dockerfile b/services/graph/dev.Dockerfile deleted file mode 100644 index 58fbf213..00000000 --- a/services/graph/dev.Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:20 - -RUN apt-get update && \ - apt-get install -y tini && \ - apt-get clean && \ - rm -rf /usr/share/doc /usr/share/man /usr/share/zsh - - -WORKDIR /app - -EXPOSE 3000 - -ENV START_PROCESS="api" - -VOLUME "/app" diff --git a/services/graph/docker-compose.yaml b/services/graph/docker-compose.yaml deleted file mode 100644 index 6a0f3fd9..00000000 --- a/services/graph/docker-compose.yaml +++ /dev/null @@ -1,94 +0,0 @@ -x-graph-service-environment: &graph-service-environment - FREQUENCY_URL: ws://frequency:9944 - REDIS_URL: redis://redis:6379 - QUEUE_HIGH_WATER: 1000 - API_PORT: 3000 - DEBOUNCE_SECONDS: 10 - CAPACITY_LIMIT: '{"type": "percentage", "value": 80 }' - GRAPH_ENVIRONMENT_TYPE: Mainnet - PROVIDER_ACCOUNT_SEED_PHRASE: '//Alice' - PROVIDER_ID: 1 - RECONNECTION_SERVICE_REQUIRED: false - WEBHOOK_FAILURE_THRESHOLD: 3 - WEBHOOK_RETRY_INTERVAL_SECONDS: 10 - HEALTH_CHECK_MAX_RETRY_INTERVAL_SECONDS: 10 - HEALTH_CHECK_MAX_RETRIES: 4 - -services: - redis: - image: redis:latest - ports: - - 6379:6379 - volumes: - - redis_data:/data/redis - networks: - - graph-service - - frequency: - image: dsnp/instant-seal-node-with-deployed-schemas:latest - # We need to specify the platform because it's the only image - # built by Frequency at the moment, and auto-pull won't work otherwise - platform: linux/amd64 - # Uncomment SEALING_MODE and SEALING_INTERVAL if you want to use interval sealing. - # Other options you may want to add depending on your test scenario. - environment: - - SEALING_MODE=interval - - SEALING_INTERVAL=1 - # - CREATE_EMPTY_BLOCKS=true - # Uncomment below if you want to let the chain run and keep all of the historical blocks - # command: --state-pruning=archive - ports: - - 9944:9944 - networks: - - graph-service - volumes: - - chainstorage:/data - - graph-service-base: - pull_policy: never - image: graph-service-dev:latest - build: - context: . - dockerfile: dev.Dockerfile - tags: - - graph-service-dev:latest - - api: - pull_policy: never - image: graph-service-dev:latest - ports: - - 3000:3000 - command: ['npm', 'run', 'start:api:watch'] - environment: - <<: [*graph-service-environment] - volumes: - - ./:/app - depends_on: - - redis - - frequency - networks: - - graph-service - restart: on-failure - - worker: - image: graph-service-dev:latest - pull_policy: never - environment: - <<: [*graph-service-environment] - volumes: - - ./:/app - depends_on: - - redis - - frequency - - graph-service-base - networks: - - graph-service - restart: on-failure - -volumes: - redis_data: - chainstorage: - external: false - -networks: - graph-service: diff --git a/services/graph/k6-test/README.md b/services/graph/k6-test/README.md index 64a75216..5afbf52b 100644 --- a/services/graph/k6-test/README.md +++ b/services/graph/k6-test/README.md @@ -1,6 +1,6 @@ # Generated k6 script -The `script.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. +The `script.k6.js` file contains most of the Swagger/OpenAPI specification and you can customize it to your needs. Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query. diff --git a/services/graph/k6-test/health-check.k6.js b/services/graph/k6-test/health-check.k6.js new file mode 100644 index 00000000..c7eb063b --- /dev/null +++ b/services/graph/k6-test/health-check.k6.js @@ -0,0 +1,41 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable func-names */ +/* + * Account Service + * Account Service API + * + * OpenAPI spec version: 1.0 + * + * NOTE: This class is auto generated by OpenAPI Generator. + * https://github.com/OpenAPITools/openapi-generator + * + * Generator version: 7.7.0-SNAPSHOT + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 100, + duration: '10s', + thresholds: { + http_req_duration: ['avg<100', 'p(95)<200'], + }, + noConnectionReuse: true, +}; + +const BASE_URL = 'http://localhost:3000'; +// Sleep duration between successive requests. +const SLEEP_DURATION = 0.1; + +export default function () { + // Request No. 1: ApiController_health + // eslint-disable-next-line no-lone-blocks + const url = `${BASE_URL}/healthz`; + const request = http.get(url); + + check(request, { + 'Service is healthy': (r) => r.status === 200, + }); + sleep(SLEEP_DURATION); +} diff --git a/services/graph/package-lock.json b/services/graph/package-lock.json index 52d3e42a..b2563533 100644 --- a/services/graph/package-lock.json +++ b/services/graph/package-lock.json @@ -76,6 +76,7 @@ } }, "../../packages/ts-config": { + "name": "@amplica-labs/ts-config", "version": "1.0.0", "dev": true, "license": "Apache-2.0" diff --git a/services/graph/package.json b/services/graph/package.json index e3af6f3b..b1eb33ee 100644 --- a/services/graph/package.json +++ b/services/graph/package.json @@ -21,15 +21,13 @@ "start:worker:prod": "node dist/apps/worker/main.js", "start:worker:dev": "set -a ; . .env ; nest start worker", "start:worker:debug": "set -a ; . .env ;nest start worker --debug=9230 --watch", - "docker:build": "docker build -t graph-service .", - "docker:run": "docker build -t graph-service-deploy . ; docker run --env-file .env graph-service-deploy", "chain-setup": "tsx ./test-setup/main.ts", "clean": "rm -Rf dist", "lint": "eslint \"{apps,libs}/**/*.ts\" --fix", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "test": "jest --coverage --verbose", "test:e2e": "set -a ; . .env ; jest --testRegex \".e2e-spec.ts$\" --detectOpenHandles", - "test:k6": "k6 run k6-test/script.js" + "test:k6": "k6 run k6-test/script.k6.js" }, "repository": { "type": "git", diff --git a/start-gateway.sh b/start-gateway.sh new file mode 100755 index 00000000..ec6ce2ea --- /dev/null +++ b/start-gateway.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# Script to start all Gateway services on the Frequency Paseo Testnet + +# Function to ask for input with a default value and write to .env-saved +ask_and_save() { + local var_name=${1} + local prompt=${2} + local default_value=${3} + read -rp $'\n'"${prompt} [${default_value}]: " input + local value=${input:-$default_value} + echo "${var_name}=\"${value}\"" >> .env-saved +} + +# Check for Docker and Docker Compose +if ! command -v docker &> /dev/null || ! command -v docker compose &> /dev/null; then + printf "Docker and Docker Compose are required but not installed. Please install them and try again.\n" + exit 1 +fi + +# Load existing .env-saved file if it exists +if [ -f .env-saved ]; then + echo -e "Found saved environment from a previous run:\n" + cat .env-saved + echo + read -p "Do you want to re-use the saved paramters? [y/N]: " REUSE_SAVED + + if [[ ${REUSE_SAVED} =~ ^[Yy] ]] + then + cat << EOI +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Loading existing .env-saved file environment values... ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +EOI + else + cat << EOI +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Removing previous saved environment... | +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +EOI + rm .env-saved + fi +fi + +if [ ! -f .env-saved ] +then + # Setup some variables for easy port management + STARTING_PORT=3010 + for i in {0..10} + do + eval SERVICE_PORT_${i}=$(( STARTING_PORT + i )) + eval "export SERVICE_PORT_${i}=\${SERVICE_PORT_${i}}" + eval "echo SERVICE_PORT_${i}=\${SERVICE_PORT_${i}}" >> .env-saved + done + + # Create .env-saved file to store environment variables + cat << EOI +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Creating .env-saved file to store environment variables... ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +EOI + echo "COMPOSE_PROJECT_NAME='gateway-dev'" >> .env-saved + # Ask the user if they want to start on testnet or local + read -p "Do you want to start on Frequency Paseo Testnet [y/N]: " TESTNET_ENV + echo "TESTNET_ENV=\"$TESTNET_ENV\"" >> .env-saved + + if [[ $TESTNET_ENV =~ ^[Yy]$ ]] + then + cat << EOI + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Setting defaults for testnet... ┃ +┃ Hit to accept the default value or enter new value and then hit ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +EOI + DEFAULT_TESTNET_ENV="testnet" + DEFAULT_FREQUENCY_URL="wss://0.rpc.testnet.amplica.io" + DEFAULT_FREQUENCY_HTTP_URL="https://0.rpc.testnet.amplica.io" + DEFAULT_PROVIDER_ID="729" + DEFAULT_PROVIDER_ACCOUNT_SEED_PHRASE="DEFAULT seed phrase needed" + DEFAULT_IPFS_VOLUME="/data/ipfs" + else + echo -e "\nStarting on local..." + DEFAULT_TESTNET_ENV="local" + DEFAULT_FREQUENCY_URL="ws://frequency:9944" + DEFAULT_FREQUENCY_HTTP_URL="http://localhost:9944" + DEFAULT_PROVIDER_ID="1" + DEFAULT_PROVIDER_ACCOUNT_SEED_PHRASE="//Alice" + DEFAULT_IPFS_VOLUME="/data/ipfs" + fi + DEFAULT_IPFS_ENDPOINT="http://ipfs:5001" + DEFAULT_IPFS_GATEWAY_URL='https://ipfs.io/ipfs/[CID]' + DEFAULT_IPFS_BASIC_AUTH_USER="" + DEFAULT_IPFS_BASIC_AUTH_SECRET="" + DEFAULT_IPFS_UA_GATEWAY_URL="http://localhost:8080" + DEFAULT_CONTENT_DB_VOLUME="content_db" + + + ask_and_save FREQUENCY_URL "Enter the Frequency Testnet RPC URL" "$DEFAULT_FREQUENCY_URL" + ask_and_save FREQUENCY_HTTP_URL "Enter the Frequency HTTP Testnet RPC URL" "$DEFAULT_FREQUENCY_HTTP_URL" +cat << EOI + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🔗💠📡 📡💠🔗 ┃ +┃ 🔗💠📡 A Provider is required to start the services. 📡💠🔗 ┃ +┃ 🔗💠📡 📡💠🔗 ┃ +┃ 🔗💠📡 If you need to become a provider, visit 📡💠🔗 ┃ +┃ 🔗💠📡 https://provider.frequency.xyz/ to get a Provider ID. 📡💠🔗 ┃ +┃ 🔗💠📡 📡💠🔗 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +EOI + ask_and_save PROVIDER_ID "Enter Provider ID" "$DEFAULT_PROVIDER_ID" + ask_and_save PROVIDER_ACCOUNT_SEED_PHRASE "Enter Provider Seed Phrase" "$DEFAULT_PROVIDER_ACCOUNT_SEED_PHRASE" + cat << EOI + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ IPFS settings ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +EOI + read -p "Do you want to change the IPFS settings? [y/N]: " CHANGE_IPFS_SETTINGS + + if [[ $CHANGE_IPFS_SETTINGS =~ ^[Yy]$ ]] + then + ask_and_save IPFS_VOLUME "Enter the IPFS volume" "$DEFAULT_IPFS_VOLUME" + ask_and_save IPFS_ENDPOINT "Enter the IPFS Endpoint" "$DEFAULT_IPFS_ENDPOINT" + ask_and_save IPFS_GATEWAY_URL "Enter the IPFS Gateway URL" "$DEFAULT_IPFS_GATEWAY_URL" + ask_and_save IPFS_BASIC_AUTH_USER "Enter the IPFS Basic Auth User" "$DEFAULT_IPFS_BASIC_AUTH_USER" + ask_and_save IPFS_BASIC_AUTH_SECRET "Enter the IPFS Basic Auth Secret" "$DEFAULT_IPFS_BASIC_AUTH_SECRET" + ask_and_save IPFS_UA_GATEWAY_URL "Enter the browser-resolveable IPFS Gateway URL" "$DEFAULT_IPFS_UA_GATEWAY_URL" + else + cat >> .env-saved << EOI +IPFS_VOLUME="${DEFAULT_IPFS_VOLUME}" +IPFS_ENDPOINT="${DEFAULT_IPFS_ENDPOINT}" +IPFS_GATEWAY_URL="${DEFAULT_IPFS_GATEWAY_URL}" +IPFS_BASIC_AUTH_USER="${DEFAULT_IPFS_BASIC_AUTH_USER}" +IPFS_BASIC_AUTH_SECRET="${DEFAULT_IPFS_BASIC_AUTH_SECRET}" +IPFS_UA_GATEWAY_URL="${DEFAULT_IPFS_UA_GATEWAY_URL}" +EOI + fi +fi +set -a; source .env-saved; set +a + +if [[ ! $TESTNET_ENV =~ ^[Yy]$ ]] +then + # Start specific services in detached mode + echo -e "\nStarting local frequency services..." + docker compose up -d frequency + + # Wait for 15 seconds + echo "Waiting 15 seconds for Frequency to be ready..." + sleep 15 + + # Run npm run local:init + # echo "Running npm run local:init to provision Provider with capacity, etc..." + # cd backend && npm run local:init && cd .. +fi + +# Start all services in detached mode +echo -e "\nStarting all services..." +docker compose --profile backend up -d + +cat << EOI + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🚀 You can access the Gateway at the following local addresses: 🚀 ┃ +┃ * account-service: ┃ +┃ - API: http://localhost:${SERVICE_PORT_3} ┃ +┃ - Queue management: http://localhost:${SERVICE_PORT_3}/queues ┃ +┃ - Swagger UI: http://localhost:${SERVICE_PORT_3}/docs/swagger ┃ +┃ ┃ +┃ * content-publishing-service ┃ +┃ - API: http://localhost:${SERVICE_PORT_0} ┃ +┃ - Queue management: http://localhost:${SERVICE_PORT_0}/queues ┃ +┃ - Swagger UI: http://localhost:${SERVICE_PORT_0}/docs/swagger ┃ +┃ ┃ +┃ * content-watcher-service ┃ +┃ - API: http://localhost:${SERVICE_PORT_1} ┃ +┃ - Queue management: http://localhost:${SERVICE_PORT_1}/queues ┃ +┃ - Swagger UI: http://localhost:${SERVICE_PORT_1}/docs/swagger ┃ +┃ ┃ +┃ * graph-service ┃ +┃ - API: http://localhost:${SERVICE_PORT_2} ┃ +┃ - Queue management: http://localhost:${SERVICE_PORT_2}/queues ┃ +┃ - Swagger UI: http://localhost:${SERVICE_PORT_2}/docs/swagger ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +EOI diff --git a/stop-gateway.sh b/stop-gateway.sh new file mode 100755 index 00000000..eae7db39 --- /dev/null +++ b/stop-gateway.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Stop all services and optionally remove specified volumes to remove all state and start fresh + +# Export the variables that are used in the docker-compose.yaml file +if [ -f .env-saved ]; then + set -a; source .env-saved; set +a +fi + +# Shutting down any running services +echo "Shutting down any running services..." +docker compose --profile local-node --profile backend --profile frontend down + +# Ask the user if they want to remove specified volumes +read -p "Do you want to remove specified volumes to remove all state and start fresh? [y/N]: " REMOVE_VOLUMES + +if [[ $REMOVE_VOLUMES =~ ^[Yy]$ ]] +then + echo "Removing specified volumes..." + # Docker volume names are lowercase versions of the directory name + # In the root directory of the repository, we get from the system directory name + docker volume rm ${COMPOSE_PROJECT_NAME}_redis_data + docker volume rm ${COMPOSE_PROJECT_NAME}_ipfs_data + docker volume rm ${COMPOSE_PROJECT_NAME}_account_node_cache + docker volume rm ${COMPOSE_PROJECT_NAME}_graph_node_cache + docker volume rm ${COMPOSE_PROJECT_NAME}_content_publishing_node_cache + docker volume rm ${COMPOSE_PROJECT_NAME}_content_watcher_node_cache + if [[ ! $TESTNET_ENV =~ ^[Yy]$ ]] + then + docker volume rm ${COMPOSE_PROJECT_NAME}_chainstorage + fi +else + echo "Leaving Docker volumes alone." +fi diff --git a/tools/ci-k6/main.mjs b/tools/ci-k6/main.mjs new file mode 100644 index 00000000..c43ba5d3 --- /dev/null +++ b/tools/ci-k6/main.mjs @@ -0,0 +1,52 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { Keyring } from '@polkadot/keyring'; + +const keyring = new Keyring({ type: 'sr25519' }); + +export async function createAndStake(providerUrl, keyUri) { + const api = await ApiPromise.create({ provider: new WsProvider(providerUrl) }); + + console.log('Connected...'); + + const account = keyring.createFromUri(keyUri); + + const call = api.tx.utility.batchAll([ + api.tx.msa.create(), + api.tx.msa.createProvider('alice'), + api.tx.capacity.stake(1, 10_000_000_000_000), + ]); + + console.log('Submitting call...'); + await new Promise(async (resolve, reject) => { + const unsub = await call.signAndSend(account, ({ status, events }) => { + if (status.isInBlock || status.isFinalized) { + console.log( + `Block hash: ${(status.isInBlock && status.asInBlock) || (status.isFinalized && status.asFinalized)}`, + ); + if (events) + console.log( + 'All Events', + events.map((x) => x.toHuman()), + ); + const success = events.find((x) => api.events.system.ExtrinsicSuccess.is(x.event)); + const failure = events.find((x) => api.events.system.ExtrinsicFailed.is(x.event)); + unsub(); + if (success && !failure) { + console.log('Success!'); + resolve(); + } else { + console.error('FAILED!'); + reject(); + } + } + }); + }); +} + +try { + await createAndStake('ws://localhost:9944', '//Alice'); + process.exit(0); +} catch (error) { + console.error('Error:', error); + process.exit(1); +} diff --git a/tools/ci-k6/package-lock.json b/tools/ci-k6/package-lock.json new file mode 100644 index 00000000..ff290439 --- /dev/null +++ b/tools/ci-k6/package-lock.json @@ -0,0 +1,852 @@ +{ + "name": "ci-k6", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ci-k6", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "^12.2.3", + "@polkadot/keyring": "^13.0.2" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz", + "integrity": "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA==", + "optional": true + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1.tgz", + "integrity": "sha512-gmVDUP8LpCH0BXewbzqXF2sdHddq1H1q+XrAW2of+KZj4woQkIGBRGTJHeBEVHe30EB+UejR1N2dT4PO/RvDdg==", + "optional": true + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1.tgz", + "integrity": "sha512-GCI78BHDzXAF/L2pZD6Aod/yl82adqQ7ftNmKg51ixRL02JpWUA+SpUKTJE5MY1p8kiJJIo09P2um24SiJHxNA==", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.0.1", + "@polkadot-api/utils": "0.0.1" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.1.0.tgz", + "integrity": "sha512-GBCGDRztKorTLna/unjl/9SWZcRmvV58o9jwU2Y038VuPXZcr01jcw/1O3x+yeAuwyGzbucI/mLTDa1QoEml3A==", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.0.1", + "@polkadot-api/substrate-bindings": "0.0.1", + "@polkadot-api/substrate-client": "0.0.1", + "@polkadot-api/utils": "0.0.1" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1.tgz", + "integrity": "sha512-bAe7a5bOPnuFVmpv7y4BBMRpNTnMmE0jtTqRUw/+D8ZlEHNVEJQGr4wu3QQCl7k1GnSV1wfv3mzIbYjErEBocg==", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.0.1", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.0.1.tgz", + "integrity": "sha512-9Bg9SGc3AwE+wXONQoW8GC00N3v6lCZLW74HQzqB6ROdcm5VAHM4CB/xRzWSUF9CXL78ugiwtHx3wBcpx4H4Wg==", + "optional": true + }, + "node_modules/@polkadot-api/utils": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.0.1.tgz", + "integrity": "sha512-3j+pRmlF9SgiYDabSdZsBSsN5XHbpXOAce1lWj56IEEaFZVjsiCaxDOA7C9nCcgfVXuvnbxqqEGQvnY+QfBAUw==", + "optional": true + }, + "node_modules/@polkadot/api": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-12.2.3.tgz", + "integrity": "sha512-qpC29Uq0JZh/7Spcvmw+jUREG/ZYeb7miGUKomqHqU1hwBvyk9bqy7Vr10g3Hh0bkl5nP29YmnrLrG0NG+EtPg==", + "dependencies": { + "@polkadot/api-augment": "12.2.3", + "@polkadot/api-base": "12.2.3", + "@polkadot/api-derive": "12.2.3", + "@polkadot/keyring": "^13.0.2", + "@polkadot/rpc-augment": "12.2.3", + "@polkadot/rpc-core": "12.2.3", + "@polkadot/rpc-provider": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/types-augment": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/types-create": "12.2.3", + "@polkadot/types-known": "12.2.3", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-12.2.3.tgz", + "integrity": "sha512-w3FYQAzVzZuD1xAUGwEeEftJr5N5oYigItrWkEc3nk+I3wUjNuHNlab3hCJZslRlHrE2zYVK5mGDDZYVPyn86Q==", + "dependencies": { + "@polkadot/api-base": "12.2.3", + "@polkadot/rpc-augment": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/types-augment": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-12.2.3.tgz", + "integrity": "sha512-fUJt3+uvBViwjz5tiiEE1VQkcDiXLzAPdex2OeECXopNnHt9gq8n6dS2arBzfG2eEDv/viCyjggj0wcSaV2yUg==", + "dependencies": { + "@polkadot/rpc-core": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/util": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-12.2.3.tgz", + "integrity": "sha512-zcQOuLoBeYXTMr2r9oPQiIJ7t4997eoQ1yM76KK2/2KTESKfJHus6nA0IK9fDk+c5vIdFKd/BJ0UukQ1AJiLLA==", + "dependencies": { + "@polkadot/api": "12.2.3", + "@polkadot/api-augment": "12.2.3", + "@polkadot/api-base": "12.2.3", + "@polkadot/rpc-core": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/keyring": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.0.2.tgz", + "integrity": "sha512-NeLbhyKDT5W8LI9seWTZGePxNTOVpDhv2018HSrEDwJq9Ie0C4TZhUf3KNERCkSveuThXjfQJMs+1CF33ZXPWw==", + "dependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/util-crypto": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/util-crypto": "13.0.2" + } + }, + "node_modules/@polkadot/networks": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-13.0.2.tgz", + "integrity": "sha512-ABAL+vug/gIwkdFEzeh87JoJd0YKrxSYg/HjUrZ+Zis2ucxQEKpvtCpJ34ku+YrjacBfVqIAkkwd3ZdIPGq9aQ==", + "dependencies": { + "@polkadot/util": "13.0.2", + "@substrate/ss58-registry": "^1.46.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-12.2.3.tgz", + "integrity": "sha512-3V+Xp5cGb8hA0YZ4V4jXdC0POZGirQ63DkUnypmq86Fa1A7NCuVgD+s9ayOc8kNUMuKJIRKr3cLTj97S6f15lw==", + "dependencies": { + "@polkadot/rpc-core": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-12.2.3.tgz", + "integrity": "sha512-XJyPpwYBe+ijlivEKcRYRlQ5vx/CUXG0PZ23/TLKMRNlh5BVAC4HK/4dzBmOc3FT0ulOMbu7/TH+mk7ppQHrKg==", + "dependencies": { + "@polkadot/rpc-augment": "12.2.3", + "@polkadot/rpc-provider": "12.2.3", + "@polkadot/types": "12.2.3", + "@polkadot/util": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-12.2.3.tgz", + "integrity": "sha512-hzw6YGV+3daU49rsEPmdl/UDupAmc3lqBYN2gj7lxQCMSqYjBr0Pj1ScGJJXzlR8ZyiY97e/TGIW13W6ivmIGQ==", + "dependencies": { + "@polkadot/keyring": "^13.0.2", + "@polkadot/types": "12.2.3", + "@polkadot/types-support": "12.2.3", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "@polkadot/x-fetch": "^13.0.2", + "@polkadot/x-global": "^13.0.2", + "@polkadot/x-ws": "^13.0.2", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.10" + } + }, + "node_modules/@polkadot/types": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-12.2.3.tgz", + "integrity": "sha512-p6y3WdZBvdgT5+m+gvPaHXUaei1DQjMI9BxhzHS5FfOvDMSDf0uBacamtRmkdII5bJuUgGBYG9BjHic8yWu0/g==", + "dependencies": { + "@polkadot/keyring": "^13.0.2", + "@polkadot/types-augment": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/types-create": "12.2.3", + "@polkadot/util": "^13.0.2", + "@polkadot/util-crypto": "^13.0.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-12.2.3.tgz", + "integrity": "sha512-RLHWl4TIgJqWFuGDgstKTYqB7EWGx4oJ5nzIdKCQgYAeOi+LFYXyZjE2ffhmX258VPsSXu4syeQpcBIEWns8kA==", + "dependencies": { + "@polkadot/types": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-12.2.3.tgz", + "integrity": "sha512-oBHAEXyAMZ6ghEEgKW95cc4OFdkxiRKazx18Dk433sWk2HGkwGoKd9uK6xdelMgO1EnbBzZwc2epOhKH7rTEmQ==", + "dependencies": { + "@polkadot/util": "^13.0.2", + "@polkadot/x-bigint": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-12.2.3.tgz", + "integrity": "sha512-4XR04QFgKeHZEj7NyBK3A55EgzmGZtC175Hbq5y3+j8XV84amOOhVqj7gDQqnSyRMAtl7+HSsfpx3+Loh+4l+g==", + "dependencies": { + "@polkadot/types-codec": "12.2.3", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-12.2.3.tgz", + "integrity": "sha512-hB3fBlZ51dBaGRJf6ParvoqCSig9ovqjDgpFwysewXsc74GRoPPR7RQFw/hITxwdKL5ldyTZnBIGBxROiF86Tg==", + "dependencies": { + "@polkadot/networks": "^13.0.2", + "@polkadot/types": "12.2.3", + "@polkadot/types-codec": "12.2.3", + "@polkadot/types-create": "12.2.3", + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-12.2.3.tgz", + "integrity": "sha512-/YVZ0j126el/5e/BTrhw1SuDmlyV394zKak7LkYcAJ8IyDmT53cajMK2TQe03uVsE/vveligkYmJ24IEjZ+DRg==", + "dependencies": { + "@polkadot/util": "^13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.0.2.tgz", + "integrity": "sha512-/6bS9sfhJLhs8QuqWaR1eRapzfDdGC5XAQZEPL9NN5sTTA7HxWos8rVleai0UERm8QUMabjZ9rK9KpzbXl7ojg==", + "dependencies": { + "@polkadot/x-bigint": "13.0.2", + "@polkadot/x-global": "13.0.2", + "@polkadot/x-textdecoder": "13.0.2", + "@polkadot/x-textencoder": "13.0.2", + "@types/bn.js": "^5.1.5", + "bn.js": "^5.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.0.2.tgz", + "integrity": "sha512-woUsJJ6zd/caL7U+D30a5oM/+WK9iNI00Y8aNUHSj6Zq/KPzK9uqDBaLGWwlgrejoMQkxxiU2X0f2LzP15AtQg==", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.0.2", + "@polkadot/util": "13.0.2", + "@polkadot/wasm-crypto": "^7.3.2", + "@polkadot/wasm-util": "^7.3.2", + "@polkadot/x-bigint": "13.0.2", + "@polkadot/x-randomvalues": "13.0.2", + "@scure/base": "^1.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.3.2.tgz", + "integrity": "sha512-AJEXChcf/nKXd5Q/YLEV5dXQMle3UNT7jcXYmIffZAo/KI394a+/24PaISyQjoNC0fkzS1Q8T5pnGGHmXiVz2g==", + "dependencies": { + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.3.2.tgz", + "integrity": "sha512-+neIDLSJ6jjVXsjyZ5oLSv16oIpwp+PxFqTUaZdZDoA2EyFRQB8pP7+qLsMNk+WJuhuJ4qXil/7XiOnZYZ+wxw==", + "dependencies": { + "@polkadot/wasm-bridge": "7.3.2", + "@polkadot/wasm-crypto-asmjs": "7.3.2", + "@polkadot/wasm-crypto-init": "7.3.2", + "@polkadot/wasm-crypto-wasm": "7.3.2", + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.2.tgz", + "integrity": "sha512-QP5eiUqUFur/2UoF2KKKYJcesc71fXhQFLT3D4ZjG28Mfk2ZPI0QNRUfpcxVQmIUpV5USHg4geCBNuCYsMm20Q==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.2.tgz", + "integrity": "sha512-FPq73zGmvZtnuJaFV44brze3Lkrki3b4PebxCy9Fplw8nTmisKo9Xxtfew08r0njyYh+uiJRAxPCXadkC9sc8g==", + "dependencies": { + "@polkadot/wasm-bridge": "7.3.2", + "@polkadot/wasm-crypto-asmjs": "7.3.2", + "@polkadot/wasm-crypto-wasm": "7.3.2", + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.2.tgz", + "integrity": "sha512-15wd0EMv9IXs5Abp1ZKpKKAVyZPhATIAHfKsyoWCEFDLSOA0/K0QGOxzrAlsrdUkiKZOq7uzSIgIDgW8okx2Mw==", + "dependencies": { + "@polkadot/wasm-util": "7.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.3.2.tgz", + "integrity": "sha512-bmD+Dxo1lTZyZNxbyPE380wd82QsX+43mgCm40boyKrRppXEyQmWT98v/Poc7chLuskYb6X8IQ6lvvK2bGR4Tg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.0.2.tgz", + "integrity": "sha512-h2jKT/UaxiEal8LhQeH6+GCjO7GwEqVAD2SNYteCOXff6yNttqAZYJuHZsndbVjVNwqRNf8D5q/zZkD0HUd6xQ==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-13.0.2.tgz", + "integrity": "sha512-B/gf9iriUr6za/Ui7zIFBfHz7UBZ68rJEIteWHx1UHRCZPcLqv+hgpev6xIGrkfFljI0/lI7IwtN2qy6HYzFBg==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "node-fetch": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-global": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.0.2.tgz", + "integrity": "sha512-OoNIXLB5y8vIKpk4R+XmpDPhipNXWSUvEwUnpQT7NAxNLmzgMq1FhbrwBWWPRNHPrQonp7mqxV/X+v5lv1HW/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.0.2.tgz", + "integrity": "sha512-SGj+L0H/7TWZtSmtkWlixO4DFzXDdluI0UscN2h285os2Ns8PnmBbue+iJ8PVSzpY1BOxd66gvkkpboPz+jXFQ==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.0.2", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.0.2.tgz", + "integrity": "sha512-mauglOkTJxLGmLwLc3J5Jlq/W+SHP53eiy3F8/8JxxfnXrZKgWoQXGpvXYPjFnMZj0MzDSy/6GjyGWnDCgdQFA==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.0.2.tgz", + "integrity": "sha512-Lq08H2OnVXj97uaOwg7tcmRS7a4VJYkHEeWO4FyEMOk6P6lU6W8OVNjjxG0se9PCEgmyZPUDbJI//1ynzP4cXw==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-13.0.2.tgz", + "integrity": "sha512-nC5e2eY5D5ZR5teQOB7ib+dWLbmNws86cTz3BjKCalSMBBIn6i3V9ElgABpierBmnSJe9D94EyrH1BxdVfDxUg==", + "dependencies": { + "@polkadot/x-global": "13.0.2", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scure/base": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@substrate/connect": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.10.tgz", + "integrity": "sha512-DIyQ13DDlXqVFnLV+S6/JDgiGowVRRrh18kahieJxhgvzcWicw5eLc6jpfQ0moVVLBYkO7rctB5Wreldwpva8w==", + "deprecated": "versions below 1.x are no longer maintained", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.4", + "@substrate/light-client-extension-helpers": "^0.0.6", + "smoldot": "2.0.22" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.0.0.tgz", + "integrity": "sha512-nKu8pDrE3LNCEgJjZe1iGXzaD6OSIDD4Xzz/yo4KO9mQ6LBvf49BVrt4qxBFGL6++NneLiWUZGoh+VSd4PyVIg==", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.2.1.tgz", + "integrity": "sha512-2EShoa4DlwSqEFVjGpt+2zNDzIgHQ/lcteP/GRisRkZ6KZDhr7A0Q3eCcbmXVmO65sWLcPGs/eAZez8kc5/SOw==", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-0.0.6.tgz", + "integrity": "sha512-girltEuxQ1BvkJWmc8JJlk4ZxnlGXc/wkLcNguhY+UoDEMBK0LsdtfzQKIfrIehi4QdeSBlFEFBoI4RqPmsZzA==", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.1", + "@polkadot-api/json-rpc-provider-proxy": "0.0.1", + "@polkadot-api/observable-client": "0.1.0", + "@polkadot-api/substrate-client": "0.0.1", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.4", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.49.0.tgz", + "integrity": "sha512-leW6Ix4LD7XgvxT7+aobPWSw+WvPcN2Rxof1rmd0mNC5t2n99k1N7UNEvz7YEFSOUeHWmKIY7F5q8KeIqYoHfA==" + }, + "node_modules/@types/bn.js": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scale-ts": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.0.tgz", + "integrity": "sha512-Ja5VCjNZR8TGKhUumy9clVVxcDpM+YFjAnkMuwQy68Hixio3VRRvWdE3g8T/yC+HXA0ZDQl2TGyUmtmbcVl40Q==", + "optional": true + }, + "node_modules/smoldot": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.22.tgz", + "integrity": "sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g==", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/tools/ci-k6/package.json b/tools/ci-k6/package.json new file mode 100644 index 00000000..e4edb222 --- /dev/null +++ b/tools/ci-k6/package.json @@ -0,0 +1,16 @@ +{ + "name": "ci-k6", + "version": "1.0.0", + "description": "", + "main": "main.mjs", + "scripts": { + "main": "node main.mjs", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "^12.2.3", + "@polkadot/keyring": "^13.0.2" + } +}