diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..56126368 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,28 @@ +language: "en" +reviews: + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + path_instructions: + - path: "**/*.ts" + instructions: "Review the JavaScript code for conformity with the Semi-Standard style guide, highlighting any deviations." + - path: "**/*.ts" + instructions: "Analyze the logic of the code and the efficiency of the algorithms used. Suggest improvements if any inefficient algorithms are found." + - path: "/**/*.spec.ts" + instructions: | + "Assess the unit test code employing the jest testing framework. Confirm that: + - The tests adhere to jest's established best practices. + - Test descriptions are sufficiently detailed to clarify the purpose of each test." + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + drafts: true + base_branches: + - "master" + - "alpha" +chat: + auto_reply: true \ No newline at end of file diff --git a/.github/workflows/authenticate-commits.yml b/.github/workflows/authenticate-commits.yml new file mode 100644 index 00000000..58b32d8b --- /dev/null +++ b/.github/workflows/authenticate-commits.yml @@ -0,0 +1,48 @@ +name: Authenticate Commits +on: + pull_request: + types: [opened, reopened, synchronize] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Import allowed SSH keys + env: + ALLOWED_SIGNERS: ${{ vars.MIDDLEWARE_ALLOWED_SIGNERS }} + run: | + mkdir -p ~/.ssh + echo "$ALLOWED_SIGNERS" > ~/.ssh/allowed_signers + git config --global gpg.ssh.allowedSignersFile "~/.ssh/allowed_signers" + + - name: Validate commit signatures + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + # Function to verify a commit + verify_commit() { + local commit=$1 + local status=$(git show --pretty="format:%G?" $commit | head -n 1) + + if [ "$status" != "G" ]; then + local committer=$(git log -1 --pretty=format:'%cn (%ce)' $commit) + echo "Commit $commit from $committer has an invalid signature or is not signed by an allowed key." + exit 1 + fi + + } + + # Get all commits in the PR + commits=$(git rev-list $BASE_SHA..$HEAD_SHA) + + # Iterate over all commits in the PR and verify each one + for COMMIT in $commits; do + verify_commit $COMMIT + done + + echo "All commits are signed with allowed keys." diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 962fc854..ffed1b8d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -13,24 +13,33 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + submodules: 'true' + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.ASSOCIATION_DOCKER_USERNAME }} password: ${{ secrets.ASSOCIATION_DOCKER_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: images: ${{ secrets.ASSOCIATION_DOCKER_HUB_REPO }} - name: Build and push Docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/fast-forward.yml b/.github/workflows/fast-forward.yml new file mode 100644 index 00000000..679dcd0a --- /dev/null +++ b/.github/workflows/fast-forward.yml @@ -0,0 +1,27 @@ +name: fast-forward +on: + issue_comment: + types: [created, edited] +jobs: + fast-forward: + # Only run if the comment contains the /fast-forward command. + if: ${{ contains(github.event.comment.body, '/fast-forward') + && github.event.issue.pull_request }} + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + issues: write + + steps: + - name: Fast forwarding + uses: sequoia-pgp/fast-forward@v1 + with: + merge: true + # To reduce the workflow's verbosity, use 'on-error' + # to only post a comment when an error occurs, or 'never' to + # never post a comment. (In all cases the information is + # still available in the step's summary.) + comment: on-error + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} ## This allows to trigger push action from within this workflow. Read more - https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6470c0b..1838a5d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [master, alpha] + branches: [master, alpha, confidential-assets] pull_request: types: [assigned, opened, synchronize, reopened] @@ -14,6 +14,9 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -30,6 +33,9 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -46,6 +52,9 @@ jobs: CI: true steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' @@ -59,16 +68,69 @@ jobs: name: Building and releasing project runs-on: ubuntu-latest needs: [lint, build, test] + if: github.event_name == 'push' steps: - uses: actions/checkout@v3 + with: + persist-credentials: false + fetch-depth: 0 + submodules: 'true' - uses: actions/setup-node@v3 with: node-version: '18.x' cache: 'yarn' - name: install dependencies run: yarn --frozen-lockfile + - name: Setup SSH signing key + run: | + echo "$SSH_KEY_PRIVATE" | tr -d '\r' > /tmp/id_ed25519 + echo $SSH_KEY_PUBLIC > /tmp/id_ed25519.pub + chmod 600 /tmp/id_ed25519 + eval "$(ssh-agent -s)" + ssh-add /tmp/id_ed25519 + git config --global gpg.format ssh + git config --global commit.gpgsign true + git config --global user.signingkey /tmp/id_ed25519.pub + mkdir -p ~/.config/git + echo "${{ vars.RB_EMAIL }} $SSH_KEY_PUBLIC" > ~/.config/git/allowed_signers + git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers + shell: bash + env: + SSH_KEY_PRIVATE: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_KEY_PUBLIC: ${{ vars.SSH_PUBLIC_KEY }} - name: release env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.GH_RELEASE_BOT_PAT }} + GIT_AUTHOR_NAME: ${{ vars.RB_NAME }} + GIT_AUTHOR_EMAIL: ${{ vars.RB_EMAIL }} + GIT_COMMITTER_NAME: ${{ vars.RB_COMMITTER_NAME }} + GIT_COMMITTER_EMAIL: ${{ vars.RB_COMMITTER_EMAIL }} run: yarn semantic-release + - name: Clear SSH key + run: | + shred /tmp/id_ed25519 + + check-fast-forward: + name: Check if fast forwarding is possible + runs-on: ubuntu-latest + needs: [lint, build, test] + if: github.event_name == 'pull_request' + + permissions: + contents: read + # We appear to need write permission for both pull-requests and + # issues in order to post a comment to a pull request. + pull-requests: write + issues: write + + steps: + - name: Checking if fast forwarding is possible + uses: sequoia-pgp/fast-forward@v1 + with: + merge: false + # To reduce the workflow's verbosity, use 'on-error' + # to only post a comment when an error occurs, or 'never' to + # never post a comment. (In all cases the information is + # still available in the step's summary.) + comment: never # TODO @polymath-eric: add SonarCloud step when the account confusion is sorted diff --git a/.gitignore b/.gitignore index 64900104..ad4b7b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # compiled output /dist /node_modules -polymesh-rest-api-swagger-spec.json +polymesh-private-rest-api-swagger-spec.yaml # Logs logs @@ -36,3 +36,4 @@ lerna-debug.log* # Env *.env +polymesh-rest-api-swagger-spec.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..409e67d4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/polymesh-rest-api"] + path = src/polymesh-rest-api + url = https://github.com/PolymeshAssociation/polymesh-rest-api diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fd17ed6..e4b6ce4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "**/.pnp.*": true }, "eslint.nodePath": ".yarn/sdks", - "prettier.prettierPath": ".yarn/sdks/prettier/index.js", + "prettier.prettierPath": "", "typescript.enablePromptUseWorkspaceTsdk": true, "cSpell.ignorePaths": [ "package.json", @@ -26,6 +26,7 @@ ], "cSpell.words": [ "Custodied", + "Gamal", "Hashicorp", "Isin", "metatype", diff --git a/Jenkinsfile b/Jenkinsfile index 0f08c36e..424e1d15 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ def withSecretEnv(List varAndPasswordList, Closure closure) { node { - env.PROJECT_NAME = 'polymesh-rest-api' + env.PROJECT_NAME = 'polymesh-private-rest-api' env.GIT_REPO = "ssh://git@ssh.gitea.polymesh.dev:4444/Deployment/${PROJECT_NAME}.git" properties([[$class: 'BuildDiscarderProperty', diff --git a/Procfile b/Procfile deleted file mode 100644 index e6bad1f0..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: npm run start:prod \ No newline at end of file diff --git a/README.md b/README.md index cceba1bb..bcfd32d2 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/standard/semistandard) -[![Github Actions Workflow](https://github.com/PolymeshAssociation/polymesh-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/polymesh-rest-api/actions) -[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_polymesh-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_polymesh-rest-api) -[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/polymesh-rest-api)](https://github.com/PolymeshAssociation/polymesh-rest-api/issues) +[![Github Actions Workflow](https://github.com/PolymeshAssociation/polymesh-private-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/PolymeshAssociation/polymesh-private-rest-api/actions) +[![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=PolymeshAssociation_polymesh-private-rest-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=PolymeshAssociation_polymesh-private-rest-api) +[![Issues](https://img.shields.io/github/issues/PolymeshAssociation/polymesh-private-rest-api)](https://github.com/PolymeshAssociation/polymesh-private-rest-api/issues) ## Description -A REST API wrapper for the Polymesh blockchain. +A REST API wrapper for the Polymesh Private blockchain. -This version is compatible with chain versions 5.4.x - 6.0.x +This version is compatible with chain versions 1.0.x ## Setup ### Requirements -- node.js version 14.x +- node.js version 18.x - yarn version 1.x Note, if running with node v16+ the env `NODE_OPTIONS` should be set to `--unhandled-rejections=warn` @@ -62,8 +62,6 @@ $ yarn test:cov PORT=## port in which the server will listen. Defaults to 3000 ## POLYMESH_NODE_URL=## websocket URL for a Polymesh node ## POLYMESH_MIDDLEWARE_V2_URL=## URL for an instance of the Polymesh GraphQL Middleware Native SubQuery service ## -POLYMESH_MIDDLEWARE_URL=## URL for an instance of the Polymesh GraphQL Middleware service @deprecated in favour of POLYMESH_MIDDLEWARE_V2_URL## -POLYMESH_MIDDLEWARE_API_KEY=## API key for the Middleware GraphQL service ## LOCAL_SIGNERS=## list of comma separated IDs to refer to the corresponding mnemonic ## LOCAL_MNEMONICS=## list of comma separated mnemonics for the signer service (each mnemonic corresponds to a signer in LOCAL_SIGNERS) ## @@ -91,18 +89,52 @@ NOTIFICATIONS_LEGITIMACY_SECRET=## A secret used to create HMAC signatures ## AUTH_STRATEGY=## list of comma separated auth strategies to use e.g. (`apiKey,open`) ## API_KEYS=## list of comma separated api keys to initialize the `apiKey` strategy with ## # Datastore: -REST_POSTGRES_HOST=## Domain or IP indicating of the DB ## +REST_POSTGRES_HOST=## Domain or IP of DB instance ## REST_POSTGRES_PORT=## Port the DB is listening (usually 5432) ## REST_POSTGRES_USER=## DB user to use## REST_POSTGRES_PASSWORD=## Password of the user ## REST_POSTGRES_DATABASE=## Database to use ## +# Artemis: +ARTEMIS_HOST=localhost## Domain or IP of artemis instance ## +ARTEMIS_USERNAME=artemis ## Artemis user ## +ARTEMIS_PASSWORD=artemis ## Artemis password ## +ARTEMIS_PORT=5672 ## Port of AMQP acceptor ## + +# Proof Server: +PROOF_SERVER_URL=## API path where the proof server is hosted ``` -### Signing Transactions +## Signing Transactions + +The REST API has endpoints that submit transactions to the block chain (generally POST routes). Each of these endpoints share a field `"options"` that controls what key will sign it, and how it will be processed. + +e.g. + +``` +{ + options: { + signer: "alice", + processMode: "submit" + }, + ...transactionParams +} +``` + +Process modes include: + +- `submit` This will create a transaction payload, sign it and submit it to the chain. It will respond with 201 when the transaction has been successfully finalized. (Usually around 15 seconds). +- `submitWithCallback` This works like submit, but returns a response as soon as the transaction is submitted. The URL specified by `webhookUrl` will receive updates as the transaction is processed +- `dryRun` This creates and validates a transaction, and returns an estimate of its fees. +- `offline` This creates an unsigned transaction and returns a serialized JSON payload. The information can be signed, and then submitted to the chain. +- `AMQP` This creates an transaction to be processed by worker processes using an AMQP broker to ensure reliable processing + +### Signing Managers + +A signing manager is required for `submit` and `submitWithCallback` processing modes. There are currently three [signing managers](https://github.com/PolymeshAssociation/signing-managers#projects) the REST API can be configured with, the local signer, the [Hashicorp Vault](https://www.vaultproject.io/) signer or the [Fireblocks](https://www.fireblocks.com/) signing manager. If args for multiple are given the precedence order is Vault over Fireblocks over Local. -For any method that modifies chain state, the key to sign with can be controlled with the "signer" field. +For any method that modifies chain state, the key to sign with can be controlled with the "options.signer" field. This can either be the SS58 encoded address, or an ID that is dependent on the particular signing manager. 1. Vault Signing: By setting `VAULT_URL` and `VAULT_TOKEN` an external [Vault](https://www.vaultproject.io/) instance will be used to sign transactions. The URL should point to a transit engine in Vault that has Ed25519 keys in it. @@ -119,6 +151,47 @@ For any method that modifies chain state, the key to sign with can be controlled 1. Local Signing: By using `LOCAL_SIGNERS` and `LOCAL_MNEMONICS` private keys will be initialized in memory. When making a transaction that requires a signer use the corresponding `LOCAL_SIGNERS` (by array offset). +### Offline + +Offline payloads contain a field `"unsignedTransaction"`, which consists of 4 keys. `payload` and `rawPayload` correspond to `signPayload` and `signRaw`. You will need to pass one of these to the respective signer you are using (or replicate `signRaw` in your environment). `method` is the hex encoded transaction, which can help verify what is being signed. `metadata` is an echo of whatever is passed as `metadata` in the options. It has no effect on operation, but can be useful for attaching extra info to the transactions, e.g. `clientId` or `memo` + +After being generated the signature with the payload can be passed to `/submit` to be submitted to the chain. + +This mode introduces the risk transactions are rejected due to incorrect nonces or elapsed lifetime. See the [options DTO](src/common/dto/transaction-options.dto.ts) definition for full details + +### AMQP + +AMQP is a form on offline processing where the payload will be published on an AMQP topic, instead of being returned. Currently there are a set of "offline" modules, that setup listeners to the different queues. + +1. A transaction with "AMQP" mode is received. This gets serialized to an offline payload and published on `Requests` +1. A signer process subscribes to `Requests`. For each message it generates a signature, and publishes a message on `Signatures` +1. A submitter process subscribes to `Signatures` and submits to the chain. It publishes to `Finalizations`, for consumer applications to subscribe to + +To use AMQP mode a message broker must be configured. The implementation assumes [ArtemisMQ](https://activemq.apache.org/components/artemis/) is used, with an AMQP acceptor. In theory any AMQP 1.0 compliant broker should work though. + +If using AMQP, it is strongly recommended to use a persistent data store (i.e postgres). There are two tables related to AMQP processing: `offline_tx` and `offline_event`: + +- `offline_tx` is a table for the submitter process. This provides a convenient way to query submitted transactions, and to detect ones rejected by the chain for some reason +- `offline_event` is a table for the recorder process. This uses Artemis diverts to record every message exchanged in the process, serving as an audit log + +If using the project's compose file, an Artemis console will be exposed on `:8181` with `artemis` being both username and password. + +### Webhooks (alpha) + +Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. When processMode `submitAndCallback` is used the `webhookUrl` param must also be provided. The server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). + +Before sending any information to the endpoint the service will first make a request with the header `x-hook-secret` set to a value. The endpoint should return a `200` response with this header copied into the response headers. + +If you are a developer you can toggle an endpoint to aid with testing by setting the env `DEVELOPER_UTILS=true` which will enabled a endpoint at `/developer-testing/webhook` which can then be supplied as the `webhookUrl`. Note, the IsUrl validator doesn't recognize `localhost` as a valid URL, either use the IP `127.0.0.1` or create an entry in `/etc/hosts` like `127.0.0.1 rest.local` and use that instead. + +#### Warning + +Webhooks are still being developed and should not be used against mainnet. However the API should be stable to develop against for testing and demo purposes + +Webhooks have yet to implement a Repo to maintain subscription state, or AMQP to ensure it won't miss events. As such it can not guarantee delivery of messages + +The plan is to use a datastore and a message broker to make this module production ready + ### Authentication The REST API uses [passport.js](https://www.passportjs.org/) for authentication. This allows the service to be configurable with multiple strategies. @@ -151,22 +224,6 @@ To implement a new repo for a service, first define an abstract class describing To implement a new datastore create a new module in `~/datastores` and create a set of `Repos` that will implement the abstract classes. You will then need to set up the `DatastoreModule` to export the module when it is configured. For testing, each implemented Repo should be able to pass the `test` method defined on the abstract class it is implementing. -### Webhooks (alpha) - -Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. Alternatively `webhookUrl` can be given in any state modifying endpoint. When given, the server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). - -Before sending any information to the endpoint the service will first make a request with the header `x-hook-secret` set to a value. The endpoint should return a `200` response with this header copied into the response headers. - -If you are a developer you can toggle an endpoint to aid with testing by setting the env `DEVELOPER_UTILS=true` which will enabled a endpoint at `/developer-testing/webhook` which can then be supplied as the `webhookUrl`. Note, the IsUrl validator doesn't recognize `localhost` as a valid URL, either use the IP `127.0.0.1` or create an entry in `/etc/hosts` like `127.0.0.1 rest.local` and use that instead. - -#### Warning - -Webhooks are still being developed and should not be used against mainnet. However the API should be stable to develop against for testing and demo purposes - -Webhooks have yet to implement a Repo. As such the subscription status is not persisted and the service can not guarantee delivery in the face of ordinary compting faults. - -In its current state the transactions would have to be reconciled with chain events as there is a chance for notifications to not be delivered. - ### With docker To pass in the env variables you can use `-e` to pass them individually, or use a file with `--env-file`. @@ -179,6 +236,12 @@ docker run -it --env-file .pme.env -p $HOST_PORT:3000 $image_name Accessing `http://localhost:` will take you to the swagger playground UI where all endpoints are documented and can be tested +### ActiveMQ (Apple Silicone) + +You may need to enable "Use Rosetta for x86/amd64 emulation on Apple Silicon" in order for the Artemis AMQP container to start + +Currently in "Settings" > "Features in development" in docker desktop + ## License This project uses [NestJS](https://nestjs.com/), which is [MIT licensed](./LICENSE.MIT). diff --git a/compose/broker.xml b/compose/broker.xml new file mode 100644 index 00000000..1d722540 --- /dev/null +++ b/compose/broker.xml @@ -0,0 +1,145 @@ + + + + + + + 0.0.0.0 + + + true + + + 1 + + + + + + + + + + + + + + tcp://0.0.0.0:5672?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpMinLargeMessageSize=102400;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + false + false + + + + DLQ + ExpiryQueue + 0 + 3 + + 10 + PAGE + false + false + false + false + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+
+ + + +
Requests
+ EventsLog + false +
+ + +
Signatures
+ EventsLog + false +
+ + +
Finalizations
+ EventsLog + false +
+
+ +
+
diff --git a/chain-entry.sh b/compose/chain-entry.sh similarity index 100% rename from chain-entry.sh rename to compose/chain-entry.sh diff --git a/compose/init-db.sh b/compose/init-db.sh new file mode 100755 index 00000000..274e89dc --- /dev/null +++ b/compose/init-db.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + SELECT 'CREATE DATABASE rest' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'rest')\gexec +EOSQL \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1cdabb78..a4d91cdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,9 +11,77 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' volumes: - - './chain-entry.sh:/chain-entry.sh' + - './compose/chain-entry.sh:/chain-entry.sh' entrypoint: '/chain-entry.sh' command: [ '--alice --chain dev' ] + healthcheck: + test: "timeout 5 bash -c 'cat < /dev/null > /dev/tcp/localhost/9933' && exit 0 || exit 1" + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + + subquery: + platform: 'linux/amd64' + image: '${SUBQUERY_IMAGE}' + init: true + restart: unless-stopped + depends_on: + - 'postgres' + environment: + START_BLOCK: 1 + NETWORK_ENDPOINT: ws://chain:9944 + NETWORK_HTTP_ENDPOINT: http://chain:9933 + DB_USER: '${PG_USER:-postgres}' + DB_PASS: '${PG_PASSWORD:-postgres}' + DB_DATABASE: '${PG_DB:-postgres}' + DB_PORT: '${PG_PORT:-5432}' + DB_HOST: '${PG_HOST:-postgres}' + NODE_ENV: local + command: + - --batch-size=500 + - -f=/app + - --local + healthcheck: + test: curl --fail http://localhost:3000/meta || exit 1 + interval: 20s + retries: 10 + start_period: 20s + timeout: 10s + + graphql: + image: onfinality/subql-query:v1.0.0 + restart: unless-stopped + ports: + - ${SQ_PORT:-3001}:3000 + depends_on: + postgres: + condition: service_started + subquery: + condition: service_healthy + environment: + DB_DATABASE: postgres + DB_USER: '${PG_USER:-postgres}' + DB_PASS: '${PG_PASSWORD:-postgres}' + DB_PORT: '${PG_PORT:-5432}' + DB_HOST: '${PG_HOST:-postgres}' + command: + - --name=public + - --playground + - --indexer=http://subquery:3000 + + artemis: + image: apache/activemq-artemis:2.31.2 + ports: + - 8161:8161 # Web Server + - 61616:61616 # Core,MQTT,AMQP,HORNETQ,STOMP,OpenWire + - 5672:5672 # AMQP + volumes: + - './compose/broker.xml:/var/lib/artemis-instance/etc-override/broker.xml' + environment: + ARTEMIS_USERNAME: artemis + ARTEMIS_PASSWORD: artemis + AMQ_EXTRA_ARGS: "--nio" # "aio" offers better performance, but less platforms support it postgres: image: postgres:15 @@ -21,6 +89,7 @@ services: - $REST_POSTGRES_PORT:5432 volumes: - db-data:/var/lib/postgresql/data + - ./compose/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh environment: POSTGRES_USER: $REST_POSTGRES_USER POSTGRES_PASSWORD: $REST_POSTGRES_PASSWORD diff --git a/jest.config.js b/jest.config.js index 66deaacd..70264e79 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,9 +3,17 @@ module.exports = { transformIgnorePatterns: ['/node_modules/(?![@polymeshassociation/src]).+\\.js$'], testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testPathIgnorePatterns: ['/dist/*'], + testPathIgnorePatterns: ['/dist/*', '/src/polymesh-rest-api/*'], moduleNameMapper: { - '~/(.*)': '/src/$1', + '~/app.module': '/src/app.module', + '~/confidential(.*)': '/src/confidential$1', + '~/extended(.*)': '/src/extended$1', + '~/middleware/(.*)': '/src/confidential-middleware/$1', + '~/test-utils/(.*)': '/src/test-utils/$1', + '~/polymesh/(.*)': '/src/polymesh/$1', + '~/transactions/(.*)': '/src/transactions/$1', + '~/polymesh-rest-api/(.*)': '/src/polymesh-rest-api/$1', + '~/(.*)': '/src/polymesh-rest-api/src/$1', }, testRegex: '.*\\.spec\\.ts$', coverageDirectory: './coverage', diff --git a/package.json b/package.json index a227a6b6..7453324e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "polymesh-rest-api", - "version": "5.0.0-alpha.1", - "description": "Provides a REST like interface for interacting with the Polymesh blockchain", + "name": "polymesh-private-rest-api", + "version": "1.0.0-alpha.7", + "description": "Provides a REST like interface for interacting with the Polymesh Private blockchain", "author": "Polymesh Association", "private": true, "license": "Apache-2.0", @@ -32,6 +32,7 @@ "postgres:dev:reset": "yarn postgres:dev down postgres --volumes && yarn postgres:dev:start", "postgres:dev:migration:generate": "source ./postgres.dev.config && yarn postgres migration:generate", "postgres:dev:migration:run": "source ./postgres.dev.config && yarn postgres migration:run", + "postgres:dev:migration:revert": "source ./postgres.dev.config && yarn postgres migration:revert", "postgres:migration:run": "yarn postgres migration:run" }, "dependencies": { @@ -48,8 +49,8 @@ "@polymeshassociation/fireblocks-signing-manager": "^2.3.0", "@polymeshassociation/hashicorp-vault-signing-manager": "^3.1.0", "@polymeshassociation/local-signing-manager": "^3.1.0", + "@polymeshassociation/polymesh-private-sdk": "1.2.0-alpha.2", "@polymeshassociation/signing-manager-types": "^3.1.0", - "@polymeshassociation/polymesh-sdk": "^23.0.0", "class-transformer": "0.5.1", "class-validator": "^0.14.0", "joi": "17.4.0", @@ -60,6 +61,7 @@ "passport-headerapikey": "^1.2.2", "pg": "^8.11.3", "reflect-metadata": "0.1.13", + "rhea-promise": "^3.0.1", "rimraf": "3.0.2", "rxjs": "^7.5.7", "swagger-ui-express": "5.0.0", @@ -74,7 +76,6 @@ "@nestjs/testing": "^10.2.4", "@semantic-release/changelog": "^6.0.1", "@semantic-release/exec": "^6.0.3", - "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^9.0.2", "@types/axios": "^0.14.0", "@types/cron": "^1.7.3", @@ -86,6 +87,9 @@ "@types/node": "^18.15.11", "@types/passport": "^1.0.11", "@types/supertest": "2.0.12", + "@typescript-eslint/eslint-plugin": "6.9.1", + "@typescript-eslint/parser": "6.9.1", + "@zerollup/ts-transform-paths": "1.7.11", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-config-semistandard": "17.0.0", @@ -97,6 +101,8 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-simple-import-sort": "10.0.0", "husky": "8.0.3", + "jest": "29.7.0", + "jest-when": "^3.6.0", "lint-staged": "14.0.1", "prettier": "2.8.8", "prettier-eslint": "15.0.1", @@ -107,11 +113,6 @@ "ts-loader": "9.4.4", "ts-node": "10.9.1", "tsconfig-paths": "^4.2.0", - "@typescript-eslint/eslint-plugin": "6.9.1", - "@typescript-eslint/parser": "6.9.1", - "@zerollup/ts-transform-paths": "1.7.11", - "jest": "29.7.0", - "jest-when": "^3.6.0", "typescript": "4.8.2" }, "config": { diff --git a/postgres.dev.config b/postgres.dev.config index f647fadc..0f3fa83a 100644 --- a/postgres.dev.config +++ b/postgres.dev.config @@ -1,7 +1,7 @@ # WARNING this file is committed and should only be used with development values! export REST_POSTGRES_HOST=localhost -export REST_POSTGRES_PORT=4432 +export REST_POSTGRES_PORT=5432 export REST_POSTGRES_USER=postgres export REST_POSTGRES_PASSWORD=postgres -export REST_POSTGRES_DATABASE=postgres +export REST_POSTGRES_DATABASE=rest diff --git a/prepareRelease.sh b/prepareRelease.sh index f26f0ba1..7ecb999d 100755 --- a/prepareRelease.sh +++ b/prepareRelease.sh @@ -18,9 +18,13 @@ sed -i.bak -e "s/.setVersion('.*')/.setVersion('$nextVersion')/g" src/main.ts rm src/main.ts.bak export CHAIN_IMAGE="$CHAIN_REPO:$CHAIN_TAG" +export SUBQUERY_IMAGE="polymeshassociation/polymesh-subquery:v12.1.0" -docker compose up -d chain +# temporarly switch to development server, until the polymesh-private is public -SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='ws://localhost:9944' yarn generate:swagger > /dev/null 2>&1 +# docker compose up -d chain -docker compose down chain \ No newline at end of file +# SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='ws://localhost:9944' yarn generate:swagger > /dev/null 2>&1 +SWAGGER_VERSION=$nextVersion POLYMESH_NODE_URL='wss://dev.polymesh.tech/confidential/v1/' yarn generate:swagger > /dev/null 2>&1 + +# docker compose down chain \ No newline at end of file diff --git a/release.config.js b/release.config.js index 4f5d602b..518e4db9 100644 --- a/release.config.js +++ b/release.config.js @@ -1,11 +1,15 @@ module.exports = { - repositoryUrl: 'https://github.com/PolymeshAssociation/polymesh-rest-api.git', + repositoryUrl: 'https://github.com/PolymeshAssociation/polymesh-private-rest-api.git', branches: [ 'master', { name: 'alpha', prerelease: true, }, + { + name: 'confidential-assets', + prerelease: true, + }, ], // Note, the expectation is for Github plugin to create a tag that begins with `v`, which triggers a workflow that publishes a docker image @@ -26,16 +30,10 @@ module.exports = { npmPublish: false, }, ], - [ - '@semantic-release/git', - { - assets: ['package.json', 'src/main.ts'], - }, - ], [ '@semantic-release/github', { - assets: ['CHANGELOG.md', 'polymesh-rest-api-swagger-spec.json'], + assets: ['CHANGELOG.md', 'polymesh-private-rest-api-swagger-spec.json'], }, ], ], diff --git a/src/accounts/accounts.consts.ts b/src/accounts/accounts.consts.ts deleted file mode 100644 index 39826cdb..00000000 --- a/src/accounts/accounts.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const MAX_MEMO_LENGTH = 32; diff --git a/src/accounts/accounts.controller.spec.ts b/src/accounts/accounts.controller.spec.ts deleted file mode 100644 index 76096ec5..00000000 --- a/src/accounts/accounts.controller.spec.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { HttpStatus } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ExtrinsicsOrderBy, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; - -import { AccountsController } from '~/accounts/accounts.controller'; -import { AccountsService } from '~/accounts/accounts.service'; -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; -import { AccountModel } from '~/identities/models/account.model'; -import { NetworkService } from '~/network/network.service'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { extrinsic, testValues } from '~/test-utils/consts'; -import { - createMockResponseObject, - createMockSubsidy, - MockAsset, - MockPortfolio, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - mockNetworkServiceProvider, - mockSubsidyServiceProvider, -} from '~/test-utils/service-mocks'; - -const { signer, did, testAccount, txResult } = testValues; - -describe('AccountsController', () => { - let controller: AccountsController; - let mockNetworkService: DeepMocked; - const mockAccountsService = new MockAccountsService(); - let mockSubsidyService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AccountsController], - providers: [AccountsService, mockNetworkServiceProvider, mockSubsidyServiceProvider], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - mockNetworkService = mockNetworkServiceProvider.useValue as DeepMocked; - mockSubsidyService = mockSubsidyServiceProvider.useValue as DeepMocked; - controller = module.get(AccountsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getAccountBalance', () => { - it('should return the POLYX balance of an Account', async () => { - const mockResult = { - free: new BigNumber(10), - locked: new BigNumber(1), - total: new BigNumber(11), - }; - mockAccountsService.getAccountBalance.mockResolvedValue(mockResult); - - const result = await controller.getAccountBalance({ account: '5xdd' }); - - expect(result).toEqual(mockResult); - }); - }); - - describe('transferPolyx', () => { - it('should return the transaction details on transferring POLYX balance', async () => { - mockAccountsService.transferPolyx.mockResolvedValue(txResult); - - const body = { - signer, - to: 'address', - amount: new BigNumber(10), - memo: 'Sample memo', - }; - - const result = await controller.transferPolyx(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('getTransactionHistory', () => { - const mockTransaction = extrinsic; - - const mockTransactions = { - data: [mockTransaction], - next: null, - count: new BigNumber(1), - }; - - it('should return the list of Asset documents', async () => { - mockAccountsService.getTransactionHistory.mockResolvedValue(mockTransactions); - - const result = await controller.getTransactionHistory( - { account: 'someAccount' }, - { orderBy: ExtrinsicsOrderBy.CreatedAtDesc } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: [new ExtrinsicModel(mockTransaction)], - total: new BigNumber(1), - next: null, - }) - ); - }); - }); - - describe('getPermissions', () => { - const mockPermissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset()], - }, - portfolios: { - type: PermissionType.Include, - values: [new MockPortfolio()], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - it('should return the Account Permissions', async () => { - mockAccountsService.getPermissions.mockResolvedValue(mockPermissions); - - const result = await controller.getPermissions({ account: 'someAccount' }); - - expect(result).toEqual({ - assets: { - type: PermissionType.Include, - values: ['TICKER'], - }, - portfolios: { - type: PermissionType.Include, - values: [ - { - id: '1', - did, - }, - ], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }); - }); - }); - - describe('getSubsidy', () => { - let mockResponse: DeepMocked; - - beforeEach(() => { - mockResponse = createMockResponseObject(); - }); - it(`should return the ${HttpStatus.NO_CONTENT} if the Account has no subsidy`, async () => { - mockSubsidyService.getSubsidy.mockResolvedValue(null); - - await controller.getSubsidy({ account: 'someAccount' }, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT); - }); - - it('should return the Account Subsidy', async () => { - const subsidyWithAllowance = { - subsidy: createMockSubsidy(), - allowance: new BigNumber(10), - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockSubsidyService.getSubsidy.mockResolvedValue(subsidyWithAllowance as any); - - await controller.getSubsidy({ account: 'someAccount' }, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); - expect(mockResponse.json).toHaveBeenCalledWith({ - beneficiary: new AccountModel({ address: 'beneficiary' }), - subsidizer: new AccountModel({ address: 'subsidizer' }), - allowance: new BigNumber(10), - }); - }); - }); - - describe('freezeSecondaryAccounts', () => { - it('should freeze secondary accounts', async () => { - mockAccountsService.freezeSecondaryAccounts.mockResolvedValue(txResult); - const body = { - signer, - }; - - const result = await controller.freezeSecondaryAccounts(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('unfreezeSecondaryAccounts', () => { - it('should unfreeze secondary accounts', async () => { - mockAccountsService.unfreezeSecondaryAccounts.mockResolvedValue(txResult); - const body = { - signer, - }; - - const result = await controller.unfreezeSecondaryAccounts(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('revokePermissions', () => { - it('should call the service and return the transaction details', async () => { - mockAccountsService.revokePermissions.mockResolvedValue(txResult); - - const body = { - signer, - secondaryAccounts: ['someAddress'], - }; - - const result = await controller.revokePermissions(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('modifyPermissions', () => { - it('should call the service and return the transaction details', async () => { - mockAccountsService.modifyPermissions.mockResolvedValue(txResult); - - const body = { - signer, - secondaryAccounts: [ - new PermissionedAccountDto({ - secondaryAccount: 'someAddress', - permissions: new PermissionsLikeDto({ - assets: null, - portfolios: null, - transactionGroups: [TxGroup.PortfolioManagement], - }), - }), - ], - }; - - const result = await controller.modifyPermissions(body); - - expect(result).toEqual(txResult); - }); - }); - - describe('getTreasuryAccount', () => { - it('should call the service and return treasury Account details', async () => { - mockNetworkService.getTreasuryAccount.mockReturnValue(testAccount); - - const result = controller.getTreasuryAccount(); - - expect(result).toEqual(new AccountModel({ address: testAccount.address })); - }); - }); -}); diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts deleted file mode 100644 index caf23333..00000000 --- a/src/accounts/accounts.controller.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNoContentResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { Response } from 'express'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { createPermissionsModel } from '~/accounts/accounts.util'; -import { AccountParamsDto } from '~/accounts/dto/account-params.dto'; -import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto'; -import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; -import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; -import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { BalanceModel } from '~/assets/models/balance.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { AccountModel } from '~/identities/models/account.model'; -import { NetworkService } from '~/network/network.service'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { createSubsidyModel } from '~/subsidy/subsidy.util'; - -@ApiTags('accounts') -@Controller('accounts') -export class AccountsController { - constructor( - private readonly accountsService: AccountsService, - private readonly networkService: NetworkService, - private readonly subsidyService: SubsidyService - ) {} - - @ApiOperation({ - summary: 'Get POLYX balance of an Account', - description: 'This endpoint provides the free, locked and total POLYX balance of an Account', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose balance is to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Free, locked and total POLYX balance of the Account', - type: BalanceModel, - }) - @Get(':account/balance') - async getAccountBalance(@Param() { account }: AccountParamsDto): Promise { - const accountBalance = await this.accountsService.getAccountBalance(account); - return new BalanceModel(accountBalance); - } - - @ApiOperation({ - summary: 'Transfer an amount of POLYX to an account', - description: 'This endpoint transfers an amount of POLYX to a specified Account', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The destination Account doesn't have an associated Identity
  • " + - '
  • The receiver Identity has an invalid CDD claim
  • ' + - '
  • Insufficient free balance
  • ' + - '
', - }) - @Post('transfer') - async transferPolyx(@Body() params: TransferPolyxDto): Promise { - const result = await this.accountsService.transferPolyx(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get transaction history of an Account', - description: - 'This endpoint provides a list of transactions signed by the given Account. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose transaction history is to be fetched', - type: 'string', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - @ApiArrayResponse(ExtrinsicModel, { - description: 'List of transactions signed by the given Account', - paginated: true, - }) - @Get(':account/transactions') - async getTransactionHistory( - @Param() { account }: AccountParamsDto, - @Query() filters: TransactionHistoryFiltersDto - ): Promise> { - const { data, count, next } = await this.accountsService.getTransactionHistory( - account, - filters - ); - return new PaginatedResultsModel({ - results: data.map(extrinsic => new ExtrinsicModel(extrinsic)), - total: count, - next, - }); - } - - @ApiOperation({ - summary: 'Get Account Permissions', - description: - 'The endpoint retrieves the Permissions an Account has as a Permissioned Account for its corresponding Identity', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose Permissions are to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Permissions of the Account', - type: PermissionsModel, - }) - @ApiNotFoundResponse({ - description: 'Account is not associated with any Identity', - }) - @Get(':account/permissions') - async getPermissions(@Param() { account }: AccountParamsDto): Promise { - const permissions = await this.accountsService.getPermissions(account); - return createPermissionsModel(permissions); - } - - @ApiOperation({ - summary: 'Get Account Subsidy', - description: - 'The endpoint retrieves the subsidized balance of this Account and the subsidizer Account', - }) - @ApiParam({ - name: 'account', - description: 'The Account address whose subsidy is to be fetched', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Subsidy details for the Account', - type: SubsidyModel, - }) - @ApiNoContentResponse({ - description: 'Account is not being subsidized', - }) - @Get(':account/subsidy') - async getSubsidy(@Param() { account }: AccountParamsDto, @Res() res: Response): Promise { - const result = await this.subsidyService.getSubsidy(account); - - if (result) { - res.status(HttpStatus.OK).json(createSubsidyModel(result)); - } else { - res.status(HttpStatus.NO_CONTENT).send({}); - } - } - - @ApiOperation({ - summary: 'Freeze secondary Accounts', - description: - 'This endpoint freezes all the secondary Accounts in the signing Identity. This means revoking their permission to perform any operation on the chain and freezing their funds until the Accounts are unfrozen', - }) - @ApiOkResponse({ - description: 'Secondary Accounts have been frozen', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `signer` is not authorized to freeze their Identities secondary Accounts', - }) - @ApiBadRequestResponse({ - description: 'The secondary Accounts are already frozen', - }) - @Post('freeze') - async freezeSecondaryAccounts( - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.accountsService.freezeSecondaryAccounts(transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Unfreeze secondary Accounts', - description: 'This endpoint unfreezes all of the secondary Accounts in the signing Identity', - }) - @ApiOkResponse({ - description: 'Secondary Accounts have been unfrozen', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `signer` is not authorized to unfreeze their Identities secondary Accounts', - }) - @ApiBadRequestResponse({ - description: 'The secondary Accounts are already unfrozen', - }) - @Post('unfreeze') - async unfreezeSecondaryAccounts( - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.accountsService.unfreezeSecondaryAccounts(transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Revoke all permissions for any secondary Account', - description: - 'This endpoint revokes all permissions of a list of secondary Accounts associated with the signing Identity', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'One of the Accounts is not a secondary Account for the signing Identity', - }) - @Post('permissions/revoke') - async revokePermissions(@Body() params: RevokePermissionsDto): Promise { - const result = await this.accountsService.revokePermissions(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify all permissions for any secondary Account', - description: - 'This endpoint modifies all the permissions of a list of secondary Accounts associated with the signing Identity', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'One of the Accounts is not a secondary Account for the signing Identity', - }) - @Post('permissions/modify') - async modifyPermissions(@Body() params: ModifyPermissionsDto): Promise { - const result = await this.accountsService.modifyPermissions(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: "Get chain's treasury Account", - description: - 'This endpoint retrieves treasury Account details which holds the accumulated fees used for chain development and can only be accessed through governance', - }) - @ApiOkResponse({ - description: 'Details about the treasury Account', - type: AccountModel, - }) - @Get('treasury') - getTreasuryAccount(): AccountModel { - const { address } = this.networkService.getTreasuryAccount(); - - return new AccountModel({ address }); - } -} diff --git a/src/accounts/accounts.module.ts b/src/accounts/accounts.module.ts deleted file mode 100644 index eb3f0619..00000000 --- a/src/accounts/accounts.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsController } from '~/accounts/accounts.controller'; -import { AccountsService } from '~/accounts/accounts.service'; -import { NetworkModule } from '~/network/network.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SubsidyModule } from '~/subsidy/subsidy.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, NetworkModule, forwardRef(() => SubsidyModule)], - controllers: [AccountsController], - providers: [AccountsService], - exports: [AccountsService], -}) -export class AccountsModule {} diff --git a/src/accounts/accounts.service.spec.ts b/src/accounts/accounts.service.spec.ts deleted file mode 100644 index 87cfcc46..00000000 --- a/src/accounts/accounts.service.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicsOrderBy } from '@polymeshassociation/polymesh-sdk/middleware/types'; -import { PermissionType, TxGroup, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningModule } from '~/signing/signing.module'; -import { extrinsic, testValues } from '~/test-utils/consts'; -import { MockAccount, MockAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer } = testValues; - -describe('AccountsService', () => { - let service: AccountsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule], - providers: [AccountsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(AccountsService); - polymeshService = module.get(PolymeshService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Account for valid Account address', async () => { - const mockAccount = 'account'; - - mockPolymeshApi.accountManagement.getAccount.mockResolvedValue(mockAccount); - - const result = await service.findOne('address'); - - expect(result).toBe(mockAccount); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.accountManagement.getAccount.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findOne(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('getAccountBalance', () => { - it('should return the POLYX balance of an Account', async () => { - const fakeBalance = 'balance'; - - mockPolymeshApi.accountManagement.getAccountBalance.mockReturnValue(fakeBalance); - - const result = await service.getAccountBalance('fakeAccount'); - - expect(result).toBe(fakeBalance); - }); - }); - - describe('transferPolyx', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.balances.TransferWithMemo, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.network.transferPolyx.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - to: 'address', - amount: new BigNumber(10), - memo: 'Sample memo', - }; - - const result = await service.transferPolyx(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.network.transferPolyx, - { amount: new BigNumber(10), memo: 'Sample memo', to: 'address' }, - { signer } - ); - }); - }); - - describe('getTransactionHistory', () => { - const mockTransactions = { - data: [extrinsic], - next: null, - count: new BigNumber(1), - }; - - it('should return the transaction history of the Asset', async () => { - const mockAccount = new MockAccount(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAccount as any); - mockAccount.getTransactionHistory.mockResolvedValue(mockTransactions); - - const result = await service.getTransactionHistory('address', { - orderBy: ExtrinsicsOrderBy.CreatedAtDesc, - }); - expect(result).toEqual(mockTransactions); - }); - }); - - describe('getPermissions', () => { - const mockPermissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset()], - }, - portfolios: { - type: PermissionType.Include, - values: [], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - let findOneSpy: jest.SpyInstance; - let mockAccount: MockAccount; - - beforeEach(() => { - mockAccount = new MockAccount(); - findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAccount as any); - }); - - it('should return the Account Permissions for a valid address', async () => { - mockAccount.getPermissions.mockResolvedValue(mockPermissions); - - const result = await service.getPermissions('address'); - - expect(result).toEqual(mockPermissions); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAccount.getPermissions.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.getPermissions('address')).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('freezeSecondaryAccounts', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.FreezeSecondaryKeys, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.freezeSecondaryAccounts.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - }; - - const result = await service.freezeSecondaryAccounts(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.freezeSecondaryAccounts, - undefined, - { signer } - ); - }); - }); - - describe('unfreezeSecondaryAccounts', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.FreezeSecondaryKeys, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.unfreezeSecondaryAccounts.mockResolvedValue( - mockTransaction - ); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - }; - - const result = await service.unfreezeSecondaryAccounts(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.unfreezeSecondaryAccounts, - undefined, - { signer } - ); - }); - }); - - describe('revokePermissions', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.SetPermissionToSigner, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.revokePermissions.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const secondaryAccounts = ['someAddress']; - const body = { - signer, - secondaryAccounts, - }; - - const result = await service.revokePermissions(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.revokePermissions, - { secondaryAccounts }, - { signer } - ); - }); - }); - - describe('modifyPermissions', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.SetPermissionToSigner, - }; - const mockTransaction = new MockTransaction(transaction); - mockPolymeshApi.accountManagement.modifyPermissions.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const account = 'someAddress'; - const permissions = { - assets: null, - portfolios: null, - transactionGroups: [TxGroup.PortfolioManagement], - }; - const secondaryAccounts = [ - new PermissionedAccountDto({ - secondaryAccount: account, - permissions: new PermissionsLikeDto(permissions), - }), - ]; - const body = { - signer, - secondaryAccounts, - }; - - const result = await service.modifyPermissions(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.modifyPermissions, - { secondaryAccounts: [{ account, permissions }] }, - { signer } - ); - }); - }); -}); diff --git a/src/accounts/accounts.service.ts b/src/accounts/accounts.service.ts deleted file mode 100644 index 6f81adaf..00000000 --- a/src/accounts/accounts.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - Account, - AccountBalance, - ExtrinsicData, - Permissions, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto'; -import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto'; -import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto'; -import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AccountsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(address: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return await polymeshApi.accountManagement.getAccount({ address }).catch(error => { - throw handleSdkError(error); - }); - } - - public async getAccountBalance(account: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return polymeshApi.accountManagement.getAccountBalance({ account }); - } - - public async transferPolyx(params: TransferPolyxDto): ServiceReturn { - const { base, args } = extractTxBase(params); - const { polymeshService, transactionsService } = this; - - const { transferPolyx } = polymeshService.polymeshApi.network; - return transactionsService.submit(transferPolyx, args, base); - } - - public async getTransactionHistory( - address: string, - filters: TransactionHistoryFiltersDto - ): Promise> { - const account = await this.findOne(address); - - return account.getTransactionHistory(filters); - } - - public async getPermissions(address: string): Promise { - const account = await this.findOne(address); - return await account.getPermissions().catch(error => { - throw handleSdkError(error); - }); - } - - public async freezeSecondaryAccounts( - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { freezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(freezeSecondaryAccounts, undefined, transactionBaseDto); - } - - public async unfreezeSecondaryAccounts(opts: TransactionBaseDto): ServiceReturn { - const { unfreezeSecondaryAccounts } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(unfreezeSecondaryAccounts, undefined, opts); - } - - public async revokePermissions(params: RevokePermissionsDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { revokePermissions } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(revokePermissions, args, base); - } - - public async modifyPermissions(params: ModifyPermissionsDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { modifyPermissions } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit( - modifyPermissions, - { - secondaryAccounts: args.secondaryAccounts.map( - ({ secondaryAccount: account, permissions }) => ({ - account, - permissions: permissions.toPermissionsLike(), - }) - ), - }, - base - ); - } -} diff --git a/src/accounts/accounts.util.spec.ts b/src/accounts/accounts.util.spec.ts deleted file mode 100644 index 1b9e87d3..00000000 --- a/src/accounts/accounts.util.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - Account, - FungibleAsset, - NumberedPortfolio, - PermissionedAccount, - Permissions, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { createPermissionedAccountModel, createPermissionsModel } from '~/accounts/accounts.util'; -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; -import { testValues } from '~/test-utils/consts'; -import { MockAccount, MockAsset, MockPortfolio } from '~/test-utils/mocks'; - -describe('createPermissionsModel', () => { - const { did } = testValues; - - it('should transform Permissions to PermissionsModel', () => { - let permissions: Permissions = { - assets: { - type: PermissionType.Include, - values: [new MockAsset() as unknown as FungibleAsset], - }, - portfolios: { - type: PermissionType.Include, - values: [new MockPortfolio() as unknown as NumberedPortfolio], - }, - transactions: { - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - let result = createPermissionsModel(permissions); - - expect(result).toEqual({ - assets: new AssetPermissionsModel({ - type: PermissionType.Include, - values: ['TICKER'], - }), - portfolios: new PortfolioPermissionsModel({ - type: PermissionType.Include, - values: [ - new PortfolioIdentifierModel({ - id: '1', - did, - }), - ], - }), - transactions: new TransactionPermissionsModel({ - type: PermissionType.Include, - values: [TxTags.asset.AddDocuments], - }), - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }); - - permissions = { - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [TxGroup.Issuance, TxGroup.StoManagement], - }; - - result = createPermissionsModel(permissions); - - expect(result).toEqual({ - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [], - }); - }); -}); - -describe('createPermissionedAccountModel', () => { - it('should transform PermissionedAccount to PermissionedAccountModel', () => { - const account = new MockAccount() as unknown as Account; - const permissions = { - assets: null, - portfolios: null, - transactions: null, - transactionGroups: [], - }; - const permissionedAccount: PermissionedAccount = { - account, - permissions, - }; - - const result = createPermissionedAccountModel(permissionedAccount); - - const { address } = account; - expect(result).toEqual({ - account: new AccountModel({ address }), - permissions: new PermissionsModel(permissions), - }); - }); -}); diff --git a/src/accounts/accounts.util.ts b/src/accounts/accounts.util.ts deleted file mode 100644 index 3fca6a4f..00000000 --- a/src/accounts/accounts.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { PermissionedAccount, Permissions } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createPermissionsModel(permissions: Permissions): PermissionsModel { - let { assets, portfolios, transactions, transactionGroups } = permissions; - - let assetPermissions: AssetPermissionsModel | null; - if (assets) { - const { type, values } = assets; - assetPermissions = new AssetPermissionsModel({ type, values: values.map(v => v.toHuman()) }); - } else { - assetPermissions = null; - } - - let portfolioPermissions: PortfolioPermissionsModel | null; - if (portfolios) { - const { type, values } = portfolios; - portfolioPermissions = new PortfolioPermissionsModel({ - type, - values: values.map(createPortfolioIdentifierModel), - }); - } else { - portfolioPermissions = null; - } - - let transactionPermissions: TransactionPermissionsModel | null; - if (transactions) { - transactionPermissions = new TransactionPermissionsModel(transactions); - } else { - transactionPermissions = null; - transactionGroups = []; - } - - return new PermissionsModel({ - assets: assetPermissions, - portfolios: portfolioPermissions, - transactions: transactionPermissions, - transactionGroups, - }); -} - -export function createPermissionedAccountModel( - permissionedAccount: PermissionedAccount -): PermissionedAccountModel { - const { - account: { address }, - permissions, - } = permissionedAccount; - return new PermissionedAccountModel({ - account: new AccountModel({ address }), - permissions: createPermissionsModel(permissions), - }); -} diff --git a/src/accounts/dto/modify-permissions.dto.ts b/src/accounts/dto/modify-permissions.dto.ts deleted file mode 100644 index b5136b7b..00000000 --- a/src/accounts/dto/modify-permissions.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyPermissionsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of secondary Accounts containing address and modified permissions', - type: PermissionedAccountDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => PermissionedAccountDto) - readonly secondaryAccounts: PermissionedAccountDto[]; -} diff --git a/src/accounts/dto/permissioned-account.dto.ts b/src/accounts/dto/permissioned-account.dto.ts deleted file mode 100644 index 2f3a9847..00000000 --- a/src/accounts/dto/permissioned-account.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateNested } from 'class-validator'; - -import { IsPermissionsLike } from '~/identities/decorators/validation'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; - -export class PermissionedAccountDto { - @ApiProperty({ - description: 'Account address', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly secondaryAccount: string; - - @ApiProperty({ - description: 'Permissions to be granted to the `secondaryAccount`', - type: PermissionsLikeDto, - }) - @ValidateNested() - @Type(() => PermissionsLikeDto) - @IsPermissionsLike() - readonly permissions: PermissionsLikeDto; - - constructor(dto: PermissionedAccountDto) { - Object.assign(this, dto); - } -} diff --git a/src/accounts/dto/revoke-permissions.dto.ts b/src/accounts/dto/revoke-permissions.dto.ts deleted file mode 100644 index e61ff802..00000000 --- a/src/accounts/dto/revoke-permissions.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RevokePermissionsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of secondary Account addresses whose permissions are to be revoked', - type: 'string', - isArray: true, - example: ['5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'], - }) - @IsString({ each: true }) - readonly secondaryAccounts: string[]; -} diff --git a/src/accounts/dto/transaction-history-filters.dto.ts b/src/accounts/dto/transaction-history-filters.dto.ts deleted file mode 100644 index 4c404399..00000000 --- a/src/accounts/dto/transaction-history-filters.dto.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicsOrderBy, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { IsBoolean, IsEnum, IsOptional, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTxTag } from '~/common/decorators/validation'; -import { getTxTags } from '~/common/utils'; - -export class TransactionHistoryFiltersDto { - @ApiPropertyOptional({ - description: 'Number of the Block', - type: 'string', - example: '1000000', - }) - @IsOptional() - @IsBigNumber({ min: 0 }) - @ToBigNumber() - readonly blockNumber?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Hash of the Block. Note, this property will be ignored if `blockNumber` is also specified', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - @ValidateIf( - ({ blockNumber, blockHash }: TransactionHistoryFiltersDto) => !blockNumber && !!blockHash - ) - @IsString() - readonly blockHash?: string; - - @ApiPropertyOptional({ - description: 'Transaction tags to be filtered', - type: 'string', - enum: getTxTags(), - example: TxTags.asset.RegisterTicker, - }) - @IsOptional() - @IsTxTag() - readonly tag?: TxTag; - - @ApiPropertyOptional({ - description: 'If true, only successful transactions are fetched', - type: 'boolean', - example: true, - }) - @IsOptional() - @IsBoolean() - readonly success?: boolean; - - @ApiPropertyOptional({ - description: 'Number of transactions to be fetched', - type: 'string', - example: '10', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly size?: BigNumber; - - @ApiPropertyOptional({ - description: 'Start index from which transactions are to be fetched', - type: 'string', - example: '1', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly start?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Order in which the transactions will be sorted based on the value of the `field`. Note, this property will be ignored if `field` is not specified', - type: 'string', - enum: ExtrinsicsOrderBy, - example: ExtrinsicsOrderBy.CreatedAtDesc, - }) - @IsEnum(ExtrinsicsOrderBy) - readonly orderBy: ExtrinsicsOrderBy = ExtrinsicsOrderBy.CreatedAtDesc; -} diff --git a/src/accounts/dto/transfer-polyx.dto.ts b/src/accounts/dto/transfer-polyx.dto.ts deleted file mode 100644 index b5c6e4da..00000000 --- a/src/accounts/dto/transfer-polyx.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsOptional, IsString, MaxLength } from 'class-validator'; - -import { MAX_MEMO_LENGTH } from '~/accounts/accounts.consts'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class TransferPolyxDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account that will receive the POLYX', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly to: string; - - @ApiProperty({ - description: - "Amount of POLYX to be transferred. Note that amount to be transferred should not be greater than the origin Account's free balance", - type: 'string', - example: '123', - }) - @IsBigNumber() - @ToBigNumber() - readonly amount: BigNumber; - - @ApiPropertyOptional({ - description: 'A note to help differentiate transfers', - type: 'string', - example: 'Sample transfer', - }) - @IsOptional() - @IsString() - @MaxLength(MAX_MEMO_LENGTH) - readonly memo?: string; -} diff --git a/src/accounts/models/asset-permissions.model.ts b/src/accounts/models/asset-permissions.model.ts deleted file mode 100644 index 38955627..00000000 --- a/src/accounts/models/asset-permissions.model.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; - -export class AssetPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: 'List of included/excluded Assets', - type: 'string', - isArray: true, - example: ['TICKER123456'], - }) - readonly values: string[]; - - constructor(model: AssetPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/accounts/models/permission-type.model.ts b/src/accounts/models/permission-type.model.ts deleted file mode 100644 index 29d80994..00000000 --- a/src/accounts/models/permission-type.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PermissionType } from '@polymeshassociation/polymesh-sdk/types'; - -export class PermissionTypeModel { - @ApiProperty({ - description: 'Indicates whether the permissions are inclusive or exclusive', - type: 'string', - enum: PermissionType, - example: PermissionType.Include, - }) - readonly type: PermissionType; - - constructor(model: PermissionTypeModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/permissioned-account.model.ts b/src/accounts/models/permissioned-account.model.ts deleted file mode 100644 index 3ddcc267..00000000 --- a/src/accounts/models/permissioned-account.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { AccountModel } from '~/identities/models/account.model'; - -export class PermissionedAccountModel { - @ApiProperty({ - description: 'Account details', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly account: AccountModel; - - @ApiProperty({ - description: 'Permissions present for this Permissioned Account', - type: PermissionsModel, - }) - @Type(() => PermissionsModel) - readonly permissions: PermissionsModel; - - constructor(model: PermissionedAccountModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/permissions.model.ts b/src/accounts/models/permissions.model.ts deleted file mode 100644 index 511b7c86..00000000 --- a/src/accounts/models/permissions.model.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TxGroup } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { AssetPermissionsModel } from '~/accounts/models/asset-permissions.model'; -import { PortfolioPermissionsModel } from '~/accounts/models/portfolio-permissions.model'; -import { TransactionPermissionsModel } from '~/accounts/models/transaction-permissions.model'; - -export class PermissionsModel { - @ApiProperty({ - description: - 'Assets over which the Account has permissions. A null value represents full permissions', - type: AssetPermissionsModel, - nullable: true, - }) - @Type(() => AssetPermissionsModel) - readonly assets: AssetPermissionsModel | null; - - @ApiProperty({ - description: - 'Portfolios over which the Account has permissions. A null value represents full permissions', - type: PortfolioPermissionsModel, - nullable: true, - }) - @Type(() => PortfolioPermissionsModel) - readonly portfolios: PortfolioPermissionsModel | null; - - @ApiProperty({ - description: - 'Transactions that the Account can execute. A null value represents full permissions', - type: TransactionPermissionsModel, - nullable: true, - }) - @Type(() => TransactionPermissionsModel) - readonly transactions: TransactionPermissionsModel | null; - - @ApiProperty({ - description: - 'Transaction Groups that the Account can execute. Having permissions over a [TxGroup](https://github.com/polymeshassociation/polymesh-sdk/blob/docs/v14/docs/enums/txgroup.md) means having permissions over every TxTag in said group. Note if `transactions` is null, ignore this value', - isArray: true, - enum: TxGroup, - example: [TxGroup.PortfolioManagement], - }) - readonly transactionGroups: TxGroup[]; - - constructor(model: PermissionsModel) { - Object.assign(this, model); - } -} diff --git a/src/accounts/models/portfolio-permissions.model.ts b/src/accounts/models/portfolio-permissions.model.ts deleted file mode 100644 index e2152875..00000000 --- a/src/accounts/models/portfolio-permissions.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class PortfolioPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: 'List of included/excluded Portfolios', - isArray: true, - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly values: PortfolioIdentifierModel[]; - - constructor(model: PortfolioPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/accounts/models/transaction-permissions.model.ts b/src/accounts/models/transaction-permissions.model.ts deleted file mode 100644 index 60f20ce0..00000000 --- a/src/accounts/models/transaction-permissions.model.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ModuleName, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { PermissionTypeModel } from '~/accounts/models/permission-type.model'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; - -export class TransactionPermissionsModel extends PermissionTypeModel { - @ApiProperty({ - description: - 'List of included/excluded transactions. A module name (a string without a period separator) represents all the transactions in said module', - isArray: true, - enum: getTxTagsWithModuleNames(), - example: [ModuleName.Asset, TxTags.checkpoint.CreateCheckpoint], - }) - readonly values: (TxTag | ModuleName)[]; - - @ApiPropertyOptional({ - description: - 'Transactions exempted from inclusion or exclusion. For example, if "type" is "Include", "values" contains "asset" and "exceptions" includes "asset.registerTicker", it means that all transactions in the "asset" module are included, EXCEPT for "registerTicker"', - isArray: true, - enum: getTxTags(), - example: [TxTags.asset.RegisterTicker], - }) - readonly exceptions?: TxTag[]; - - constructor(model: TransactionPermissionsModel) { - const { type, ...rest } = model; - super({ type }); - - Object.assign(this, rest); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 3e9a3c2c..9e87fd09 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,36 +2,47 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; import Joi from 'joi'; -import { AccountsModule } from '~/accounts/accounts.module'; -import { AssetsModule } from '~/assets/assets.module'; -import { AuthModule } from '~/auth/auth.module'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AuthorizationsModule } from '~/authorizations/authorizations.module'; -import { CheckpointsModule } from '~/checkpoints/checkpoints.module'; -import { ClaimsModule } from '~/claims/claims.module'; -import { AppConfigError } from '~/common/errors'; -import { ComplianceModule } from '~/compliance/compliance.module'; -import { CorporateActionsModule } from '~/corporate-actions/corporate-actions.module'; -import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; -import { EventsModule } from '~/events/events.module'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { MetadataModule } from '~/metadata/metadata.module'; -import { NetworkModule } from '~/network/network.module'; -import { NftsModule } from '~/nfts/nfts.module'; -import { NotificationsModule } from '~/notifications/notifications.module'; -import { OfferingsModule } from '~/offerings/offerings.module'; +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialMiddlewareModule } from '~/confidential-middleware/confidential-middleware.module'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { ExtendedIdentitiesModule } from '~/extended-identities/identities.module'; import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosModule } from '~/portfolios/portfolios.module'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import { SettlementsModule } from '~/settlements/settlements.module'; -import { SigningModule } from '~/signing/signing.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; -import { SubsidyModule } from '~/subsidy/subsidy.module'; -import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservations.module'; +import { AccountsModule } from '~/polymesh-rest-api/src/accounts/accounts.module'; +import { ArtemisModule } from '~/polymesh-rest-api/src/artemis/artemis.module'; +import { AssetsModule } from '~/polymesh-rest-api/src/assets/assets.module'; +import { AuthModule } from '~/polymesh-rest-api/src/auth/auth.module'; +import { AuthStrategy } from '~/polymesh-rest-api/src/auth/strategies/strategies.consts'; +import { AuthorizationsModule } from '~/polymesh-rest-api/src/authorizations/authorizations.module'; +import { CheckpointsModule } from '~/polymesh-rest-api/src/checkpoints/checkpoints.module'; +import { ClaimsModule } from '~/polymesh-rest-api/src/claims/claims.module'; +import { AppConfigError } from '~/polymesh-rest-api/src/common/errors'; +import { ComplianceModule } from '~/polymesh-rest-api/src/compliance/compliance.module'; +import { CorporateActionsModule } from '~/polymesh-rest-api/src/corporate-actions/corporate-actions.module'; +import { DeveloperTestingModule } from '~/polymesh-rest-api/src/developer-testing/developer-testing.module'; +import { EventsModule } from '~/polymesh-rest-api/src/events/events.module'; +import { IdentitiesModule } from '~/polymesh-rest-api/src/identities/identities.module'; +import { MetadataModule } from '~/polymesh-rest-api/src/metadata/metadata.module'; +import { NetworkModule } from '~/polymesh-rest-api/src/network/network.module'; +import { NftsModule } from '~/polymesh-rest-api/src/nfts/nfts.module'; +import { NotificationsModule } from '~/polymesh-rest-api/src/notifications/notifications.module'; +import { OfferingsModule } from '~/polymesh-rest-api/src/offerings/offerings.module'; +import { OfflineRecorderModule } from '~/polymesh-rest-api/src/offline-recorder/offline-recorder.module'; +import { OfflineSignerModule } from '~/polymesh-rest-api/src/offline-signer/offline-signer.module'; +import { OfflineStarterModule } from '~/polymesh-rest-api/src/offline-starter/offline-starter.module'; +import { OfflineSubmitterModule } from '~/polymesh-rest-api/src/offline-submitter/offline-submitter.module'; +import { PortfoliosModule } from '~/polymesh-rest-api/src/portfolios/portfolios.module'; +import { SettlementsModule } from '~/polymesh-rest-api/src/settlements/settlements.module'; +import { SigningModule } from '~/polymesh-rest-api/src/signing/signing.module'; +import { SubscriptionsModule } from '~/polymesh-rest-api/src/subscriptions/subscriptions.module'; +import { SubsidyModule } from '~/polymesh-rest-api/src/subsidy/subsidy.module'; +import { TickerReservationsModule } from '~/polymesh-rest-api/src/ticker-reservations/ticker-reservations.module'; +import { UsersModule } from '~/polymesh-rest-api/src/users/users.module'; import { TransactionsModule } from '~/transactions/transactions.module'; -import { UsersModule } from '~/users/users.module'; @Module({ imports: [ @@ -39,8 +50,6 @@ import { UsersModule } from '~/users/users.module'; validationSchema: Joi.object({ PORT: Joi.number().default(3000), POLYMESH_NODE_URL: Joi.string().required(), - POLYMESH_MIDDLEWARE_URL: Joi.string(), - POLYMESH_MIDDLEWARE_API_KEY: Joi.string(), SUBSCRIPTIONS_TTL: Joi.number().default(60000), SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES: Joi.number().default(5), SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL: Joi.number().default(5000), @@ -61,15 +70,22 @@ import { UsersModule } from '~/users/users.module'; console.warn('Defaulting to "open" for "AUTH_STRATEGY"'); return AuthStrategy.Open; }), + ARTEMIS_PORT: Joi.number().default(5672), + ARTEMIS_HOST: Joi.string(), + ARTEMIS_USERNAME: Joi.string(), + ARTEMIS_PASSWORD: Joi.string(), + PROOF_SERVER_API: Joi.string().default(''), + PROOF_SERVER_URL: Joi.string().default(''), }) - .and('POLYMESH_MIDDLEWARE_URL', 'POLYMESH_MIDDLEWARE_API_KEY') .and('LOCAL_SIGNERS', 'LOCAL_MNEMONICS') - .and('VAULT_TOKEN', 'VAULT_URL'), + .and('VAULT_TOKEN', 'VAULT_URL') + .and('ARTEMIS_HOST', 'ARTEMIS_PASSWORD', 'ARTEMIS_USERNAME'), }), AssetsModule, TickerReservationsModule, PolymeshModule, IdentitiesModule, + ExtendedIdentitiesModule, SettlementsModule, SigningModule, AuthorizationsModule, @@ -92,6 +108,20 @@ import { UsersModule } from '~/users/users.module'; MetadataModule, SubsidyModule, NftsModule, + ...(process.env.ARTEMIS_HOST + ? [ + ArtemisModule, + OfflineSignerModule, + OfflineSubmitterModule, + OfflineStarterModule, + OfflineRecorderModule, + ] + : []), + ConfidentialProofsModule.register(), + ConfidentialAssetsModule, + ConfidentialAccountsModule, + ConfidentialTransactionsModule, + ConfidentialMiddlewareModule.register(), ], }) export class AppModule {} diff --git a/src/assets/assets.consts.ts b/src/assets/assets.consts.ts deleted file mode 100644 index 7c2bab99..00000000 --- a/src/assets/assets.consts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const MAX_TICKER_LENGTH = 12; - -export const MAX_CONTENT_HASH_LENGTH = 130; diff --git a/src/assets/assets.controller.spec.ts b/src/assets/assets.controller.spec.ts deleted file mode 100644 index 73eddda1..00000000 --- a/src/assets/assets.controller.spec.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* eslint-disable import/first */ -const mockIsFungibleAsset = jest.fn(); - -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType, SecurityIdentifierType } from '@polymeshassociation/polymesh-sdk/types'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; -import { AssetsController } from '~/assets/assets.controller'; -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { MetadataService } from '~/metadata/metadata.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockAuthorizationRequest } from '~/test-utils/mocks'; -import { MockAssetService, mockMetadataServiceProvider } from '~/test-utils/service-mocks'; - -const { signer, did, txResult } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isFungibleAsset: mockIsFungibleAsset, -})); - -describe('AssetsController', () => { - let controller: AssetsController; - - const mockAssetsService = new MockAssetService(); - let mockMetadataService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AssetsController], - providers: [AssetsService, mockMetadataServiceProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - mockMetadataService = mockMetadataServiceProvider.useValue as DeepMocked; - - controller = module.get(AssetsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getGlobalMetadataKeys', () => { - it('should return all global metadata keys', async () => { - const mockGlobalMetadata = [ - { - name: 'Global Metadata', - id: new BigNumber(1), - specs: { description: 'Some description' }, - }, - ]; - mockMetadataService.findGlobalKeys.mockResolvedValue(mockGlobalMetadata); - - const result = await controller.getGlobalMetadataKeys(); - - expect(result).toEqual(mockGlobalMetadata); - }); - }); - - describe('getDetails', () => { - it('should return the details', async () => { - const mockAssetDetails = { - assetType: KnownAssetType.EquityCommon, - isDivisible: false, - name: 'NAME', - owner: { - did, - }, - totalSupply: new BigNumber(1), - }; - const mockIdentifiers = [ - { - type: SecurityIdentifierType.Isin, - value: 'US000000000', - }, - ]; - const mockAssetIsFrozen = false; - const mockAsset = new MockAsset(); - mockAsset.details.mockResolvedValue(mockAssetDetails); - mockAsset.getIdentifiers.mockResolvedValue(mockIdentifiers); - mockAsset.isFrozen.mockResolvedValue(mockAssetIsFrozen); - - const mockFundingRound = 'Series A'; - mockAsset.currentFundingRound.mockResolvedValue(mockFundingRound); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - mockIsFungibleAsset.mockReturnValue(true); - - const result = await controller.getDetails({ ticker: 'TICKER' }); - - const mockResult = { - ...mockAssetDetails, - securityIdentifiers: mockIdentifiers, - fundingRound: mockFundingRound, - isFrozen: mockAssetIsFrozen, - }; - - expect(result).toEqual(mockResult); - }); - }); - - describe('getHolders', () => { - const mockHolders = { - data: [ - { - identity: { did }, - balance: new BigNumber(1), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset holders', async () => { - mockAssetsService.findHolders.mockResolvedValue(mockHolders); - - const result = await controller.getHolders({ ticker: 'TICKER' }, { size: new BigNumber(1) }); - const expectedResults = mockHolders.data.map(holder => { - return { identity: holder.identity.did, balance: holder.balance }; - }); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expectedResults, - total: new BigNumber(mockHolders.count), - next: mockHolders.next, - }) - ); - }); - - it('should return the list of Asset holders from a start value', async () => { - mockAssetsService.findHolders.mockResolvedValue(mockHolders); - - const result = await controller.getHolders( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'SOME_START_KEY' } - ); - - const expectedResults = mockHolders.data.map(holder => { - return { identity: holder.identity.did, balance: holder.balance }; - }); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: expectedResults, - total: new BigNumber(mockHolders.count), - next: mockHolders.next, - }) - ); - }); - }); - - describe('getDocuments', () => { - const mockDocuments = { - data: [ - { - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset documents', async () => { - mockAssetsService.findDocuments.mockResolvedValue(mockDocuments); - - const result = await controller.getDocuments( - { ticker: 'TICKER' }, - { size: new BigNumber(1) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockDocuments.data, - total: new BigNumber(mockDocuments.count), - next: mockDocuments.next, - }) - ); - }); - - it('should return the list of Asset documents from a start value', async () => { - mockAssetsService.findDocuments.mockResolvedValue(mockDocuments); - - const result = await controller.getDocuments( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'SOME_START_KEY' } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockDocuments.data, - total: new BigNumber(mockDocuments.count), - next: mockDocuments.next, - }) - ); - }); - }); - - describe('setDocuments', () => { - it('should call the service and return the results', async () => { - const body = { - signer: '0x6000', - documents: [ - new AssetDocumentDto({ - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }), - ], - }; - const ticker = 'TICKER'; - mockAssetsService.setDocuments.mockResolvedValue(txResult); - - const result = await controller.setDocuments({ ticker }, body); - expect(result).toEqual(txResult); - expect(mockAssetsService.setDocuments).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('createAsset', () => { - it('should call the service and return the results', async () => { - const input = { - signer: '0x6000', - name: 'Ticker Corp', - ticker: 'TICKER', - isDivisible: false, - assetType: KnownAssetType.EquityCommon, - }; - mockAssetsService.createAsset.mockResolvedValue(txResult); - - const result = await controller.createAsset(input); - expect(result).toEqual(txResult); - expect(mockAssetsService.createAsset).toHaveBeenCalledWith(input); - }); - }); - - describe('issue', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - mockAssetsService.issue.mockResolvedValue(txResult); - - const result = await controller.issue({ ticker }, { signer, amount }); - expect(result).toEqual(txResult); - expect(mockAssetsService.issue).toHaveBeenCalledWith(ticker, { signer, amount }); - }); - }); - - describe('transferOwnership', () => { - it('should call the service and return the results', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockAssetsService.transferOwnership.mockResolvedValue(mockData); - - const body = { signer: '0x6000', target: '0x1000' }; - const ticker = 'SOME_TICKER'; - - const result = await controller.transferOwnership({ ticker }, body); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - expect(mockAssetsService.transferOwnership).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('redeem', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - const from = new BigNumber(1); - mockAssetsService.redeem.mockResolvedValue(txResult); - - const result = await controller.redeem({ ticker }, { signer, amount, from }); - expect(result).toEqual(txResult); - expect(mockAssetsService.redeem).toHaveBeenCalledWith(ticker, { signer, amount, from }); - }); - }); - - describe('freeze', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - mockAssetsService.freeze.mockResolvedValue(txResult); - - const result = await controller.freeze({ ticker }, { signer }); - expect(result).toEqual(txResult); - expect(mockAssetsService.freeze).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('unfreeze', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - mockAssetsService.unfreeze.mockResolvedValue(txResult); - - const result = await controller.unfreeze({ ticker }, { signer }); - expect(result).toEqual(txResult); - expect(mockAssetsService.unfreeze).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('controllerTransfer', () => { - it('should call the service and return the results', async () => { - const ticker = 'TICKER'; - const amount = new BigNumber(1000); - const origin = new PortfolioDto({ id: new BigNumber(1), did: '0x1000' }); - - mockAssetsService.controllerTransfer.mockResolvedValue(txResult); - - const result = await controller.controllerTransfer({ ticker }, { signer, origin, amount }); - - expect(result).toEqual(txResult); - expect(mockAssetsService.controllerTransfer).toHaveBeenCalledWith(ticker, { - signer, - origin, - amount, - }); - }); - }); - - describe('getOperationHistory', () => { - it('should call the service and return the results', async () => { - const mockAgent = { - did: 'Ox6'.padEnd(66, '0'), - }; - const mockHistory = [ - { - blockNumber: new BigNumber(123), - blockHash: 'blockHash', - blockDate: new Date('07/11/2022'), - eventIndex: new BigNumber(1), - }, - ]; - const mockAgentOperations = [ - { - identity: mockAgent, - history: mockHistory, - }, - ]; - mockAssetsService.getOperationHistory.mockResolvedValue(mockAgentOperations); - - const result = await controller.getOperationHistory({ ticker: 'TICKER' }); - - expect(result).toEqual([ - { - did: mockAgent.did, - history: mockHistory, - }, - ]); - }); - }); -}); diff --git a/src/assets/assets.controller.ts b/src/assets/assets.controller.ts deleted file mode 100644 index a38d0c0b..00000000 --- a/src/assets/assets.controller.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiGoneResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; - -import { AssetsService } from '~/assets/assets.service'; -import { createAssetDetailsModel } from '~/assets/assets.util'; -import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; -import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; -import { IssueDto } from '~/assets/dto/issue.dto'; -import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; -import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { AgentOperationModel } from '~/assets/models/agent-operation.model'; -import { AssetDetailsModel } from '~/assets/models/asset-details.model'; -import { AssetDocumentModel } from '~/assets/models/asset-document.model'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { MetadataService } from '~/metadata/metadata.service'; -import { GlobalMetadataModel } from '~/metadata/models/global-metadata.model'; - -@ApiTags('assets') -@Controller('assets') -export class AssetsController { - constructor( - private readonly assetsService: AssetsService, - private readonly metadataService: MetadataService - ) {} - - @ApiTags('metadata') - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch an Global Asset Metadata', - description: 'This endpoint retrieves all the Asset Global Metadata on chain', - }) - @ApiOkResponse({ - description: 'List of Asset Global Metadata which includes id, name and specs', - isArray: true, - type: GlobalMetadataModel, - }) - @Get('global-metadata') - public async getGlobalMetadataKeys(): Promise { - const result = await this.metadataService.findGlobalKeys(); - - return result.map(globalKey => new GlobalMetadataModel(globalKey)); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch Asset details', - description: 'This endpoint will provide the basic details of an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose details are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Basic details of the Asset', - type: AssetDetailsModel, - }) - @Get(':ticker') - public async getDetails(@Param() { ticker }: TickerParamsDto): Promise { - const asset = await this.assetsService.findOne(ticker); - - return createAssetDetailsModel(asset); - } - - @ApiOperation({ - summary: 'Fetch a list of Asset holders', - description: - 'This endpoint will provide the list of Asset holders along with their current balance', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose holders are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Asset holders to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Asset holders are to be fetched', - type: 'string', - required: false, - }) - @ApiArrayResponse(IdentityBalanceModel, { - description: 'List of Asset holders, each consisting of a DID and their current Asset balance', - paginated: true, - }) - @Get(':ticker/holders') - public async getHolders( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.assetsService.findHolders(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ identity, balance }) => - new IdentityBalanceModel({ - identity: identity.did, - balance, - }) - ), - total, - next, - }); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Fetch a list of Asset documents', - description: 'This endpoint will provide the list of documents attached to an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached documents are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of documents to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which documents are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiArrayResponse(AssetDocumentModel, { - description: 'List of documents attached to the Asset', - paginated: true, - }) - @Get(':ticker/documents') - public async getDocuments( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.assetsService.findDocuments(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ name, uri, contentHash, type, filedAt }) => - new AssetDocumentModel({ - name, - uri, - contentHash, - type, - filedAt, - }) - ), - total, - next, - }); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Set a list of Documents for an Asset', - description: - 'This endpoint assigns a new list of Documents to the Asset by replacing the existing list of Documents with the ones passed in the body', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose documents are to be updated', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - }) - @ApiNotFoundResponse({ - description: 'Asset was not found', - }) - @ApiBadRequestResponse({ - description: 'The supplied Document list is equal to the current one', - }) - @Post(':ticker/documents/set') - public async setDocuments( - @Param() { ticker }: TickerParamsDto, - @Body() setAssetDocumentsDto: SetAssetDocumentsDto - ): Promise { - const result = await this.assetsService.setDocuments(ticker, setAssetDocumentsDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Issue more of an Asset', - description: 'This endpoint issues more of a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to issue', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Post(':ticker/issue') - public async issue( - @Param() { ticker }: TickerParamsDto, - @Body() params: IssueDto - ): Promise { - const result = await this.assetsService.issue(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Create an Asset', - description: 'This endpoint allows for the creation of new assets', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The ticker reservation does not exist', - }) - @ApiGoneResponse({ - description: 'The ticker has already been used to create an asset', - }) - @Post('create') - public async createAsset(@Body() params: CreateAssetDto): Promise { - const result = await this.assetsService.createAsset(params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Transfer ownership of an Asset', - description: - 'This endpoint transfers ownership of the Asset to a `target` Identity. This generates an authorization request that must be accepted by the `target` Identity', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker of the Asset whose ownership is to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @Post('/:ticker/transfer-ownership') - public async transferOwnership( - @Param() { ticker }: TickerParamsDto, - @Body() params: TransferOwnershipDto - ): Promise { - const serviceResult = await this.assetsService.transferOwnership(ticker, params); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiOperation({ - summary: 'Redeem Asset tokens', - description: - "This endpoint allows to redeem (burn) an amount of an Asset tokens. These tokens are removed from Signer's Default Portfolio", - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: - "The amount to be redeemed is larger than the free balance in the Signer's Default Portfolio", - }) - @Post(':ticker/redeem') - public async redeem( - @Param() { ticker }: TickerParamsDto, - @Body() params: RedeemTokensDto - ): Promise { - const result = await this.assetsService.redeem(ticker, params); - return handleServiceResult(result); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Freeze transfers for an Asset', - description: - 'This endpoint submits a transaction that causes the Asset to become frozen. This means that it cannot be transferred or minted until it is unfrozen', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to freeze', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The Asset is already frozen', - }) - @Post(':ticker/freeze') - public async freeze( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.assetsService.freeze(ticker, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiTags('nfts') - @ApiOperation({ - summary: 'Unfreeze transfers for an Asset', - description: - 'This endpoint submits a transaction that unfreezes the Asset. This means that transfers and minting can be performed until it is frozen again', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to unfreeze', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The Asset is already unfrozen', - }) - @Post(':ticker/unfreeze') - public async unfreeze( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.assetsService.unfreeze(ticker, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Controller Transfer', - description: - 'This endpoint forces a transfer from the `origin` Portfolio to the signer’s Default Portfolio', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @ApiUnprocessableEntityResponse({ - description: 'The `origin` Portfolio does not have enough free balance for the transfer', - }) - @Post(':ticker/controller-transfer') - public async controllerTransfer( - @Param() { ticker }: TickerParamsDto, - @Body() params: ControllerTransferDto - ): Promise { - const result = await this.assetsService.controllerTransfer(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: "Fetch an Asset's operation history", - description: - "This endpoint provides a list of events triggered by transactions performed by various agent Identities, related to the Asset's configuration", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose operation history is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'List of operations grouped by the agent Identity who performed them', - isArray: true, - type: AgentOperationModel, - }) - @Get(':ticker/operations') - public async getOperationHistory( - @Param() { ticker }: TickerParamsDto - ): Promise { - const agentOperations = await this.assetsService.getOperationHistory(ticker); - - return agentOperations.map(agentOperation => new AgentOperationModel(agentOperation)); - } -} diff --git a/src/assets/assets.module.ts b/src/assets/assets.module.ts deleted file mode 100644 index 9e8d4031..00000000 --- a/src/assets/assets.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsController } from '~/assets/assets.controller'; -import { AssetsService } from '~/assets/assets.service'; -import { MetadataModule } from '~/metadata/metadata.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => MetadataModule)], - controllers: [AssetsController], - providers: [AssetsService], - exports: [AssetsService], -}) -export class AssetsModule {} diff --git a/src/assets/assets.service.spec.ts b/src/assets/assets.service.spec.ts deleted file mode 100644 index 84f73197..00000000 --- a/src/assets/assets.service.spec.ts +++ /dev/null @@ -1,539 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { AppNotFoundError } from '~/common/errors'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockAuthorizationRequest, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { did, signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('AssetsService', () => { - let service: AssetsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [AssetsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(AssetsService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Asset for a valid ticker', async () => { - const mockAsset = new MockAsset(); - - mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset); - - const result = await service.findOne('TICKER'); - - expect(result).toEqual(mockAsset); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getAsset.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findOne(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findFungible', () => { - it('should return the Asset for a valid ticker', async () => { - const mockAsset = new MockAsset(); - - mockPolymeshApi.assets.getFungibleAsset.mockResolvedValue(mockAsset); - - const result = await service.findFungible('TICKER'); - - expect(result).toEqual(mockAsset); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getFungibleAsset.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findFungible(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findAllByOwner', () => { - describe('if the identity does not exist', () => { - it('should throw a AppNotFoundError', async () => { - mockPolymeshApi.identities.isIdentityValid.mockResolvedValue(false); - - let error; - try { - await service.findAllByOwner('TICKER'); - } catch (err) { - error = err; - } - - expect(error).toBeInstanceOf(AppNotFoundError); - }); - }); - describe('otherwise', () => { - it('should return a list of Assets', async () => { - mockPolymeshApi.identities.isIdentityValid.mockResolvedValue(true); - - const assets = [{ ticker: 'FOO' }, { ticker: 'BAR' }, { ticker: 'BAZ' }]; - - mockPolymeshApi.assets.getAssets.mockResolvedValue(assets); - - const result = await service.findAllByOwner('0x1'); - - expect(result).toEqual(assets); - }); - }); - }); - - describe('findHolders', () => { - const mockHolders = { - data: [ - { - identity: did, - balance: new BigNumber(1), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset holders', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.assetHolders.get.mockResolvedValue(mockHolders); - - const result = await service.findHolders('TICKER', new BigNumber(10)); - expect(result).toEqual(mockHolders); - }); - - it('should return the list of Asset holders from a start value', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.assetHolders.get.mockResolvedValue(mockHolders); - - const result = await service.findHolders('TICKER', new BigNumber(10), 'NEXT_KEY'); - expect(result).toEqual(mockHolders); - }); - }); - - describe('findDocuments', () => { - const mockAssetDocuments = { - data: [ - { - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - it('should return the list of Asset documents', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.documents.get.mockResolvedValue(mockAssetDocuments); - - const result = await service.findDocuments('TICKER', new BigNumber(10)); - expect(result).toEqual(mockAssetDocuments); - }); - - it('should return the list of Asset documents from a start value', async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - mockAsset.documents.get.mockResolvedValue(mockAssetDocuments); - - const result = await service.findDocuments('TICKER', new BigNumber(10), 'NEXT_KEY'); - expect(result).toEqual(mockAssetDocuments); - }); - }); - - describe('setDocuments', () => { - it('should run a set procedure and return the queue results', async () => { - const mockAsset = new MockAsset(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.AddDocuments, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockAsset, - transactions: [mockTransaction], - }); - - const body = { - signer, - documents: [ - new AssetDocumentDto({ - name: 'TEST-DOC', - uri: 'URI', - contentHash: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }), - ], - }; - - const result = await service.setDocuments('TICKER', body); - expect(result).toEqual({ - result: mockAsset, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.documents.set, - { documents: body.documents }, - { signer } - ); - }); - }); - - describe('createAsset', () => { - const createBody = { - signer, - name: 'Ticker Corp', - ticker: 'TICKER', - isDivisible: false, - assetType: KnownAssetType.EquityCommon, - }; - - it('should create the asset', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.CreateAsset, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockAsset, - transactions: [mockTransaction], - }); - - const result = await service.createAsset(createBody); - expect(result).toEqual({ - result: mockAsset, - transactions: [mockTransaction], - }); - }); - }); - - describe('issue', () => { - const issueBody = { - signer, - amount: new BigNumber(1000), - }; - it('should issue the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Issue, - }; - const findSpy = jest.spyOn(service, 'findFungible'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.issue('TICKER', issueBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('transferOwnership', () => { - const ticker = 'TICKER'; - const body = { - signer, - target: '0x1000', - expiry: new Date(), - }; - - it('should run a transferOwnership procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const mockResult = new MockAuthorizationRequest(); - - const mockTransaction = new MockTransaction(transaction); - mockTransaction.run.mockResolvedValue(mockResult); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - const result = await service.transferOwnership(ticker, body); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('redeem', () => { - const amount = new BigNumber(1000); - const from = new BigNumber(1); - const redeemBody = { - signer, - amount, - from, - }; - - it('should run a redeem procedure and return the queue results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Redeem, - }; - const findSpy = jest.spyOn(service, 'findFungible'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount, from }, { signer }) - .mockResolvedValue({ transactions: [mockTransaction] }); - - let result = await service.redeem('TICKER', redeemBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - when(mockTransactionsService.submit) - .calledWith(mockAsset.redeem, { amount }, { signer }) - .mockResolvedValue({ transactions: [mockTransaction] }); - - result = await service.redeem('TICKER', { ...redeemBody, from: new BigNumber(0) }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('freeze', () => { - const freezeBody = { - signer, - }; - it('should freeze the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Freeze, - }; - const findSpy = jest.spyOn(service, 'findOne'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.freeze('TICKER', freezeBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('unfreeze', () => { - const unfreezeBody = { - signer, - }; - it('should unfreeze the asset', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.Unfreeze, - }; - const findSpy = jest.spyOn(service, 'findOne'); - - const mockTransaction = new MockTransaction(transaction); - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.unfreeze('TICKER', unfreezeBody); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('controllerTransfer', () => { - it('should run a controllerTransfer procedure and return the queue results', async () => { - const origin = new PortfolioDto({ id: new BigNumber(1), did }); - const amount = new BigNumber(100); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.ControllerTransfer, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockAsset.controllerTransfer.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockAsset as any); - - const result = await service.controllerTransfer('TICKER', { signer, origin, amount }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.controllerTransfer, - { - originPortfolio: { - identity: did, - id: new BigNumber(1), - }, - amount, - }, - { signer } - ); - }); - }); - - describe('getOperationHistory', () => { - it("should return the Asset's operation history", async () => { - const mockAsset = new MockAsset(); - - const findOneSpy = jest.spyOn(service, 'findFungible'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockAsset as any); - - const mockOperations = [ - { - identity: { - did: 'Ox6'.padEnd(66, '0'), - }, - history: [ - { - blockNumber: new BigNumber(123), - blockHash: 'blockHash', - blockDate: new Date('07/11/2022'), - eventIndex: new BigNumber(1), - }, - ], - }, - ]; - mockAsset.getOperationHistory.mockResolvedValue(mockOperations); - - const result = await service.getOperationHistory('TICKER'); - expect(result).toEqual(mockOperations); - }); - }); -}); diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts deleted file mode 100644 index 98cfd77a..00000000 --- a/src/assets/assets.service.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Asset, - AssetDocument, - AuthorizationRequest, - FungibleAsset, - HistoricAgentOperation, - IdentityBalance, - NftCollection, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto'; -import { CreateAssetDto } from '~/assets/dto/create-asset.dto'; -import { IssueDto } from '~/assets/dto/issue.dto'; -import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto'; -import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { AppNotFoundError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AssetsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(ticker: string): Promise { - return await this.polymeshService.polymeshApi.assets.getAsset({ ticker }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findFungible(ticker: string): Promise { - return await this.polymeshService.polymeshApi.assets - .getFungibleAsset({ ticker }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async findAllByOwner(owner: string): Promise<(FungibleAsset | NftCollection)[]> { - const { - polymeshService: { polymeshApi }, - } = this; - const isDidValid = await polymeshApi.identities.isIdentityValid({ identity: owner }); - - if (!isDidValid) { - throw new AppNotFoundError(owner, 'identity'); - } - - return polymeshApi.assets.getAssets({ owner }); - } - - public async findHolders( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.findFungible(ticker); - return asset.assetHolders.get({ size, start }); - } - - public async findDocuments( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.findOne(ticker); - return asset.documents.get({ size, start }); - } - - public async setDocuments(ticker: string, params: SetAssetDocumentsDto): ServiceReturn { - const { - documents: { set }, - } = await this.findOne(ticker); - const { base, args } = extractTxBase(params); - - return this.transactionsService.submit(set, args, base); - } - - public async createAsset(params: CreateAssetDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const createAsset = this.polymeshService.polymeshApi.assets.createAsset; - return this.transactionsService.submit(createAsset, args, base); - } - - public async issue(ticker: string, params: IssueDto): ServiceReturn { - const { base, args } = extractTxBase(params); - const asset = await this.findFungible(ticker); - - return this.transactionsService.submit(asset.issuance.issue, args, base); - } - - public async transferOwnership( - ticker: string, - params: TransferOwnershipDto - ): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, base); - } - - public async redeem(ticker: string, params: RedeemTokensDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { redeem } = await this.findFungible(ticker); - - return this.transactionsService.submit( - redeem, - { ...args, from: toPortfolioId(args.from) }, - base - ); - } - - public async freeze(ticker: string, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const asset = await this.findOne(ticker); - - return this.transactionsService.submit(asset.freeze, {}, transactionBaseDto); - } - - public async unfreeze( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.findOne(ticker); - - return this.transactionsService.submit(asset.unfreeze, {}, transactionBaseDto); - } - - public async controllerTransfer( - ticker: string, - params: ControllerTransferDto - ): ServiceReturn { - const { - base, - args: { origin, amount }, - } = extractTxBase(params); - const { controllerTransfer } = await this.findFungible(ticker); - - return this.transactionsService.submit( - controllerTransfer, - { originPortfolio: origin.toPortfolioLike(), amount }, - base - ); - } - - public async getOperationHistory(ticker: string): Promise { - const asset = await this.findFungible(ticker); - return asset.getOperationHistory(); - } -} diff --git a/src/assets/assets.util.ts b/src/assets/assets.util.ts deleted file mode 100644 index e9a521b0..00000000 --- a/src/assets/assets.util.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { Asset } from '@polymeshassociation/polymesh-sdk/types'; -import { isFungibleAsset } from '@polymeshassociation/polymesh-sdk/utils'; - -import { AssetDetailsModel } from '~/assets/models/asset-details.model'; - -/** - * Fetch and assemble data for an Asset - */ -export async function createAssetDetailsModel(asset: Asset): Promise { - const [ - { owner, assetType, name, totalSupply, isDivisible }, - securityIdentifiers, - fundingRound, - isFrozen, - ] = await Promise.all([ - asset.details(), - asset.getIdentifiers(), - isFungibleAsset(asset) ? asset.currentFundingRound() : null, - asset.isFrozen(), - ]); - - return new AssetDetailsModel({ - owner, - assetType, - name, - totalSupply, - isDivisible, - securityIdentifiers, - fundingRound, - isFrozen, - }); -} diff --git a/src/assets/dto/asset-document.dto.ts b/src/assets/dto/asset-document.dto.ts deleted file mode 100644 index 26e0d6ac..00000000 --- a/src/assets/dto/asset-document.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { AssetDocument } from '@polymeshassociation/polymesh-sdk/types'; -import { IsDate, IsHexadecimal, IsOptional, IsString, Matches, MaxLength } from 'class-validator'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; - -export class AssetDocumentDto { - @ApiProperty({ - description: 'The name of the document', - example: 'Annual report, 2021', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'URI (Uniform Resource Identifier) of the document', - example: 'https://example.com/sec/10k-05-23-2021.htm', - }) - @IsString() - readonly uri: string; - - @ApiPropertyOptional({ - description: - "Hash of the document's content. Used to verify the integrity of the document pointed at by the URI", - example: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }) - @IsOptional() - @IsHexadecimal({ - message: 'Content Hash must be a hexadecimal number', - }) - @Matches(/^0x.+/, { - message: 'Content Hash must start with "0x"', - }) - @MaxLength(MAX_CONTENT_HASH_LENGTH, { - message: `Content Hash must be ${MAX_CONTENT_HASH_LENGTH} characters long`, - }) - readonly contentHash?: string; - - @ApiPropertyOptional({ - description: 'Type of the document', - example: 'Private Placement Memorandum', - }) - @IsOptional() - @IsString() - readonly type?: string; - - @ApiPropertyOptional({ - description: 'Date at which the document was filed', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly filedAt?: Date; - - public toAssetDocument(): AssetDocument { - return { ...this }; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/assets/dto/controller-transfer.dto.ts b/src/assets/dto/controller-transfer.dto.ts deleted file mode 100644 index 99e72fbf..00000000 --- a/src/assets/dto/controller-transfer.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class ControllerTransferDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Portfolio from which Asset tokens will be transferred', - type: () => PortfolioDto, - }) - @ValidateNested() - @Type(() => PortfolioDto) - origin: PortfolioDto; - - @ApiProperty({ - description: 'The amount of the Asset tokens to be transferred', - example: '1000', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; -} diff --git a/src/assets/dto/create-asset.dto.ts b/src/assets/dto/create-asset.dto.ts deleted file mode 100644 index 494dbc49..00000000 --- a/src/assets/dto/create-asset.dto.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownAssetType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { SecurityIdentifierDto } from '~/assets/dto/security-identifier.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateAssetDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Asset', - example: 'Ticker Corp', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'The ticker of the Asset. This must either be free or reserved by the Signer', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiPropertyOptional({ - description: 'The initial supply count of the Asset', - example: '627880', - type: 'string', - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly initialSupply?: BigNumber; - - @ApiProperty({ - description: 'Specifies if the Asset can be divided', - example: 'false', - }) - @IsBoolean() - readonly isDivisible: boolean; - - @ApiProperty({ - description: 'The type of Asset', - enum: KnownAssetType, - example: KnownAssetType.EquityCommon, - }) - @IsString() - readonly assetType: string; - - @ApiPropertyOptional({ - description: "List of Asset's Security Identifiers", - isArray: true, - type: SecurityIdentifierDto, - }) - @ValidateNested({ each: true }) - @Type(() => SecurityIdentifierDto) - readonly securityIdentifiers?: SecurityIdentifierDto[]; - - @ApiPropertyOptional({ - description: 'The current funding round of the Asset', - example: 'Series A', - }) - @IsOptional() - @IsString() - readonly fundingRound?: string; - - @ApiPropertyOptional({ - description: 'Documents related to the Asset', - isArray: true, - type: AssetDocumentDto, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents?: AssetDocumentDto[]; -} diff --git a/src/assets/dto/issue.dto.ts b/src/assets/dto/issue.dto.ts deleted file mode 100644 index 0ce3453a..00000000 --- a/src/assets/dto/issue.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class IssueDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The amount of the Asset to issue', - example: '1000', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; -} diff --git a/src/assets/dto/redeem-tokens.dto.ts b/src/assets/dto/redeem-tokens.dto.ts deleted file mode 100644 index ee4d24be..00000000 --- a/src/assets/dto/redeem-tokens.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RedeemTokensDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The amount of Asset tokens to be redeemed', - example: '100', - type: 'string', - }) - @ToBigNumber() - @IsBigNumber() - readonly amount: BigNumber; - - @ApiProperty({ - description: - 'Portfolio number from which the Asset tokens must be redeemed. Use 0 for the Default Portfolio', - example: '1', - type: 'string', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; -} diff --git a/src/assets/dto/security-identifier.dto.ts b/src/assets/dto/security-identifier.dto.ts deleted file mode 100644 index ed62f7d2..00000000 --- a/src/assets/dto/security-identifier.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { SecurityIdentifierType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -export class SecurityIdentifierDto { - @ApiProperty({ - description: 'The type of Asset identifier', - enum: SecurityIdentifierType, - example: SecurityIdentifierType.Isin, - }) - @IsEnum(SecurityIdentifierType) - readonly type: SecurityIdentifierType; - - @ApiProperty({ - description: 'The identifier', - example: 'US0846707026', - }) - @IsString() - readonly value: string; -} diff --git a/src/assets/dto/set-asset-documents.dto.ts b/src/assets/dto/set-asset-documents.dto.ts deleted file mode 100644 index 823504fa..00000000 --- a/src/assets/dto/set-asset-documents.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class SetAssetDocumentsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'New list of documents to replace the existing ones', - type: AssetDocumentDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents: AssetDocumentDto[]; -} diff --git a/src/assets/dto/ticker-params.dto.ts b/src/assets/dto/ticker-params.dto.ts deleted file mode 100644 index e2aa0f81..00000000 --- a/src/assets/dto/ticker-params.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ - -import { IsTicker } from '~/common/decorators/validation'; - -export class TickerParamsDto { - @IsTicker() - readonly ticker: string; -} diff --git a/src/assets/models/agent-operation.model.ts b/src/assets/models/agent-operation.model.ts deleted file mode 100644 index feaa65c0..00000000 --- a/src/assets/models/agent-operation.model.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { HistoricAgentOperation } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; - -export class AgentOperationModel { - @ApiProperty({ - description: 'DID of the Agent that performed the operations', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiProperty({ - description: 'List of Asset Operation Events that were triggered by the Agent Identity', - type: EventIdentifierModel, - isArray: true, - }) - @Type(() => EventIdentifierModel) - readonly history: EventIdentifierModel[]; - - constructor(data: HistoricAgentOperation) { - const { - identity: { did }, - history, - } = data; - Object.assign(this, { - did, - history: history.map(eventIdentifier => new EventIdentifierModel(eventIdentifier)), - }); - } -} diff --git a/src/assets/models/asset-balance.model.ts b/src/assets/models/asset-balance.model.ts deleted file mode 100644 index 805e05d7..00000000 --- a/src/assets/models/asset-balance.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { FungibleAsset } from '@polymeshassociation/polymesh-sdk/types'; - -import { BalanceModel } from '~/assets/models/balance.model'; -import { FromEntity } from '~/common/decorators/transformation'; - -export class AssetBalanceModel extends BalanceModel { - @ApiProperty({ - description: 'Ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - @FromEntity() - readonly asset: FungibleAsset; - - constructor(model: AssetBalanceModel) { - const { asset, ...balance } = model; - super(balance); - this.asset = asset; - } -} diff --git a/src/assets/models/asset-details.model.ts b/src/assets/models/asset-details.model.ts deleted file mode 100644 index 41f30542..00000000 --- a/src/assets/models/asset-details.model.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Identity, - KnownAssetType, - SecurityIdentifier, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity, FromEntityObject } from '~/common/decorators/transformation'; - -export class AssetDetailsModel { - @ApiProperty({ - description: 'The DID of the Asset owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - @ApiProperty({ - description: 'Type of the Asset', - type: 'string', - enum: KnownAssetType, - example: KnownAssetType.EquityCommon, - }) - readonly assetType: string; - - @ApiProperty({ - description: 'Name of the Asset', - type: 'string', - example: 'MyAsset', - }) - readonly name: string; - - @ApiProperty({ - description: 'Total supply count of the Asset', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly totalSupply: BigNumber; - - @ApiProperty({ - description: 'Indicator to know if Asset is divisible or not', - type: 'boolean', - example: 'false', - }) - readonly isDivisible: boolean; - - @ApiProperty({ - description: "List of Asset's Security Identifiers", - isArray: true, - example: [ - { - type: 'Isin', - value: 'US0000000000', - }, - ], - }) - @FromEntityObject() - readonly securityIdentifiers: SecurityIdentifier[]; - - @ApiProperty({ - description: 'Current funding round of the Asset', - type: 'string', - example: 'Series A', - nullable: true, - }) - readonly fundingRound: string | null; - - @ApiProperty({ - description: 'Whether transfers are frozen for the Asset', - type: 'boolean', - example: 'true', - }) - readonly isFrozen: boolean; - - constructor(model: AssetDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/asset-document.model.ts b/src/assets/models/asset-document.model.ts deleted file mode 100644 index ff67091a..00000000 --- a/src/assets/models/asset-document.model.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts'; - -export class AssetDocumentModel { - @ApiProperty({ - description: 'Name of the document', - example: 'Annual report, 2021', - }) - readonly name: string; - - @ApiProperty({ - description: 'URI (Uniform Resource Identifier) of the document', - example: 'https://example.com/sec/10k-05-23-2021.htm', - }) - readonly uri: string; - - @ApiPropertyOptional({ - description: - "Hash of the document's content. Used to verify the integrity of the document pointed at by the URI", - example: '0x'.padEnd(MAX_CONTENT_HASH_LENGTH, 'a'), - }) - readonly contentHash?: string; - - @ApiPropertyOptional({ - description: 'Type of the document', - example: 'Private Placement Memorandum', - }) - readonly type?: string; - - @ApiPropertyOptional({ - description: 'Date at which the document was filed', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly filedAt?: Date; - - constructor(model: AssetDocumentModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/balance.model.ts b/src/assets/models/balance.model.ts deleted file mode 100644 index 250248cc..00000000 --- a/src/assets/models/balance.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class BalanceModel { - @ApiProperty({ - type: 'string', - description: 'Free asset amount', - example: '123', - }) - @FromBigNumber() - readonly free: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'Locked asset amount', - example: '456', - }) - @FromBigNumber() - readonly locked: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'Sum total of locked and free asset amount', - example: '578', - }) - @FromBigNumber() - readonly total: BigNumber; - - constructor(model: BalanceModel) { - Object.assign(this, model); - } -} diff --git a/src/assets/models/identity-balance.model.ts b/src/assets/models/identity-balance.model.ts deleted file mode 100644 index 1d08e76c..00000000 --- a/src/assets/models/identity-balance.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class IdentityBalanceModel { - @ApiProperty({ - description: 'The DID of the Asset Holder', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly identity: string; - - @ApiProperty({ - description: 'Balance held by the Identity', - type: 'string', - example: '12345', - }) - @FromBigNumber() - readonly balance: BigNumber; - - constructor(model: IdentityBalanceModel) { - Object.assign(this, model); - } -} diff --git a/src/auth/__snapshots__/auth.utils.spec.ts.snap b/src/auth/__snapshots__/auth.utils.spec.ts.snap deleted file mode 100644 index 45d5f593..00000000 --- a/src/auth/__snapshots__/auth.utils.spec.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`createAuthGuard should throw if an invalid option is given 1`] = `"Auth config error! "open,apiKey,NOT_A_STRATEGY" contains an unrecognized option. Valid values are: "apiKey,open""`; diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts deleted file mode 100644 index 2b5cfa8e..00000000 --- a/src/auth/auth.controller.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AuthController } from '~/auth/auth.controller'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthService, mockAuthServiceProvider } from '~/test-utils/service-mocks'; - -const { user } = testValues; - -describe('AuthController', () => { - let controller: AuthController; - let mockAuthService: MockAuthService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockAuthServiceProvider], - controllers: [AuthController], - }).compile(); - - controller = module.get(AuthController); - mockAuthService = mockAuthServiceProvider.useValue; - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('createApiKey', () => { - it('should call the service and return the result', async () => { - const fakeResult = 'fake-result'; - const userName = user.name; - mockAuthService.createApiKey.mockResolvedValue(fakeResult); - - const result = await controller.createApiKey({ userName }); - - expect(result).toEqual(fakeResult); - expect(mockAuthService.createApiKey).toHaveBeenCalledWith({ userName }); - }); - }); - - describe('deleteApiKey', () => { - it('should call the service and return the result', async () => { - const apiKey = 'someKey'; - - const result = await controller.deleteApiKey({ apiKey }); - - expect(result).toBeUndefined(); - expect(mockAuthService.deleteApiKey).toHaveBeenCalledWith({ apiKey }); - }); - }); -}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts deleted file mode 100644 index 7c6ea131..00000000 --- a/src/auth/auth.controller.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { AuthService } from '~/auth/auth.service'; -import { CreateApiKeyDto } from '~/auth/dto/create-api-key.dto'; -import { DeleteApiKeyDto } from '~/auth/dto/delete-api-key.dto'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; - -@ApiTags('auth') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @ApiOperation({ - summary: 'Create API Key', - description: 'This endpoint will create an API Key', - }) - @ApiOkResponse({ - description: 'Details of the API key created', - type: ApiKeyModel, - }) - @Post('api-key/create') - public async createApiKey(@Body() params: CreateApiKeyDto): Promise { - return this.authService.createApiKey(params); - } - - @ApiOperation({ - summary: 'Delete an API Key', - description: 'This endpoint invalidates the given API key', - }) - @ApiNoContentResponse({ - description: 'The API key is no longer valid', - }) - @Post('/api-key/delete') - public async deleteApiKey(@Body() params: DeleteApiKeyDto): Promise { - await this.authService.deleteApiKey(params); - } -} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts deleted file mode 100644 index d7895aad..00000000 --- a/src/auth/auth.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { APP_GUARD } from '@nestjs/core'; -import { IAuthGuard, PassportModule } from '@nestjs/passport'; - -import { AuthController } from '~/auth/auth.controller'; -import { AuthService } from '~/auth/auth.service'; -import { createAuthGuard } from '~/auth/auth.utils'; -import { ApiKeyStrategy } from '~/auth/strategies/api-key.strategy'; -import { OpenStrategy } from '~/auth/strategies/open.strategy'; -import { DatastoreModule } from '~/datastore/datastore.module'; -import { UsersModule } from '~/users/users.module'; - -/** - * responsible for the REST API's authentication strategies - * - * @note authorization has not yet been implemented - all users have full access - */ -@Module({ - imports: [ - ConfigModule, - DatastoreModule.registerAsync(), - UsersModule, - PassportModule.register({ - session: false, - }), - ], - providers: [ - AuthService, - ApiKeyStrategy, - OpenStrategy, - { - provide: APP_GUARD, // registers a global guard - useFactory: (config: ConfigService): IAuthGuard => { - const configuredStrategies = config.getOrThrow('AUTH_STRATEGY'); - return createAuthGuard(configuredStrategies); - }, - inject: [ConfigService], - }, - ], - controllers: [AuthController], - exports: [AuthService, PassportModule], -}) -export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts deleted file mode 100644 index 117dbe95..00000000 --- a/src/auth/auth.service.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { AuthService } from '~/auth/auth.service'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { testValues } from '~/test-utils/consts'; -import { mockApiKeyRepoProvider, mockUserRepoProvider } from '~/test-utils/repo-mocks'; -import { mockUserServiceProvider } from '~/test-utils/service-mocks'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; - -describe('AuthService', () => { - const testApiKey = 'authServiceSecret'; - const expectedNotFoundError = new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - - let service: AuthService; - let mockUsersService: DeepMocked; - let mockApiKeyRepo: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthService, - mockUserServiceProvider, - mockApiKeyRepoProvider, - mockUserRepoProvider, - ], - }).compile(); - - service = module.get(AuthService); - mockUsersService = mockUserServiceProvider.useValue as DeepMocked; - mockApiKeyRepo = mockApiKeyRepoProvider.useValue as DeepMocked; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('method: createApiKey', () => { - it('should create an API key', async () => { - when(mockUsersService.getByName).calledWith(user.name).mockResolvedValue(user); - when(mockApiKeyRepo.createApiKey) - .calledWith(user) - .mockResolvedValue({ userId: user.id, secret: testApiKey }); - - const { userId, secret } = await service.createApiKey({ userName: user.name }); - - expect(userId).toEqual(user.id); - expect(secret.length).toBeGreaterThan(8); - }); - }); - - describe('method: validateApiKey', () => { - it('should return the user when given a valid api key', async () => { - when(mockApiKeyRepo.getUserByApiKey).calledWith(testApiKey).mockResolvedValue(user); - - const foundUser = await service.validateApiKey(testApiKey); - expect(foundUser).toEqual(user); - }); - - it('should throw a NotFoundError when given an unknown API key', () => { - mockApiKeyRepo.getUserByApiKey.mockRejectedValue(expectedNotFoundError); - - return expect(service.validateApiKey('unknown-secret')).rejects.toThrow( - expectedNotFoundError - ); - }); - }); - - describe('method: deleteApiKey', () => { - it('should delete an API key', async () => { - mockApiKeyRepo.deleteApiKey.mockResolvedValue(undefined); - - await service.deleteApiKey({ apiKey: testApiKey }); - - expect(mockApiKeyRepo.deleteApiKey).toBeCalledWith(testApiKey); - }); - }); -}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts deleted file mode 100644 index 5d49e019..00000000 --- a/src/auth/auth.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { CreateApiKeyDto } from '~/auth/dto/create-api-key.dto'; -import { DeleteApiKeyDto } from '~/auth/dto/delete-api-key.dto'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { UserModel } from '~/users/model/user.model'; -import { UsersService } from '~/users/users.service'; - -@Injectable() -export class AuthService { - constructor( - private readonly userService: UsersService, - private readonly apiKeyRepo: ApiKeyRepo - ) {} - - public async createApiKey({ userName }: CreateApiKeyDto): Promise { - const user = await this.userService.getByName(userName); - - return this.apiKeyRepo.createApiKey(user); - } - - public async validateApiKey(apiKey: string): Promise { - return this.apiKeyRepo.getUserByApiKey(apiKey); - } - - public async deleteApiKey({ apiKey }: DeleteApiKeyDto): Promise { - return this.apiKeyRepo.deleteApiKey(apiKey); - } -} diff --git a/src/auth/auth.utils.spec.ts b/src/auth/auth.utils.spec.ts deleted file mode 100644 index b9a6589a..00000000 --- a/src/auth/auth.utils.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createAuthGuard, parseApiKeysConfig } from '~/auth/auth.utils'; - -describe('createAuthGuard', () => { - it('should handle a single option', async () => { - const guard = createAuthGuard('apiKey'); - expect(guard).toBeDefined(); - }); - - it('should handle multiple valid options', () => { - const guard = createAuthGuard('apiKey,open'); - expect(guard).toBeDefined(); - }); - - it('should throw if an invalid option is given', () => { - return expect(() => - createAuthGuard('open,apiKey,NOT_A_STRATEGY') - ).toThrowErrorMatchingSnapshot(); - }); -}); - -describe('parseApiKeysConfig', () => { - it('should split and trim on commas', () => { - const result = parseApiKeysConfig('abc,def, ghi '); - expect(result).toEqual(['abc', 'def', 'ghi']); - }); -}); diff --git a/src/auth/auth.utils.ts b/src/auth/auth.utils.ts deleted file mode 100644 index 24fa6184..00000000 --- a/src/auth/auth.utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AuthGuard, IAuthGuard } from '@nestjs/passport'; - -import { AuthStrategy, authStrategyValues } from '~/auth/strategies/strategies.consts'; - -/** - * Creates an AuthGuard using the configured strategies - */ -export const createAuthGuard = (rawStrategy: string): IAuthGuard => { - const strategies = parseAuthStrategyConfig(rawStrategy); - return new (class extends AuthGuard(strategies) {})(); -}; - -/** - * transforms a raw auth strategy config into valid strategy values - * - * @throws if given invalid values - */ -export const parseAuthStrategyConfig = (rawStrategyConfig: string): AuthStrategy[] => { - const givenStrategies = rawStrategyConfig.split(',').map(strategy => strategy.trim()); - - const filteredStrategies = givenStrategies.filter(isStrategyKey); - - if (filteredStrategies.length !== givenStrategies.length) { - throw new Error( - `Auth config error! "${givenStrategies}" contains an unrecognized option. Valid values are: "${authStrategyValues}"` - ); - } - - return filteredStrategies.sort(cmpAuthStrategyOrder); -}; - -export const parseApiKeysConfig = (rawApiKeyConfig: string): string[] => { - if (rawApiKeyConfig.trim() === '') { - return []; - } - - return rawApiKeyConfig.split(',').map(rawKey => rawKey.trim()); -}; - -const isStrategyKey = (key: string): key is AuthStrategy => { - return authStrategyValues.includes(key as AuthStrategy); -}; - -/** - * A helper to be passed to `.sort`. The order is in which they will be evaluated. - * - * For example the the "open" strategy should always be last to allow for testing of other strategies - */ -const cmpAuthStrategyOrder = (a: AuthStrategy, b: AuthStrategy): number => - authStrategyValues.indexOf(a) - authStrategyValues.indexOf(b); diff --git a/src/auth/dto/create-api-key.dto.ts b/src/auth/dto/create-api-key.dto.ts deleted file mode 100644 index c98e9eb4..00000000 --- a/src/auth/dto/create-api-key.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class CreateApiKeyDto { - @ApiProperty({ - description: 'The name of the user to create the API key for', - example: 'Alice', - type: 'string', - }) - @IsString() - readonly userName: string; -} diff --git a/src/auth/dto/delete-api-key.dto.ts b/src/auth/dto/delete-api-key.dto.ts deleted file mode 100644 index 72465c93..00000000 --- a/src/auth/dto/delete-api-key.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class DeleteApiKeyDto { - @ApiProperty({ - description: 'The API key to delete', - example: 'XsQMQRpJqI/ViSdRXEa129mjOT9eJGn3pWGQL1S7Ibw=', - type: 'string', - }) - @IsString() - readonly apiKey: string; -} diff --git a/src/auth/models/api-key.model.ts b/src/auth/models/api-key.model.ts deleted file mode 100644 index 693c4b6f..00000000 --- a/src/auth/models/api-key.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { apiKeyHeader } from '~/auth/strategies/api-key.strategy'; - -export class ApiKeyModel { - @ApiProperty({ - type: 'string', - description: 'The user id associated to this key', - example: '1', - }) - readonly userId: string; - - @ApiProperty({ - type: 'string', - description: `A secret to use for the value of ${apiKeyHeader} on requests`, - example: 'XsQMQRpJqI/ViSdRXEa129mjOT9eJGn3pWGQL1S7Ibw=', - }) - readonly secret: string; - - constructor(model: ApiKeyModel) { - Object.assign(this, model); - } -} diff --git a/src/auth/repos/api-key.repo.suite.ts b/src/auth/repos/api-key.repo.suite.ts deleted file mode 100644 index 23622db3..00000000 --- a/src/auth/repos/api-key.repo.suite.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { testValues } from '~/test-utils/consts'; - -const { user } = testValues; - -export const testApiKeyRepo = async (repo: ApiKeyRepo): Promise => { - let secret: string; - let userId: string; - - const expectedNotFoundError = new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - - describe('method: createApiKey', () => { - it('should create an API key', async () => { - ({ secret, userId } = await repo.createApiKey(user)); - expect(userId).toEqual(user.id); - expect(secret).toBeDefined(); - }); - }); - - describe('method: getByApiKey', () => { - it('should return the User associated to the API key', async () => { - const foundUser = await repo.getUserByApiKey(secret); - expect(foundUser).toEqual(user); - }); - - it('should throw NotFoundError if the API key does not exist', async () => { - const unknownApiKey = 'unknownApiKey'; - return expect(repo.getUserByApiKey(unknownApiKey)).rejects.toThrow(expectedNotFoundError); - }); - }); - - describe('method: deleteApiKey', () => { - it('should remove the API key', async () => { - await repo.deleteApiKey(secret); - - return expect(repo.getUserByApiKey(secret)).rejects.toThrow(expectedNotFoundError); - }); - - it('should be a no-op on if the API key is not found', () => { - return expect(repo.deleteApiKey(secret)).resolves.not.toThrow(); - }); - }); -}; diff --git a/src/auth/repos/api-key.repo.ts b/src/auth/repos/api-key.repo.ts deleted file mode 100644 index ef385181..00000000 --- a/src/auth/repos/api-key.repo.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { testApiKeyRepo } from '~/auth/repos/api-key.repo.suite'; -import { UserModel } from '~/users/model/user.model'; - -export abstract class ApiKeyRepo { - public static type = 'ApiKey'; - - public abstract getUserByApiKey(apiKey: string): Promise; - public abstract createApiKey(user: UserModel): Promise; - public abstract deleteApiKey(apiKey: string): Promise; - - /** - * a set of tests that implementers should pass - */ - public static async test(repo: ApiKeyRepo): Promise { - return testApiKeyRepo(repo); - } -} diff --git a/src/auth/strategies/api-key.strategy.spec.ts b/src/auth/strategies/api-key.strategy.spec.ts deleted file mode 100644 index c12b999b..00000000 --- a/src/auth/strategies/api-key.strategy.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; -import passport from 'passport'; - -import { ApiKeyStrategy } from '~/auth/strategies/api-key.strategy'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { MockAuthService, mockAuthServiceProvider } from '~/test-utils/service-mocks'; - -describe('ApiKeyStrategy', () => { - let strategy: ApiKeyStrategy; - let authService: MockAuthService; - const mockedUser = 'fake-user'; - const mockApiKey = 'someSecret'; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockAuthServiceProvider, ApiKeyStrategy], - }).compile(); - - strategy = module.get(ApiKeyStrategy); - authService = mockAuthServiceProvider.useValue; - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - it('should verify with the user when given a valid api key', async () => { - const mockRequest = { - headers: { - 'x-api-key': mockApiKey, - }, - }; - - when(authService.validateApiKey).calledWith(mockApiKey).mockReturnValue(mockedUser); - - let authorizedUser; - passport.authenticate( - AuthStrategy.ApiKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (request: any, user: Express.User | false | null) => { - authorizedUser = user; - } - )(mockRequest, {}, {}); - - expect(authorizedUser).toEqual(mockedUser); - }); - - it('should return an Unauthorized response if the key is not found', async () => { - const mockRequest = { - headers: { - 'x-api-key': 'not-a-secret', - }, - }; - - when(authService.validateApiKey).calledWith(mockApiKey).mockReturnValue(mockedUser); - - let authorizedUser; - passport.authenticate( - AuthStrategy.ApiKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (request: any, user: Express.User | false | null) => { - authorizedUser = user; - } - )(mockRequest, {}, {}); - - expect(authorizedUser).toBeFalsy(); - }); -}); diff --git a/src/auth/strategies/api-key.strategy.ts b/src/auth/strategies/api-key.strategy.ts deleted file mode 100644 index a58529ea..00000000 --- a/src/auth/strategies/api-key.strategy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; - -import { AuthService } from '~/auth/auth.service'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AppUnauthorizedError } from '~/common/errors'; - -export const apiKeyHeader = 'x-api-key'; - -// eslint-disable-next-line @typescript-eslint/ban-types -type Callback = (err: Error | null, user?: Object, info?: Object) => void; - -/** - * authenticate with an API key - */ -@Injectable() -export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, AuthStrategy.ApiKey) { - constructor(private authService: AuthService) { - super({ header: apiKeyHeader }, false, (apiKey: string, done: Callback) => { - const user = this.authService.validateApiKey(apiKey); - if (!user) { - return done(new AppUnauthorizedError('API key not found'), undefined); - } - return done(null, user); - }); - } -} diff --git a/src/auth/strategies/open.strategy.spec.ts b/src/auth/strategies/open.strategy.spec.ts deleted file mode 100644 index b44c3719..00000000 --- a/src/auth/strategies/open.strategy.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import passport from 'passport'; - -import { OpenStrategy } from '~/auth/strategies/open.strategy'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { defaultUser } from '~/users/user.consts'; - -describe('OpenStrategy', () => { - let strategy: OpenStrategy; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OpenStrategy], - }).compile(); - - strategy = module.get(OpenStrategy); - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - it('should verify with the open user', async () => { - let authorizedUser; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - passport.authenticate(AuthStrategy.Open, (request: any, user: Express.User | false | null) => { - authorizedUser = user; - })({}, {}, {}); - - expect(authorizedUser).toEqual(defaultUser); - }); -}); diff --git a/src/auth/strategies/open.strategy.ts b/src/auth/strategies/open.strategy.ts deleted file mode 100644 index d57a39df..00000000 --- a/src/auth/strategies/open.strategy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { Strategy, VerifyCallback } from 'passport-custom'; - -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { defaultUser } from '~/users/user.consts'; - -/** - * authenticates with a default user - * - * @note this is intended for development or read only purposes. This strategy should **not** be used with a signer holding production keys - */ -@Injectable() -export class OpenStrategy extends PassportStrategy(Strategy, AuthStrategy.Open) { - constructor() { - const verifyEveryone: VerifyCallback = (req, done) => done(null, defaultUser); - - super(verifyEveryone); - } -} diff --git a/src/auth/strategies/strategies.consts.ts b/src/auth/strategies/strategies.consts.ts deleted file mode 100644 index 535a7615..00000000 --- a/src/auth/strategies/strategies.consts.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Different auth strategies available - */ -export enum AuthStrategy { - // note - order here can affect the evaluation order, it is not arbitrary - ApiKey = 'apiKey', - Open = 'open', -} - -export const authStrategyValues = Object.values(AuthStrategy); diff --git a/src/authorizations/authorizations.controller.spec.ts b/src/authorizations/authorizations.controller.spec.ts deleted file mode 100644 index ee906234..00000000 --- a/src/authorizations/authorizations.controller.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AuthorizationsController } from '~/authorizations/authorizations.controller'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthorizationsService } from '~/test-utils/service-mocks'; - -describe('AuthorizationsController', () => { - let controller: AuthorizationsController; - const { signer, txResult } = testValues; - const mockAuthorizationsService = new MockAuthorizationsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthorizationsController], - providers: [AuthorizationsService], - }) - .overrideProvider(AuthorizationsService) - .useValue(mockAuthorizationsService) - .compile(); - - controller = module.get(AuthorizationsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('accept', () => { - it('should call the service and return the transaction details', async () => { - mockAuthorizationsService.accept.mockResolvedValue(txResult); - - const authId = new BigNumber(1); - const result = await controller.accept({ id: authId }, { signer }); - - expect(result).toEqual(txResult); - expect(mockAuthorizationsService.accept).toHaveBeenCalledWith(authId, { signer }); - }); - }); - - describe('remove', () => { - it('should call the service and return the transaction details', async () => { - mockAuthorizationsService.remove.mockResolvedValue(txResult); - - const authId = new BigNumber(1); - const result = await controller.remove({ id: authId }, { signer }); - - expect(result).toEqual(txResult); - expect(mockAuthorizationsService.remove).toHaveBeenCalledWith(authId, { signer }); - }); - }); -}); diff --git a/src/authorizations/authorizations.controller.ts b/src/authorizations/authorizations.controller.ts deleted file mode 100644 index bc64b094..00000000 --- a/src/authorizations/authorizations.controller.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Body, Controller, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; - -@ApiTags('authorizations') -@Controller('authorizations') -export class AuthorizationsController { - constructor(private readonly authorizationsService: AuthorizationsService) {} - - @ApiOperation({ - summary: 'Accept an Authorization Request', - description: - 'This endpoint will accept a pending Authorization Request. You must be the target of the Request to be able to accept it', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization Request to be accepted', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'There is no Authorization Request with the passed ID targeting the `signer`', - }) - @Post('/:id/accept') - public async accept( - @Param() { id }: IdParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.authorizationsService.accept(id, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Remove an Authorization Request', - description: `This endpoint will reject/cancel a pending Authorization Request -
    -
  • If you are the Request issuer, this will cancel the Authorization
  • -
  • If you are the Request target, this will reject the Authorization
  • -
- `, - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization Request to be removed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: - 'There is no Authorization Request with the passed ID issued by or targeting the `signer`', - }) - @Post('/:id/remove') - public async remove( - @Param() { id }: IdParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.authorizationsService.remove(id, transactionBaseDto); - - return handleServiceResult(result); - } -} diff --git a/src/authorizations/authorizations.module.ts b/src/authorizations/authorizations.module.ts deleted file mode 100644 index c2a40d55..00000000 --- a/src/authorizations/authorizations.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { AuthorizationsController } from '~/authorizations/authorizations.controller'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, AccountsModule, forwardRef(() => IdentitiesModule)], - providers: [AuthorizationsService], - exports: [AuthorizationsService], - controllers: [AuthorizationsController], -}) -export class AuthorizationsModule {} diff --git a/src/authorizations/authorizations.service.spec.ts b/src/authorizations/authorizations.service.spec.ts deleted file mode 100644 index 4a8af50f..00000000 --- a/src/authorizations/authorizations.service.spec.ts +++ /dev/null @@ -1,343 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AuthorizationType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { AppNotFoundError } from '~/common/errors'; -import { IdentitiesService } from '~/identities/identities.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAccount, - MockAuthorizationRequest, - MockIdentity, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - MockIdentitiesService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer, did, txResult } = testValues; - -describe('AuthorizationsService', () => { - let service: AuthorizationsService; - - const mockIdentitiesService = new MockIdentitiesService(); - const mockAccountsService = new MockAccountsService(); - - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthorizationsService, - IdentitiesService, - AccountsService, - mockTransactionsProvider, - ], - }) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - - service = module.get(AuthorizationsService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findPendingByDid', () => { - const mockIdentity = new MockIdentity(); - const mockAuthorizations = [ - { - id: '1', - expiry: null, - data: { - type: AuthorizationType.PortfolioCustody, - value: { - did: '0x6'.padEnd(66, '1a1a'), - id: '1', - }, - }, - issuer: { - did: '0x6'.padEnd(66, '1a1a'), - }, - target: { - type: 'Identity', - value: did, - }, - }, - ]; - mockIdentity.authorizations.getReceived.mockResolvedValue(mockAuthorizations); - - it('should return a list of pending Authorizations', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did); - expect(result).toEqual(mockAuthorizations); - }); - - it('should return a list of pending Authorizations by whether they have expired or not', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did, false); - expect(result).toEqual(mockAuthorizations); - }); - - it('should return a list of pending Authorizations by authorization type', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findPendingByDid(did, true, AuthorizationType.PortfolioCustody); - expect(result).toEqual(mockAuthorizations); - }); - }); - - describe('findIssuedByDid', () => { - const mockIdentity = new MockIdentity(); - const mockIssuedAuthorizations = { - data: [ - { - id: '1', - expiry: null, - data: { - type: 'TransferCorporateActionAgent', - value: 'TEST', - }, - issuer: { - did, - }, - target: { - type: 'Account', - value: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }, - }, - ], - next: '0x450a3', - count: new BigNumber(15), - }; - mockIdentity.authorizations.getSent.mockResolvedValue(mockIssuedAuthorizations); - - it('should return a list of issued Authorizations', async () => { - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findIssuedByDid(did); - expect(result).toEqual(mockIssuedAuthorizations); - }); - }); - - describe('findOne', () => { - let mockIdentity: MockIdentity; - let mockAccount: MockAccount; - - beforeEach(() => { - mockIdentity = new MockIdentity(); - mockAccount = new MockAccount(); - }); - - it('should return the AuthorizationRequest details', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - mockIdentity.authorizations.getOne.mockResolvedValue(mockAuthorization); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let result = await service.findOne(mockIdentity as any, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - - mockAccount.authorizations.getOne.mockResolvedValue(mockAuthorization); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - result = await service.findOne(mockAccount as any, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAccount.authorizations.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - service.findOne(mockAccount as any, new BigNumber(1)) - ).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findOneByDid', () => { - it('should return the AuthorizationRequest details', async () => { - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const mockAuthorization = new MockAuthorizationRequest(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.spyOn(service, 'findOne').mockResolvedValue(mockAuthorization as any); - - const result = await service.findOneByDid(signer, new BigNumber(1)); - expect(result).toEqual(mockAuthorization); - }); - }); - - describe('getAuthRequest', () => { - let mockAccount: MockAccount; - let mockIdentity: MockIdentity; - let mockAuthorizationRequest: MockAuthorizationRequest; - let address: string; - let id: BigNumber; - let findOneSpy: jest.SpyInstance; - - beforeEach(() => { - address = 'address'; - id = new BigNumber(1); - mockAccount = new MockAccount(); - mockIdentity = new MockIdentity(); - mockAuthorizationRequest = new MockAuthorizationRequest(); - mockAccountsService.findOne.mockResolvedValue(mockAccount); - findOneSpy = jest.spyOn(service, 'findOne'); - }); - - it('should throw an error if AuthorizationRequest does not exist for a given ID', async () => { - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - const mockError = new Error('foo'); - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(mockError); - - await expect(() => service.getAuthRequest(address, id)).rejects.toThrowError(mockError); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockAccount as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - await expect(() => service.getAuthRequest(address, id)).rejects.toBeInstanceOf( - AppNotFoundError - ); - }); - - it('should return an AuthorizationRequest targeted to an Identity', async () => { - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockResolvedValue(mockAuthorizationRequest); - - const result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - }); - - it('should return an AuthorizationRequest targeted to an Account', async () => { - mockAccount.getIdentity.mockResolvedValue(null); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockAccount as any, id) - .mockResolvedValue(mockAuthorizationRequest); - - let result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - - mockAccount.getIdentity.mockResolvedValue(mockIdentity); - - when(findOneSpy) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .calledWith(mockIdentity as any, id) - .mockRejectedValue(new AppNotFoundError('1', 'test')); - - result = await service.getAuthRequest(address, id); - expect(result).toBe(mockAuthorizationRequest); - }); - }); - - describe('accept', () => { - it('should call the accept procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.AcceptPortfolioCustody, - }; - - const mockTransaction = new MockTransaction(transaction); - const mockAuthorizationRequest = new MockAuthorizationRequest(); - - const getAuthRequestSpy = jest.spyOn(service, 'getAuthRequest'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getAuthRequestSpy.mockResolvedValue(mockAuthorizationRequest as any); - - mockTransactionsService.getSigningAccount.mockResolvedValue('address'); - mockTransactionsService.submit.mockResolvedValue({ - ...txResult, - transactions: [mockTransaction], - }); - - const result = await service.accept(new BigNumber(1), { signer }); - expect(result).toEqual({ - ...txResult, - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('remove', () => { - it('should call the remove procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RemoveAuthorization, - }; - - const mockTransaction = new MockTransaction(transaction); - - const mockAuthorizationRequest = new MockAuthorizationRequest(); - - const getAuthRequestSpy = jest.spyOn(service, 'getAuthRequest'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getAuthRequestSpy.mockResolvedValue(mockAuthorizationRequest as any); - - mockTransactionsService.getSigningAccount.mockResolvedValue('address'); - mockTransactionsService.submit.mockResolvedValue({ - ...txResult, - transactions: [mockTransaction], - }); - - const result = await service.remove(new BigNumber(2), { signer }); - expect(result).toEqual({ - ...txResult, - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/authorizations/authorizations.service.ts b/src/authorizations/authorizations.service.ts deleted file mode 100644 index 273e91e3..00000000 --- a/src/authorizations/authorizations.service.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Account, - AuthorizationRequest, - AuthorizationType, - Identity, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppNotFoundError } from '~/common/errors'; -import { ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class AuthorizationsService { - constructor( - private readonly identitiesService: IdentitiesService, - private readonly accountsService: AccountsService, - private readonly transactionsService: TransactionsService - ) {} - - public async findPendingByDid( - did: string, - includeExpired?: boolean, - type?: AuthorizationType - ): Promise { - const identity = await this.identitiesService.findOne(did); - - return identity.authorizations.getReceived({ - includeExpired, - type, - }); - } - - public async findIssuedByDid(did: string): Promise> { - const identity = await this.identitiesService.findOne(did); - - return identity.authorizations.getSent(); - } - - public async findOne( - signatory: Identity | Account, - id: BigNumber - ): Promise { - return await signatory.authorizations.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findOneByDid(did: string, id: BigNumber): Promise { - const identity = await this.identitiesService.findOne(did); - - return this.findOne(identity, id); - } - - public async getAuthRequest(address: string, id: BigNumber): Promise { - const account = await this.accountsService.findOne(address); - - const identity = await account.getIdentity(); - - let authRequest: AuthorizationRequest | undefined; - if (identity) { - authRequest = await this.findOne(identity, id).catch(error => { - if (error instanceof AppNotFoundError) { - return undefined; - } else { - throw error; - } - }); - } - - if (!authRequest) { - authRequest = await this.findOne(account, id); - } - - return authRequest; - } - - public async accept(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { signer } = transactionBaseDto; - const address = await this.transactionsService.getSigningAccount(signer); - - const { accept } = await this.getAuthRequest(address, id); - - return this.transactionsService.submit(accept, {}, transactionBaseDto); - } - - public async remove(id: BigNumber, transactionBaseDto: TransactionBaseDto): ServiceReturn { - const { signer } = transactionBaseDto; - const address = await this.transactionsService.getSigningAccount(signer); - - const { remove } = await this.getAuthRequest(address, id); - - return this.transactionsService.submit(remove, {}, transactionBaseDto); - } -} diff --git a/src/authorizations/authorizations.util.ts b/src/authorizations/authorizations.util.ts deleted file mode 100644 index 35f224ce..00000000 --- a/src/authorizations/authorizations.util.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** istanbul ignore file */ - -import { AuthorizationRequest } from '@polymeshassociation/polymesh-sdk/types'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { TransactionResolver } from '~/common/utils'; -import { createSignerModel } from '~/identities/identities.util'; - -export function createAuthorizationRequestModel( - authorizationRequest: AuthorizationRequest -): AuthorizationRequestModel { - const { authId: id, expiry, data, issuer, target } = authorizationRequest; - return new AuthorizationRequestModel({ - id, - expiry, - data, - issuer, - target: createSignerModel(target), - }); -} - -export const authorizationRequestResolver: TransactionResolver = ({ - transactions, - details, - result, -}) => - new CreatedAuthorizationRequestModel({ - transactions, - details, - authorizationRequest: createAuthorizationRequestModel(result), - }); diff --git a/src/authorizations/dto/authorization-params.dto.ts b/src/authorizations/dto/authorization-params.dto.ts deleted file mode 100644 index 60467ec5..00000000 --- a/src/authorizations/dto/authorization-params.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ - -import { IsDid } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class AuthorizationParamsDto extends IdParamsDto { - @IsDid() - readonly did: string; -} diff --git a/src/authorizations/dto/authorizations-filter.dto.ts b/src/authorizations/dto/authorizations-filter.dto.ts deleted file mode 100644 index 13ab8505..00000000 --- a/src/authorizations/dto/authorizations-filter.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { AuthorizationType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IncludeExpiredFilterDto } from '~/common/dto/params.dto'; - -export class AuthorizationsFilterDto extends IncludeExpiredFilterDto { - @IsEnum(AuthorizationType) - @IsOptional() - readonly type?: AuthorizationType; -} diff --git a/src/authorizations/models/authorization-request.model.ts b/src/authorizations/models/authorization-request.model.ts deleted file mode 100644 index cd5b25df..00000000 --- a/src/authorizations/models/authorization-request.model.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Authorization, Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity, FromEntityObject } from '~/common/decorators/transformation'; -import { SignerModel } from '~/identities/models/signer.model'; - -export class AuthorizationRequestModel { - @ApiProperty({ - description: 'Unique ID of the Authorization Request (used to accept/reject/cancel)', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: - 'Date at which the Authorization Request expires and can no longer be accepted. A null value means that the Request never expires', - type: 'string', - example: new Date('10/14/1987').toISOString(), - nullable: true, - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: - 'Data corresponding to the type of Authorization Request' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
TypeData
Add Relayer Paying KeyBeneficiary, Relayer, Allowance
Become AgentPermission Group
Attest Primary Key RotationDID
Rotate Primary KeyDID
Transfer TickerTicker
Add MultiSig SignerAccount
Transfer Token OwnershipTicker
Join IdentityDID
Portfolio CustodyPortfolio
', - type: 'Authorization', - examples: { - type: 'PortfolioCustody', - value: { - did: '0x0600000000000000000000000000000000000000000000000000000000000000', - id: '1', - }, - }, - }) - @FromEntityObject() - readonly data: Authorization; - - @ApiProperty({ - description: 'The DID of the request issuer', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly issuer: Identity; - - @ApiProperty({ - description: 'Target Identity or Account of the request', - type: () => SignerModel, - }) - @Type(() => SignerModel) - readonly target: SignerModel; - - constructor(model: AuthorizationRequestModel) { - Object.assign(this, model); - } -} diff --git a/src/authorizations/models/created-authorization-request.model.ts b/src/authorizations/models/created-authorization-request.model.ts deleted file mode 100644 index 6c1da44c..00000000 --- a/src/authorizations/models/created-authorization-request.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedAuthorizationRequestModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Authorization Request', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly authorizationRequest: AuthorizationRequestModel; - - constructor(model: CreatedAuthorizationRequestModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/authorizations/models/pending-authorizations.model.ts b/src/authorizations/models/pending-authorizations.model.ts deleted file mode 100644 index 31620320..00000000 --- a/src/authorizations/models/pending-authorizations.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; - -export class PendingAuthorizationsModel { - @ApiProperty({ - description: 'List of pending Authorization Requests targeting the specified Identity', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly received: AuthorizationRequestModel[]; - - @ApiProperty({ - description: 'List of pending Authorization Requests issued by the specified Identity', - type: AuthorizationRequestModel, - }) - @Type(() => AuthorizationRequestModel) - readonly sent: AuthorizationRequestModel[]; - - constructor(model: PendingAuthorizationsModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/checkpoints.controller.spec.ts b/src/checkpoints/checkpoints.controller.spec.ts deleted file mode 100644 index 8bbc9d0b..00000000 --- a/src/checkpoints/checkpoints.controller.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { testValues } from '~/test-utils/consts'; -import { MockCheckpoint, MockCheckpointSchedule } from '~/test-utils/mocks'; -import { MockCheckpointsService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('CheckpointsController', () => { - let controller: CheckpointsController; - - const mockCheckpointsService = new MockCheckpointsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CheckpointsController], - providers: [CheckpointsService], - }) - .overrideProvider(CheckpointsService) - .useValue(mockCheckpointsService) - .compile(); - - controller = module.get(CheckpointsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getCheckpoint', () => { - it('should return the Checkpoint data', async () => { - const createdAt = new Date(); - const totalSupply = new BigNumber(1000); - const id = new BigNumber(1); - const ticker = 'TICKER'; - - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.createdAt.mockResolvedValue(createdAt); - mockCheckpoint.totalSupply.mockResolvedValue(totalSupply); - mockCheckpointsService.findOne.mockResolvedValue(mockCheckpoint); - - const result = await controller.getCheckpoint({ ticker, id }); - expect(result).toEqual(new CheckpointDetailsModel({ id, totalSupply, createdAt })); - }); - }); - - describe('getCheckpoints', () => { - const mockDate = new Date(); - const mockCheckpoints = { - data: [ - { - checkpoint: { - id: new BigNumber(1), - }, - createdAt: mockDate, - totalSupply: new BigNumber(10000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - const mockResult = new PaginatedResultsModel({ - results: [ - { - id: new BigNumber(1), - createdAt: mockDate, - totalSupply: new BigNumber(10000), - }, - ], - total: new BigNumber(2), - next: '0xddddd', - }); - it('should return the list of Checkpoints created on an Asset', async () => { - mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); - - const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, - { size: new BigNumber(1) } - ); - - expect(result).toEqual(mockResult); - }); - - it('should return the list of Checkpoints created on an Asset from start key', async () => { - mockCheckpointsService.findAllByTicker.mockResolvedValue(mockCheckpoints); - - const result = await controller.getCheckpoints( - { ticker: 'TICKER' }, - { size: new BigNumber(1), start: 'START_KEY' } - ); - - expect(result).toEqual(mockResult); - }); - }); - - describe('createCheckpoint', () => { - it('should return the details of newly created Checkpoint', async () => { - const mockCheckpoint = new MockCheckpoint(); - const response = { - ...txResult, - result: mockCheckpoint, - }; - mockCheckpointsService.createByTicker.mockResolvedValue(response); - const body = { - signer: 'signer', - }; - - const result = await controller.createCheckpoint({ ticker: 'TICKER' }, body); - - expect(result).toEqual({ - ...txResult, - checkpoint: mockCheckpoint, - }); - }); - }); - - describe('getSchedules', () => { - it('should return the list of active Checkpoint Schedules for an Asset', async () => { - const mockDate = new Date(); - const mockSchedules = [ - { - schedule: { - id: new BigNumber(1), - pendingPoints: [mockDate], - start: mockDate, - expiryDate: null, - }, - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }, - ]; - - mockCheckpointsService.findSchedulesByTicker.mockResolvedValue(mockSchedules); - - const result = await controller.getSchedules({ ticker: 'TICKER' }); - - const mockResult = [ - new CheckpointScheduleModel({ - id: new BigNumber(1), - ticker: 'TICKER', - pendingPoints: [mockDate], - expiryDate: null, - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }), - ]; - - expect(result).toEqual(new ResultsModel({ results: mockResult })); - }); - }); - - describe('getSchedule', () => { - it('should call the service and return the Checkpoint Schedule details', async () => { - const mockDate = new Date('10/14/1987'); - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }; - mockCheckpointsService.findScheduleById.mockResolvedValue(mockScheduleWithDetails); - - const result = await controller.getSchedule({ ticker: 'TICKER', id: new BigNumber(1) }); - - const mockResult = new CheckpointScheduleModel({ - ...mockScheduleWithDetails.schedule, - ...mockScheduleWithDetails.details, - pendingPoints: [mockDate], - }); - expect(result).toEqual(mockResult); - }); - }); - - describe('createSchedule', () => { - it('should return the details of newly created Checkpoint Schedule', async () => { - const mockDate = new Date('10/14/1987'); - - const mockCheckpointSchedule = new MockCheckpointSchedule(); - const response = { - ...txResult, - result: mockCheckpointSchedule, - }; - mockCheckpointsService.createScheduleByTicker.mockResolvedValue(response); - - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: mockDate, - }, - }; - mockCheckpointsService.findScheduleById.mockResolvedValue(mockScheduleWithDetails); - - const body = { - signer: 'signer', - points: [mockDate], - }; - - const result = await controller.createSchedule({ ticker: 'TICKER' }, body); - - const mockCreatedSchedule = new CheckpointScheduleModel({ - ...mockScheduleWithDetails.schedule, - ...mockScheduleWithDetails.details, - pendingPoints: [mockDate], - }); - expect(result).toEqual({ - ...txResult, - schedule: mockCreatedSchedule, - }); - }); - }); - - describe('getHolders', () => { - const mockAssetHolders = { - data: [ - { - identity: { did: '0xe2dd3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2' }, - balance: new BigNumber(627880), - }, - { - identity: { did: '0x666d3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2' }, - balance: new BigNumber(1000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - - const mockResult = new PaginatedResultsModel({ - results: [ - new IdentityBalanceModel({ - identity: '0xe2dd3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2', - balance: new BigNumber(627880), - }), - new IdentityBalanceModel({ - identity: '0x666d3f2cec45168793b700056404c88e17e2a4cd87060aa39a22f856be5c4fe2', - balance: new BigNumber(1000), - }), - ], - total: new BigNumber(2), - next: '0xddddd', - }); - it('should return the holders of an Asset at a given Checkpoint', async () => { - mockCheckpointsService.getHolders.mockResolvedValue(mockAssetHolders); - - const result = await controller.getHolders( - { - ticker: 'TICKER', - id: new BigNumber(1), - }, - { size: new BigNumber(10) } - ); - expect(result).toEqual(mockResult); - expect(mockCheckpointsService.getHolders).toBeCalled(); - }); - }); - - describe('getAssetBalance', () => { - it('should return the balance of an Asset for an Identity at a given Checkpoint', async () => { - const balance = new BigNumber(10); - const ticker = 'TICKER'; - const id = new BigNumber(1); - - const balanceModel = new IdentityBalanceModel({ balance, identity: did }); - - mockCheckpointsService.getAssetBalance.mockResolvedValue(balanceModel); - - const result = await controller.getAssetBalance({ - ticker, - did, - id, - }); - - expect(result).toEqual(balanceModel); - expect(mockCheckpointsService.getAssetBalance).toHaveBeenCalledWith(ticker, did, id); - }); - }); - - describe('deleteSchedule', () => { - it('should return the transaction details', async () => { - mockCheckpointsService.deleteScheduleByTicker.mockResolvedValue(txResult); - - const result = await controller.deleteSchedule( - { id: new BigNumber(1), ticker: 'TICKER' }, - { signer } - ); - - expect(result).toEqual(txResult); - }); - }); -}); diff --git a/src/checkpoints/checkpoints.controller.ts b/src/checkpoints/checkpoints.controller.ts deleted file mode 100644 index 66260338..00000000 --- a/src/checkpoints/checkpoints.controller.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { Checkpoint, CheckpointSchedule } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CheckpointParamsDto } from '~/checkpoints/dto/checkpoint.dto'; -import { CheckPointBalanceParamsDto } from '~/checkpoints/dto/checkpoint-balance.dto'; -import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; -import { CheckpointDetailsModel } from '~/checkpoints/models/checkpoint-details.model'; -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { CreatedCheckpointModel } from '~/checkpoints/models/created-checkpoint.model'; -import { CreatedCheckpointScheduleModel } from '~/checkpoints/models/created-checkpoint-schedule.model'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; - -class DeleteCheckpointScheduleParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class CheckpointScheduleParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('assets', 'checkpoints') -@Controller('assets/:ticker/checkpoints') -export class CheckpointsController { - constructor(private readonly checkpointsService: CheckpointsService) {} - - @ApiOperation({ - summary: 'Fetch Asset Checkpoints', - description: 'This endpoint will provide the list of Checkpoints created on this Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached Checkpoints are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Checkpoints to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Checkpoints are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiArrayResponse(CheckpointDetailsModel, { - description: 'List of Checkpoints created on this Asset', - paginated: true, - }) - @ApiBadRequestResponse({ - description: 'Schedule start date must be in the future', - }) - @Get() - public async getCheckpoints( - @Param() { ticker }: TickerParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.checkpointsService.findAllByTicker(ticker, size, start?.toString()); - - return new PaginatedResultsModel({ - results: data.map( - ({ checkpoint: { id }, createdAt, totalSupply }) => - new CheckpointDetailsModel({ - id, - createdAt, - totalSupply, - }) - ), - total, - next, - }); - } - - @ApiOperation({ - summary: 'Fetch details of an Asset Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Checkpoint is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint to be fetched', - type: 'string', - example: '1', - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint was not found', - }) - @ApiOkResponse({ - description: 'The Checkpoint details', - type: CheckpointDetailsModel, - }) - @Get('/:id') - public async getCheckpoint( - @Param() { ticker, id }: CheckpointParamsDto - ): Promise { - const checkpoint = await this.checkpointsService.findOne(ticker, id); - const [createdAt, totalSupply] = await Promise.all([ - checkpoint.createdAt(), - checkpoint.totalSupply(), - ]); - return new CheckpointDetailsModel({ id, createdAt, totalSupply }); - } - - @ApiOperation({ - summary: 'Create Checkpoint', - description: - 'This endpoint will create a snapshot of Asset holders and their respective balances at that moment', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Checkpoint is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Checkpoint', - type: CreatedCheckpointModel, - }) - @Post() - public async createCheckpoint( - @Param() { ticker }: TickerParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.checkpointsService.createByTicker(ticker, signerDto); - - const resolver: TransactionResolver = ({ - result: checkpoint, - transactions, - details, - }) => - new CreatedCheckpointModel({ - checkpoint, - transactions, - details, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Fetch all active Checkpoint Schedules', - description: - 'This endpoint will provide the list of active Schedules which create Checkpoints for a specific Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose attached Checkpoint Schedules are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(CheckpointScheduleModel, { - description: 'List of active Schedules which create Checkpoints for a specific Asset', - paginated: false, - }) - @Get('schedules') - public async getSchedules( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const schedules = await this.checkpointsService.findSchedulesByTicker(ticker); - return new ResultsModel({ - results: schedules.map( - ({ schedule: { id, pendingPoints, expiryDate }, details }) => - new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...details, - }) - ), - }); - } - - @ApiOperation({ - summary: 'Fetch details of an Asset Checkpoint Schedule', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Checkpoint Schedule is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint Schedule to be fetched', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'The Checkpoint Schedule details', - type: CheckpointScheduleModel, - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint Schedule does not exist', - }) - @Get('schedules/:id') - public async getSchedule( - @Param() { ticker, id }: CheckpointScheduleParamsDto - ): Promise { - const { - schedule: { pendingPoints, expiryDate }, - details, - } = await this.checkpointsService.findScheduleById(ticker, id); - - return new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...details, - }); - } - - @ApiOperation({ - summary: 'Create Schedule', - description: 'This endpoint will create a Schedule that creates Checkpoints periodically', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Checkpoint creation is to be scheduled', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Checkpoint Schedule', - type: CreatedCheckpointScheduleModel, - }) - @Post('schedules/create') - public async createSchedule( - @Param() { ticker }: TickerParamsDto, - @Body() createCheckpointScheduleDto: CreateCheckpointScheduleDto - ): Promise { - const serviceResult = await this.checkpointsService.createScheduleByTicker( - ticker, - createCheckpointScheduleDto - ); - - const resolver: TransactionResolver = async ({ - result: { id: createdScheduleId }, - transactions, - details, - }) => { - const { - schedule: { id, pendingPoints, expiryDate }, - details: scheduleDetails, - } = await this.checkpointsService.findScheduleById(ticker, createdScheduleId); - - return new CreatedCheckpointScheduleModel({ - schedule: new CheckpointScheduleModel({ - id, - ticker, - pendingPoints, - expiryDate, - ...scheduleDetails, - }), - transactions, - details, - }); - }; - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Get the Asset balance of the holders at a given Checkpoint', - description: 'This endpoint returns the Asset balance of holders at a given Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which to fetch holder balances', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Checkpoint for which to fetch Asset balances', - type: 'string', - example: '1', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Asset holders to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start key from which Asset holders are to be fetched', - type: 'string', - required: false, - example: 'START_KEY', - }) - @ApiNotFoundResponse({ - description: 'Either the Asset or the Checkpoint was not found', - }) - @ApiArrayResponse(IdentityBalanceModel, { - description: 'List of balances of the Asset holders at the Checkpoint', - paginated: true, - }) - @Get(':id/balances') - public async getHolders( - @Param() { ticker, id }: CheckpointParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.checkpointsService.getHolders(ticker, id, size, start?.toString()); - return new PaginatedResultsModel({ - results: data.map( - ({ identity, balance }) => new IdentityBalanceModel({ identity: identity.did, balance }) - ), - total, - next, - }); - } - - @ApiOperation({ - summary: 'Get the Asset balance for an Identity at a Checkpoint', - description: - 'This endpoint returns the Asset balance an Identity has at a particular Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the balance is to be fetched', - }) - @ApiParam({ - name: 'id', - description: 'The Checkpoint ID to from which to fetch the balance', - type: 'string', - example: '2', - }) - @ApiParam({ - name: 'did', - description: 'The Identity for which to fetch the Asset balance', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'The balance of the Asset the Identity held at a given Checkpoint', - type: IdentityBalanceModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset or Checkpoint was not found', - }) - @Get(':id/balances/:did') - public async getAssetBalance( - @Param() { ticker, did, id }: CheckPointBalanceParamsDto - ): Promise { - return this.checkpointsService.getAssetBalance(ticker, did, id); - } - - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete Schedule', - description: 'This endpoint will delete an existing Schedule for Checkpoint creation', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Schedule is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'Schedule ID to be deleted', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: "Schedule doesn't exist. It may have expired, been removed, or never been created", - }) - @Post('schedules/:id/delete') - public async deleteSchedule( - @Param() { ticker, id }: DeleteCheckpointScheduleParamsDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.checkpointsService.deleteScheduleByTicker( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } -} diff --git a/src/checkpoints/checkpoints.module.ts b/src/checkpoints/checkpoints.module.ts deleted file mode 100644 index 0649e397..00000000 --- a/src/checkpoints/checkpoints.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { CheckpointsController } from '~/checkpoints/checkpoints.controller'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [AssetsModule, TransactionsModule, LoggerModule], - providers: [CheckpointsService], - exports: [CheckpointsService], - controllers: [CheckpointsController], -}) -export class CheckpointsModule {} diff --git a/src/checkpoints/checkpoints.service.spec.ts b/src/checkpoints/checkpoints.service.spec.ts deleted file mode 100644 index 41d9d5d1..00000000 --- a/src/checkpoints/checkpoints.service.spec.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { CheckpointsService } from '~/checkpoints/checkpoints.service'; -import { CalendarUnit } from '~/common/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockCheckpoint, - MockCheckpointSchedule, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAssetService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer } = testValues; - -describe('CheckpointsService', () => { - let service: CheckpointsService; - let mockTransactionsService: MockTransactionsService; - - const mockAssetsService = new MockAssetService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CheckpointsService, - AssetsService, - mockPolymeshLoggerProvider, - mockTransactionsProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(CheckpointsService); - mockTransactionsService = mockTransactionsProvider.useValue; - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByTicker', () => { - const mockCheckpoints = { - data: [ - { - checkpoint: { - id: new BigNumber(1), - }, - createdAt: new Date(), - totalSupply: new BigNumber(10000), - }, - ], - next: '0xddddd', - count: new BigNumber(2), - }; - it('should return the list of Checkpoints created on an Asset', async () => { - const mockAsset = new MockAsset(); - mockAsset.checkpoints.get.mockResolvedValue(mockCheckpoints); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', new BigNumber(1)); - - expect(result).toEqual(mockCheckpoints); - }); - - it('should return the list of Checkpoints created on an Asset from start key', async () => { - const mockAsset = new MockAsset(); - mockAsset.checkpoints.get.mockResolvedValue(mockCheckpoints); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', new BigNumber(1), 'START_KEY'); - - expect(result).toEqual(mockCheckpoints); - }); - }); - - describe('findOne', () => { - it('should return a checkpoint for a valid ticker and id', async () => { - const mockAsset = new MockAsset(); - const mockCheckpoint = new MockCheckpoint(); - mockAsset.checkpoints.getOne.mockResolvedValue(mockCheckpoint); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findOne('TICKER', new BigNumber(1)); - expect(result).toEqual(mockCheckpoint); - expect(mockAssetsService.findFungible).toBeCalledWith('TICKER'); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - const mockAsset = new MockAsset(); - mockAsset.checkpoints.getOne.mockRejectedValue(mockError); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne('TICKER', new BigNumber(1))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findSchedulesByTicker', () => { - it('should return the list of active Checkpoint Schedules for an Asset', async () => { - const mockSchedules = [ - { - schedule: { - id: new BigNumber(1), - period: { - unit: CalendarUnit.Month, - amount: new BigNumber(3), - }, - start: new Date(), - complexity: new BigNumber(4), - expiryDate: null, - }, - details: { - remainingCheckpoints: new BigNumber(1), - nextCheckpointDate: new Date(), - }, - }, - ]; - - const mockAsset = new MockAsset(); - mockAsset.checkpoints.schedules.get.mockResolvedValue(mockSchedules); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findSchedulesByTicker('TICKER'); - - expect(result).toEqual(mockSchedules); - }); - }); - - describe('findScheduleById', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - const id = new BigNumber(1); - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should return the Schedule for a valid ticker and id', async () => { - const mockScheduleWithDetails = { - schedule: new MockCheckpointSchedule(), - details: { - remainingCheckpoints: 1, - nextCheckpointDate: new Date(), - }, - }; - mockAsset.checkpoints.schedules.getOne.mockResolvedValue(mockScheduleWithDetails); - - const result = await service.findScheduleById(ticker, id); - - expect(result).toEqual(mockScheduleWithDetails); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAsset.checkpoints.schedules.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findScheduleById(ticker, id)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - afterEach(() => { - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - expect(mockAsset.checkpoints.schedules.getOne).toHaveBeenCalledWith({ - id, - }); - }); - }); - - describe('createByTicker', () => { - it('should create a Checkpoint and return the queue results', async () => { - const mockCheckpoint = new MockCheckpoint(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.CreateCheckpoint, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockCheckpoint, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockReturnValue(mockAsset); - - const body = { - signer, - }; - - const result = await service.createByTicker('TICKER', body); - expect(result).toEqual({ - result: mockCheckpoint, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.create, - {}, - { - signer, - } - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('createScheduleByTicker', () => { - it('should create a Checkpoint Schedule and return the queue results', async () => { - const mockCheckpointSchedule = new MockCheckpointSchedule(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.CreateSchedule, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockCheckpointSchedule, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockReturnValue(mockAsset); - - const mockDate = new Date(); - const params = { - signer, - points: [mockDate], - }; - - const result = await service.createScheduleByTicker('TICKER', params); - expect(result).toEqual({ - result: mockCheckpointSchedule, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.schedules.create, - { - points: [mockDate], - }, - { - signer, - } - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('getHolders', () => { - const mockHolders = { - data: [ - { - identity: { - did: '0x06000', - }, - balance: new BigNumber(1000), - }, - ], - next: '0xddddd', - count: new BigNumber(1), - }; - it('should return the list of Asset holders at a Checkpoint', async () => { - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.allBalances.mockResolvedValue(mockHolders); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - - const result = await service.getHolders('TICKER', new BigNumber(1), new BigNumber(1)); - - expect(result).toEqual(mockHolders); - expect(mockCheckpoint.allBalances).toHaveBeenCalledWith({ - size: new BigNumber(1), - start: undefined, - }); - }); - - it('should return the list of Asset holders at a Checkpoint from a start key', async () => { - const mockCheckpoint = new MockCheckpoint(); - mockCheckpoint.allBalances.mockResolvedValue(mockHolders); - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - - const result = await service.getHolders( - 'TICKER', - new BigNumber(1), - new BigNumber(10), - 'START_KEY' - ); - - expect(result).toEqual(mockHolders); - expect(mockCheckpoint.allBalances).toHaveBeenCalledWith({ - start: 'START_KEY', - size: new BigNumber(10), - }); - }); - }); - - describe('getAssetBalance', () => { - it('should fetch the Asset balance for an Identity at a given Checkpoint', async () => { - const id = new BigNumber(1); - const balance = new BigNumber(10); - const mockCheckpoint = new MockCheckpoint(); - const did = '0x6000'; - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockCheckpoint as any); - mockCheckpoint.balance.mockResolvedValue(balance); - - const mockAsset = new MockAsset(); - mockAsset.checkpoints.getOne.mockResolvedValue(mockCheckpoint); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.getAssetBalance('TICKER', did, id); - expect(result).toEqual({ balance, identity: did }); - expect(mockCheckpoint.balance).toHaveBeenCalledWith({ identity: did }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith('TICKER'); - }); - }); - - describe('deleteScheduleByTicker', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.checkpoint.RemoveSchedule, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockAsset = new MockAsset(); - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - const ticker = 'TICKER'; - const id = new BigNumber(1); - - const result = await service.deleteScheduleByTicker(ticker, id, { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.checkpoints.schedules.remove, - { - schedule: id, - }, - { - signer, - } - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - }); -}); diff --git a/src/checkpoints/checkpoints.service.ts b/src/checkpoints/checkpoints.service.ts deleted file mode 100644 index 9b027e6a..00000000 --- a/src/checkpoints/checkpoints.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Checkpoint, - CheckpointSchedule, - CheckpointWithData, - IdentityBalance, - ResultSet, - ScheduleWithDetails, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { IdentityBalanceModel } from '~/assets/models/identity-balance.model'; -import { CreateCheckpointScheduleDto } from '~/checkpoints/dto/create-checkpoint-schedule.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class CheckpointsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(CheckpointsService.name); - } - - public async findAllByTicker( - ticker: string, - size: BigNumber, - start?: string - ): Promise> { - const asset = await this.assetsService.findFungible(ticker); - return asset.checkpoints.get({ start, size }); - } - - public async findOne(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - return await asset.checkpoints.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findSchedulesByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.checkpoints.schedules.get(); - } - - public async findScheduleById(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - return await asset.checkpoints.schedules.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async createByTicker( - ticker: string, - signerDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionsService.submit(asset.checkpoints.create, {}, signerDto); - } - - public async createScheduleByTicker( - ticker: string, - createCheckpointScheduleDto: CreateCheckpointScheduleDto - ): ServiceReturn { - const { base, args } = extractTxBase(createCheckpointScheduleDto); - - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionsService.submit(asset.checkpoints.schedules.create, args, base); - } - - public async getAssetBalance( - ticker: string, - did: string, - checkpointId: BigNumber - ): Promise { - const checkpoint = await this.findOne(ticker, checkpointId); - const balance = await checkpoint.balance({ identity: did }); - return new IdentityBalanceModel({ identity: did, balance }); - } - - public async getHolders( - ticker: string, - checkpointId: BigNumber, - size: BigNumber, - start?: string - ): Promise> { - const checkpoint = await this.findOne(ticker, checkpointId); - return checkpoint.allBalances({ start, size }); - } - - public async deleteScheduleByTicker( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findFungible(ticker); - return this.transactionsService.submit( - asset.checkpoints.schedules.remove, - { schedule: id }, - transactionBaseDto - ); - } -} diff --git a/src/checkpoints/dto/checkpoint-balance.dto.ts b/src/checkpoints/dto/checkpoint-balance.dto.ts deleted file mode 100644 index 103aa7cf..00000000 --- a/src/checkpoints/dto/checkpoint-balance.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { IsDid, IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class CheckPointBalanceParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; - - @IsDid() - readonly did: string; -} diff --git a/src/checkpoints/dto/checkpoint.dto.ts b/src/checkpoints/dto/checkpoint.dto.ts deleted file mode 100644 index b9fba493..00000000 --- a/src/checkpoints/dto/checkpoint.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ - -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class CheckpointParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} diff --git a/src/checkpoints/dto/create-checkpoint-schedule.dto.ts b/src/checkpoints/dto/create-checkpoint-schedule.dto.ts deleted file mode 100644 index 3849daa9..00000000 --- a/src/checkpoints/dto/create-checkpoint-schedule.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateCheckpointScheduleDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Periodic interval between Checkpoints. For example, a period of 2 weeks means that a Checkpoint will be created every 2 weeks. A null value means this Schedule creates a single Checkpoint and then expires', - type: Date, - nullable: true, - isArray: true, - }) - @IsDate() - @ValidateNested() - readonly points: Date[]; -} diff --git a/src/checkpoints/models/checkpoint-details.model.ts b/src/checkpoints/models/checkpoint-details.model.ts deleted file mode 100644 index 360cfe11..00000000 --- a/src/checkpoints/models/checkpoint-details.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CheckpointDetailsModel { - @ApiProperty({ - description: 'ID of the Checkpoint', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Date at which the Checkpoint was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'Total supply of the Asset at this Checkpoint', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalSupply: BigNumber; - - constructor(model: CheckpointDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/models/checkpoint-schedule.model.ts b/src/checkpoints/models/checkpoint-schedule.model.ts deleted file mode 100644 index 79767e98..00000000 --- a/src/checkpoints/models/checkpoint-schedule.model.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CheckpointScheduleModel { - @ApiProperty({ - description: 'ID of the Schedule', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset whose Checkpoints will be created with this Schedule', - type: 'string', - example: 'TICKER', - }) - readonly ticker: string; - - @ApiProperty({ - description: - 'Date at which the last Checkpoint will be created with this Schedule. A null value means that this Schedule never expires', - type: 'string', - nullable: true, - example: new Date('10/14/1987').toISOString(), - }) - readonly expiryDate: Date | null; - - @ApiProperty({ - description: - 'An array of dates for pending points. A date in the array represents a scheduled time to create a Checkpoint.', - type: 'array', - items: { type: 'string', format: 'date-time' }, - isArray: true, - example: [new Date('1987-10-14T00:00:00.000Z').toISOString()], - }) - readonly pendingPoints: Date[]; - - @ApiProperty({ - description: 'Number of Checkpoints left to be created by the Schedule', - type: 'string', - example: '10', - }) - @FromBigNumber() - readonly remainingCheckpoints: BigNumber; - - @ApiProperty({ - description: 'Date when the next Checkpoint will be created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly nextCheckpointDate: Date; - - constructor(model: CheckpointScheduleModel) { - Object.assign(this, model); - } -} diff --git a/src/checkpoints/models/created-checkpoint-schedule.model.ts b/src/checkpoints/models/created-checkpoint-schedule.model.ts deleted file mode 100644 index e583183f..00000000 --- a/src/checkpoints/models/created-checkpoint-schedule.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { CheckpointScheduleModel } from '~/checkpoints/models/checkpoint-schedule.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedCheckpointScheduleModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Schedule', - type: CheckpointScheduleModel, - }) - @Type(() => CheckpointScheduleModel) - readonly schedule: CheckpointScheduleModel; - - constructor(model: CreatedCheckpointScheduleModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/checkpoints/models/created-checkpoint.model.ts b/src/checkpoints/models/created-checkpoint.model.ts deleted file mode 100644 index 66ef72b3..00000000 --- a/src/checkpoints/models/created-checkpoint.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Checkpoint } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedCheckpointModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Identifiers of the newly created Checkpoint', - example: { - id: '1', - ticker: 'TICKER', - }, - }) - @FromEntity() - readonly checkpoint: Checkpoint; - - constructor(model: CreatedCheckpointModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/claims/claims.controller.spec.ts b/src/claims/claims.controller.spec.ts deleted file mode 100644 index 18592cf0..00000000 --- a/src/claims/claims.controller.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { NotFoundException } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsController } from '~/claims/claims.controller'; -import { ClaimsService } from '~/claims/claims.service'; -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; -import { CustomClaimTypeWithDid } from '~/claims/models/custom-claim-type-did.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { testValues } from '~/test-utils/consts'; -import { mockClaimsServiceProvider } from '~/test-utils/service-mocks'; - -const { did, txResult, signer } = testValues; - -describe('ClaimsController', () => { - let controller: ClaimsController; - let mockClaimsService: DeepMocked; - - const mockPayload: ModifyClaimsDto = { - claims: [ - { - target: did, - claim: { - type: ClaimType.Accredited, - }, - }, - ], - signer, - }; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - controllers: [ClaimsController], - providers: [mockClaimsServiceProvider, mockPolymeshLoggerProvider], - }).compile(); - - mockClaimsService = mockClaimsServiceProvider.useValue as DeepMocked; - controller = module.get(ClaimsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('addClaims', () => { - it('should call addClaimsOnDid method and return transaction data', async () => { - mockClaimsService.addClaimsOnDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.addClaims(mockPayload); - - expect(mockClaimsService.addClaimsOnDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('editClaims', () => { - it('should call editClaimsOnDid method and return transaction data', async () => { - mockClaimsService.editClaimsOnDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.editClaims(mockPayload); - - expect(mockClaimsService.editClaimsOnDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('revokeClaims', () => { - it('should call revokeClaimsFromDid method and return transaction data', async () => { - mockClaimsService.revokeClaimsFromDid.mockResolvedValue({ ...txResult, result: undefined }); - - const result = await controller.revokeClaims(mockPayload); - - expect(mockClaimsService.revokeClaimsFromDid).toHaveBeenCalledWith(mockPayload); - - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('registerCustomClaimType', () => { - const mockRegisterCustomClaimTypeDto = { - name: 'CustomClaimType', - description: 'Test', - signer, - }; - - it('should call registerCustomClaimType method and return transaction data', async () => { - mockClaimsService.registerCustomClaimType.mockResolvedValue({ - ...txResult, - result: new BigNumber(123), - }); - - const result = await controller.registerCustomClaimType(mockRegisterCustomClaimTypeDto); - - expect(mockClaimsService.registerCustomClaimType).toHaveBeenCalledWith( - mockRegisterCustomClaimTypeDto - ); - expect(result).toEqual({ ...txResult, results: undefined }); - }); - }); - - describe('getCustomClaimTypeById', () => { - const mockId = new BigNumber(1); - const mockName = 'CustomClaimType'; - const mockResult = { - id: mockId, - name: mockName, - }; - - it('should return custom claim type by ID', async () => { - mockClaimsService.getCustomClaimTypeById.mockResolvedValue(mockResult); - - const result = await controller.getCustomClaimType({ - identifier: mockId, - } as GetCustomClaimTypeDto); - - expect(mockClaimsService.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - expect(result).toEqual(new CustomClaimTypeModel(mockResult)); - }); - - it('should throw NotFoundException when custom claim type is not found', async () => { - mockClaimsService.getCustomClaimTypeById.mockResolvedValue(null); - - await expect( - controller.getCustomClaimType({ identifier: mockId } as GetCustomClaimTypeDto) - ).rejects.toThrow(NotFoundException); - }); - - it('should return custom claim type by name', async () => { - mockClaimsService.getCustomClaimTypeByName.mockResolvedValue(mockResult); - - const result = await controller.getCustomClaimType({ - identifier: mockName, - } as GetCustomClaimTypeDto); - - expect(mockClaimsService.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - expect(result).toEqual(new CustomClaimTypeModel(mockResult)); - }); - - it('should throw NotFoundException when custom claim type is not found', async () => { - mockClaimsService.getCustomClaimTypeByName.mockResolvedValue(null); - - await expect( - controller.getCustomClaimType({ identifier: mockName } as GetCustomClaimTypeDto) - ).rejects.toThrow(NotFoundException); - }); - }); - - describe('getCustomClaimTypes', () => { - const mockId = new BigNumber(1); - const mockName = 'CustomClaimType'; - const mockCustomClaim = { - id: mockId, - name: mockName, - }; - const mockResult = { - data: [mockCustomClaim], - count: new BigNumber(1), - next: null, - }; - - const mockResponse = new PaginatedResultsModel({ - results: [new CustomClaimTypeWithDid(mockCustomClaim)], - total: new BigNumber(1), - next: null, - }); - - it('should paginated result set of CustomClaimTypes', async () => { - mockClaimsService.getRegisteredCustomClaimTypes.mockResolvedValue(mockResult); - const size = new BigNumber(10); - - const result = await controller.getCustomClaimTypes({ size }); - - expect(mockClaimsService.getRegisteredCustomClaimTypes).toHaveBeenCalledWith( - size, - new BigNumber(0), - undefined - ); - expect(result).toEqual(mockResponse); - }); - - it('should paginated result set of CustomClaimTypes for the provided dids', async () => { - mockClaimsService.getRegisteredCustomClaimTypes.mockResolvedValue(mockResult); - const size = new BigNumber(10); - const dids = [did]; - - const result = await controller.getCustomClaimTypes({ size, dids }); - - expect(mockClaimsService.getRegisteredCustomClaimTypes).toHaveBeenCalledWith( - size, - new BigNumber(0), - dids - ); - expect(result).toEqual(mockResponse); - }); - }); -}); diff --git a/src/claims/claims.controller.ts b/src/claims/claims.controller.ts deleted file mode 100644 index 4cae4f54..00000000 --- a/src/claims/claims.controller.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { - Body, - Controller, - Get, - HttpStatus, - NotFoundException, - Param, - Post, - Query, -} from '@nestjs/common'; -import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CustomClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsService } from '~/claims/claims.service'; -import { GetCustomClaimTypePipe } from '~/claims/decorators/get-custom-claim-type.pipe'; -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; -import { GetCustomClaimTypesDto } from '~/claims/dto/get-custom-claim-types.dto'; -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { RegisterCustomClaimTypeDto } from '~/claims/dto/register-custom-claim-type.dto'; -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; -import { CustomClaimTypeWithDid } from '~/claims/models/custom-claim-type-did.model'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@ApiTags('claims') -@Controller('claims') -export class ClaimsController { - constructor( - private readonly claimsService: ClaimsService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(ClaimsController.name); - } - - @ApiOperation({ - summary: 'Add Claims targeting an Identity', - description: 'This endpoint will add Claims to an Identity', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - "A target Identity cannot have CDD claims with different IDs' this should also be added", - ], - [HttpStatus.NOT_FOUND]: ['Some of the supplied Identity IDs do not exist'], - }) - @Post('add') - async addClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.addClaimsOnDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Edit Claims targeting an Identity', - description: 'This endpoint allows changing the expiry of a Claim', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Account does not have the required roles or permissions'], - }) - @Post('edit') - async editClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.editClaimsOnDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Remove provided Claims from an Identity', - description: 'This endpoint will remove Claims from an Identity', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Account does not have the required roles or permissions'], - [HttpStatus.BAD_REQUEST]: [ - 'Attempt to revoke Investor Uniqueness claims from investors with positive balance', - ], - }) - @Post('remove') - async revokeClaims(@Body() args: ModifyClaimsDto): Promise { - const serviceResult = await this.claimsService.revokeClaimsFromDid(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Register a CustomClaimType', - description: 'This endpoint will add the CustomClaimType to the network', - }) - @ApiTransactionResponse({ - description: 'Transaction response', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: [ - 'Validation: CustomClaimType name length exceeded', - 'Validation: The CustomClaimType with provided name already exists', - ], - }) - @Post('custom-claim-type') - async registerCustomClaimType( - @Body() args: RegisterCustomClaimTypeDto - ): Promise { - const serviceResult = await this.claimsService.registerCustomClaimType(args); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Get CustomClaimTypes', - description: 'This endpoint fetches CustomClaimTypes that have been registered"', - }) - @Get('custom-claim-types') - async getCustomClaimTypes( - @Query() { size, start, dids }: GetCustomClaimTypesDto - ): Promise> { - const { data, count, next } = await this.claimsService.getRegisteredCustomClaimTypes( - size, - new BigNumber(start ?? 0), - dids - ); - - return new PaginatedResultsModel({ - results: data.map(claimType => new CustomClaimTypeWithDid(claimType)), - total: count, - next, - }); - } - - @ApiOperation({ - summary: 'Get CustomClaimType by ID', - description: 'This endpoint fetches the CustomClaimType, identified by its ID or Name', - }) - @ApiParam({ - name: 'identifier', - description: 'The ID or Name the CustomClaimType', - type: 'string', - required: false, - example: '1', - }) - @ApiNotFoundResponse({ - description: 'The CustomClaimType does not exist', - }) - @Get('custom-claim-types/:identifier') - async getCustomClaimType( - @Param('identifier', GetCustomClaimTypePipe) { identifier }: GetCustomClaimTypeDto - ): Promise { - let result: CustomClaimType | null; - - if (identifier instanceof BigNumber) { - result = await this.claimsService.getCustomClaimTypeById(identifier); - } else { - result = await this.claimsService.getCustomClaimTypeByName(identifier); - } - - if (!result) { - throw new NotFoundException('Custom claim type not found'); - } - - return new CustomClaimTypeModel(result); - } -} diff --git a/src/claims/claims.module.ts b/src/claims/claims.module.ts deleted file mode 100644 index 0d4fbcc7..00000000 --- a/src/claims/claims.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ClaimsController } from '~/claims/claims.controller'; -import { ClaimsService } from '~/claims/claims.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - controllers: [ClaimsController], - imports: [PolymeshModule, TransactionsModule, LoggerModule], - providers: [ClaimsService], - exports: [ClaimsService], -}) -export class ClaimsModule {} diff --git a/src/claims/claims.service.spec.ts b/src/claims/claims.service.spec.ts deleted file mode 100644 index 106b2d81..00000000 --- a/src/claims/claims.service.spec.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimData, ClaimType, ResultSet, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimsService } from '~/claims/claims.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; - -describe('ClaimsService', () => { - let claimsService: ClaimsService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockTransactionsService: MockTransactionsService; - - const { did, signer, ticker } = testValues; - - const mockModifyClaimsArgs = { - claims: [ - { - target: did, - claim: { - type: ClaimType.Accredited, - }, - }, - ], - signer, - }; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ClaimsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - claimsService = module.get(ClaimsService); - polymeshService = module.get(PolymeshService); - mockTransactionsService = mockTransactionsProvider.useValue; - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(claimsService).toBeDefined(); - }); - - describe('findIssuedByDid', () => { - it('should return the issued Claims', async () => { - const claimsResult = { - data: [], - next: null, - count: new BigNumber(0), - } as ResultSet; - mockPolymeshApi.claims.getIssuedClaims.mockResolvedValue(claimsResult); - const result = await claimsService.findIssuedByDid('did'); - expect(result).toBe(claimsResult); - }); - }); - - describe('findAssociatedByDid', () => { - it('should return the associated Claims', async () => { - const mockAssociatedClaims = [ - { - issuedAt: '2020-08-21T16:36:55.000Z', - expiry: null, - claim: { - type: ClaimType.Accredited, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - target: { - did, - }, - issuer: { - did: '0x6'.padEnd(66, '1'), - }, - }, - ]; - - const mockIdentitiesWithClaims = { - data: [ - { - identity: { - did, - }, - claims: mockAssociatedClaims, - }, - ], - next: null, - count: new BigNumber(1), - }; - mockPolymeshApi.claims.getIdentitiesWithClaims.mockResolvedValue(mockIdentitiesWithClaims); - const result = await claimsService.findAssociatedByDid(did); - expect(result).toStrictEqual({ - data: mockAssociatedClaims, - next: null, - count: new BigNumber(1), - }); - }); - }); - - describe('addClaimsToDid', () => { - it('should run a addClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddClaim, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.addClaimsOnDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.addClaims, - { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } - ); - }); - }); - - describe('editClaimsToDid', () => { - it('should run a editClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddClaim, // hmm thought it would be edit claim - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.editClaimsOnDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.editClaims, - { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } - ); - }); - }); - - describe('revokeClaimsFromDid', () => { - it('should run a revokeClaims procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RevokeClaim, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.revokeClaimsFromDid(mockModifyClaimsArgs); - - expect(result).toBe(mockTransaction); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.revokeClaims, - { claims: mockModifyClaimsArgs.claims }, - { signer: mockModifyClaimsArgs.signer } - ); - }); - }); - - describe('findCddClaimsByDid', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ]; - - it('should return a list of CDD Claims for given DID', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: true, - }); - }); - - it('should return a list of CDD Claims for given DID without including expired claims', async () => { - mockPolymeshApi.claims.getCddClaims.mockResolvedValue(mockCddClaims); - - const result = await claimsService.findCddClaimsByDid(did, false); - - expect(result).toBe(mockCddClaims); - - expect(mockPolymeshApi.claims.getCddClaims).toHaveBeenCalledWith({ - target: did, - includeExpired: false, - }); - }); - }); - - describe('findClaimScopesByDid', () => { - it('should return claim scopes for the target identity', async () => { - const mockClaims = [ - { - ticker, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - ]; - - mockPolymeshApi.claims.getClaimScopes.mockResolvedValue(mockClaims); - - const result = await claimsService.findClaimScopesByDid(did); - - expect(result).toBe(mockClaims); - - expect(mockPolymeshApi.claims.getClaimScopes).toHaveBeenCalledWith({ target: did }); - }); - }); - - describe('getCustomClaimTypeByName', () => { - it('should return custom claim type by name', async () => { - const mockName = 'CustomClaimType'; - const mockResult = { id: new BigNumber(1), name: mockName, description: 'Test' }; - - mockPolymeshApi.claims.getCustomClaimTypeByName.mockResolvedValue(mockResult); - - const result = await claimsService.getCustomClaimTypeByName(mockName); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - }); - - it('should return null if custom claim type is not found', async () => { - const mockName = 'NonExistentClaimType'; - mockPolymeshApi.claims.getCustomClaimTypeByName.mockResolvedValue(null); - - const result = await claimsService.getCustomClaimTypeByName(mockName); - expect(result).toBeNull(); - expect(mockPolymeshApi.claims.getCustomClaimTypeByName).toHaveBeenCalledWith(mockName); - }); - }); - - describe('getCustomClaimTypeById', () => { - it('should return custom claim type by id', async () => { - const mockId = new BigNumber(1); - const mockResult = { id: mockId, name: 'CustomClaimType', description: 'Test' }; - - mockPolymeshApi.claims.getCustomClaimTypeById.mockResolvedValue(mockResult); - - const result = await claimsService.getCustomClaimTypeById(mockId); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - }); - - it('should return null if custom claim type is not found', async () => { - const mockId = new BigNumber(999); - mockPolymeshApi.claims.getCustomClaimTypeById.mockResolvedValue(null); - - const result = await claimsService.getCustomClaimTypeById(mockId); - expect(result).toBeNull(); - expect(mockPolymeshApi.claims.getCustomClaimTypeById).toHaveBeenCalledWith(mockId); - }); - }); - - describe('registerCustomClaimType', () => { - it('should submit a transaction to register a custom claim type', async () => { - const mockRegisterCustomClaimTypeDto = { - name: 'CustomClaimType', - description: 'Test', - signer: 'Alice', - }; - const mockTransaction = new MockTransaction({ - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RegisterCustomClaimType, - }); - - mockTransactionsService.submit.mockResolvedValue(mockTransaction); - - const result = await claimsService.registerCustomClaimType(mockRegisterCustomClaimTypeDto); - - expect(result).toEqual(mockTransaction); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.claims.registerCustomClaimType, - { - name: mockRegisterCustomClaimTypeDto.name, - description: mockRegisterCustomClaimTypeDto.description, - }, - { signer: mockRegisterCustomClaimTypeDto.signer } - ); - }); - }); - - describe('getRegisteredCustomClaimTypes', () => { - it('should call the sdk and return the result', async () => { - const start = new BigNumber(0); - const size = new BigNumber(10); - const dids = [did]; - const mockResult = { - data: [], - count: new BigNumber(1), - next: new BigNumber(1), - }; - - mockPolymeshApi.claims.getAllCustomClaimTypes.mockResolvedValue(mockResult); - - const result = await claimsService.getRegisteredCustomClaimTypes(size, start, dids); - expect(result).toEqual(mockResult); - expect(mockPolymeshApi.claims.getAllCustomClaimTypes).toHaveBeenCalledWith({ - start, - size, - dids, - }); - }); - }); -}); diff --git a/src/claims/claims.service.ts b/src/claims/claims.service.ts deleted file mode 100644 index d91f39c6..00000000 --- a/src/claims/claims.service.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AddClaimsParams, - CddClaim, - ClaimData, - ClaimScope, - ClaimType, - CustomClaimType, - CustomClaimTypeWithDid, - ModifyClaimsParams, - ResultSet, - RevokeClaimsParams, - Scope, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ModifyClaimsDto } from '~/claims/dto/modify-claims.dto'; -import { RegisterCustomClaimTypeDto } from '~/claims/dto/register-custom-claim-type.dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class ClaimsService { - constructor( - private readonly polymeshService: PolymeshService, - private transactionsService: TransactionsService - ) {} - - public async findIssuedByDid( - target: string, - includeExpired?: boolean, - size?: BigNumber, - start?: BigNumber - ): Promise> { - return await this.polymeshService.polymeshApi.claims.getIssuedClaims({ - target, - includeExpired, - size, - start, - }); - } - - public async findAssociatedByDid( - target: string, - scope?: Scope, - claimTypes?: ClaimType[], - includeExpired?: boolean, - size?: BigNumber, - start?: BigNumber - ): Promise> { - const identitiesWithClaims = - await this.polymeshService.polymeshApi.claims.getIdentitiesWithClaims({ - targets: [target], - scope, - claimTypes, - includeExpired, - size, - start, - }); - return { - data: identitiesWithClaims.data?.[0].claims || [], - next: identitiesWithClaims.next, - count: identitiesWithClaims.count, - }; - } - - public async addClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); - - const { addClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(addClaims, args as AddClaimsParams, base); - } - - public async editClaimsOnDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); - - const { editClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(editClaims, args as ModifyClaimsParams, base); - } - - public async revokeClaimsFromDid(modifyClaimsDto: ModifyClaimsDto): ServiceReturn { - const { base, args } = extractTxBase(modifyClaimsDto); - - const { revokeClaims } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit(revokeClaims, args as RevokeClaimsParams, base); - } - - public async findClaimScopesByDid(target: string): Promise { - return this.polymeshService.polymeshApi.claims.getClaimScopes({ - target, - }); - } - - public async findCddClaimsByDid( - target: string, - includeExpired = true - ): Promise[]> { - return await this.polymeshService.polymeshApi.claims.getCddClaims({ - target, - includeExpired, - }); - } - - public async getCustomClaimTypeByName(name: string): Promise { - return this.polymeshService.polymeshApi.claims.getCustomClaimTypeByName(name); - } - - public async getCustomClaimTypeById(id: BigNumber): Promise { - return this.polymeshService.polymeshApi.claims.getCustomClaimTypeById(id); - } - - public async registerCustomClaimType( - registerCustomClaimTypeDto: RegisterCustomClaimTypeDto - ): ServiceReturn { - const { base, args } = extractTxBase(registerCustomClaimTypeDto); - - const { registerCustomClaimType } = this.polymeshService.polymeshApi.claims; - - return this.transactionsService.submit( - registerCustomClaimType, - args as RegisterCustomClaimTypeDto, - base - ); - } - - public async getRegisteredCustomClaimTypes( - size?: BigNumber, - start?: BigNumber, - dids?: string[] - ): Promise> { - return this.polymeshService.polymeshApi.claims.getAllCustomClaimTypes({ size, start, dids }); - } -} diff --git a/src/claims/decorators/get-custom-claim-type.pipe.ts b/src/claims/decorators/get-custom-claim-type.pipe.ts deleted file mode 100644 index 7efa3f59..00000000 --- a/src/claims/decorators/get-custom-claim-type.pipe.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable, PipeTransform } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { GetCustomClaimTypeDto } from '~/claims/dto/get-custom-claim-type.dto'; - -@Injectable() -export class GetCustomClaimTypePipe implements PipeTransform { - transform(value: string): GetCustomClaimTypeDto { - if (isNumericString(value)) { - return { identifier: new BigNumber(value) }; - } else { - return { identifier: value }; - } - } -} - -function isNumericString(value: string): boolean { - return !isNaN(Number(value)); -} diff --git a/src/claims/decorators/validation.ts b/src/claims/decorators/validation.ts deleted file mode 100644 index 3a808520..00000000 --- a/src/claims/decorators/validation.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; -import { - IsHexadecimal, - isHexadecimal, - isUppercase, - Length, - length, - Matches, - matches, - maxLength, - registerDecorator, - ValidationArguments, - ValidationOptions, -} from 'class-validator'; -import { isString } from 'lodash'; - -import { MAX_TICKER_LENGTH } from '~/assets/assets.consts'; -import { CDD_ID_LENGTH, DID_LENGTH } from '~/identities/identities.consts'; - -export function IsCddId() { - return applyDecorators( - IsHexadecimal({ - message: 'cddId must be a hexadecimal number', - }), - Matches(/^0x.+/, { - message: 'cddId must start with "0x"', - }), - Length(CDD_ID_LENGTH, undefined, { - message: `cddId must be ${CDD_ID_LENGTH} characters long`, - }) - ); -} - -/** - * Applies validation to a scope value field based on a scope type. - * `property` specifies which field to use as the scope type (probably 'type'). - */ -export function IsValidScopeValue(property: string, validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isValidScopeValue', - target: object.constructor, - options: validationOptions, - constraints: [property], - propertyName, - validator: { - validate(value: unknown, args: ValidationArguments) { - const [scopeTypeField] = args.constraints; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scopeType = (args.object as any)[scopeTypeField]; - switch (scopeType) { - case ScopeType.Ticker: - return maxLength(value, MAX_TICKER_LENGTH) && isUppercase(value); - case ScopeType.Identity: - return ( - isHexadecimal(value) && - isString(value) && - matches(value, /^0x.+/) && - length(value, DID_LENGTH, undefined) - ); - case ScopeType.Custom: - return false; - default: - return true; - } - }, - defaultMessage(args: ValidationArguments) { - const [scopeTypeField] = args.constraints; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scopeType = (args.object as any)[scopeTypeField]; - switch (scopeType) { - case ScopeType.Ticker: - return `value must be all uppercase and no longer than 12 characters for type: ${scopeType}`; - case ScopeType.Identity: - return `value must be a hex string ${DID_LENGTH} characters long and prefixed with 0x`; - case ScopeType.Custom: - return 'ScopeType.Custom not currently supported'; - } - return `value must be a valid scope value for ${property}: ${scopeType}`; - }, - }, - }); - }; -} diff --git a/src/claims/dto/claim-target.dto.ts b/src/claims/dto/claim-target.dto.ts deleted file mode 100644 index 60d24c35..00000000 --- a/src/claims/dto/claim-target.dto.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, ValidateNested } from 'class-validator'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { IsDid } from '~/common/decorators/validation'; - -export class ClaimTargetDto { - @ApiProperty({ - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiProperty({ - description: 'The Claim to be added, modified or removed', - type: ClaimDto, - }) - @ValidateNested() - @Type(() => ClaimDto) - claim: ClaimDto; - - @ApiPropertyOptional({ - description: 'The expiry date of the Claim', - example: new Date('05/23/2021').toISOString(), - }) - @IsOptional() - @IsDate() - expiry?: Date; -} diff --git a/src/claims/dto/claim.dto.spec.ts b/src/claims/dto/claim.dto.spec.ts deleted file mode 100644 index 36925b3c..00000000 --- a/src/claims/dto/claim.dto.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, CountryCode, ScopeType } from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -describe('claimsDto', () => { - const scope = { - type: ScopeType.Identity, - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }; - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: ClaimDto, - data: '', - }; - describe('valid Claims', () => { - const cases: ValidCase[] = [ - [ - 'Accredited with `scope`', - { - type: ClaimType.Accredited, - scope, - }, - ], - [ - 'Affiliate with `scope`', - { - type: ClaimType.Affiliate, - scope, - }, - ], - [ - 'BuyLockup with `scope`', - { - type: ClaimType.BuyLockup, - scope, - }, - ], - [ - 'SellLockup with `scope`', - { - type: ClaimType.SellLockup, - scope, - }, - ], - [ - 'CustomerDueDiligence with `cddId`', - { - type: ClaimType.CustomerDueDiligence, - cddId: '0x60000000000000000000000000000000', - }, - ], - [ - 'KnowYourCustomer with `scope`', - { - type: ClaimType.KnowYourCustomer, - scope, - }, - ], - [ - 'Jurisdiction claim with `code` and `scope`', - { - type: ClaimType.Jurisdiction, - scope, - code: CountryCode.Ca, - }, - ], - [ - 'Exempted claim with `scope`', - { - type: ClaimType.Exempted, - scope, - }, - ], - [ - 'Blocked claim with `scope`', - { - type: ClaimType.Blocked, - scope, - }, - ], - [ - 'Accredited with valid `issuers`', - { - type: ClaimType.Accredited, - scope, - issuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ], - }, - ], - [ - 'Custom claim with `customClaimTypeId`', - { - type: ClaimType.Custom, - scope, - customClaimTypeId: new BigNumber('1'), - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${err.getResponse().message}`); - }); - }); - }); - - describe('invalid Claims', () => { - const cases: InvalidCase[] = [ - [ - 'Jurisdiction claim without `code`', - { - type: ClaimType.Jurisdiction, - scope, - }, - [ - 'code must be one of the following values: Af, Ax, Al, Dz, As, Ad, Ao, Ai, Aq, Ag, Ar, Am, Aw, Au, At, Az, Bs, Bh, Bd, Bb, By, Be, Bz, Bj, Bm, Bt, Bo, Ba, Bw, Bv, Br, Vg, Io, Bn, Bg, Bf, Bi, Kh, Cm, Ca, Cv, Ky, Cf, Td, Cl, Cn, Hk, Mo, Cx, Cc, Co, Km, Cg, Cd, Ck, Cr, Ci, Hr, Cu, Cy, Cz, Dk, Dj, Dm, Do, Ec, Eg, Sv, Gq, Er, Ee, Et, Fk, Fo, Fj, Fi, Fr, Gf, Pf, Tf, Ga, Gm, Ge, De, Gh, Gi, Gr, Gl, Gd, Gp, Gu, Gt, Gg, Gn, Gw, Gy, Ht, Hm, Va, Hn, Hu, Is, In, Id, Ir, Iq, Ie, Im, Il, It, Jm, Jp, Je, Jo, Kz, Ke, Ki, Kp, Kr, Kw, Kg, La, Lv, Lb, Ls, Lr, Ly, Li, Lt, Lu, Mk, Mg, Mw, My, Mv, Ml, Mt, Mh, Mq, Mr, Mu, Yt, Mx, Fm, Md, Mc, Mn, Me, Ms, Ma, Mz, Mm, Na, Nr, Np, Nl, An, Nc, Nz, Ni, Ne, Ng, Nu, Nf, Mp, No, Om, Pk, Pw, Ps, Pa, Pg, Py, Pe, Ph, Pn, Pl, Pt, Pr, Qa, Re, Ro, Ru, Rw, Bl, Sh, Kn, Lc, Mf, Pm, Vc, Ws, Sm, St, Sa, Sn, Rs, Sc, Sl, Sg, Sk, Si, Sb, So, Za, Gs, Ss, Es, Lk, Sd, Sr, Sj, Sz, Se, Ch, Sy, Tw, Tj, Tz, Th, Tl, Tg, Tk, To, Tt, Tn, Tr, Tm, Tc, Tv, Ug, Ua, Ae, Gb, Us, Um, Uy, Uz, Vu, Ve, Vn, Vi, Wf, Eh, Ye, Zm, Zw, Bq, Cw, Sx', - ], - ], - [ - 'Jurisdiction claim with bad `code`', - { - type: ClaimType.Jurisdiction, - scope, - code: '123', - }, - [ - 'code must be one of the following values: Af, Ax, Al, Dz, As, Ad, Ao, Ai, Aq, Ag, Ar, Am, Aw, Au, At, Az, Bs, Bh, Bd, Bb, By, Be, Bz, Bj, Bm, Bt, Bo, Ba, Bw, Bv, Br, Vg, Io, Bn, Bg, Bf, Bi, Kh, Cm, Ca, Cv, Ky, Cf, Td, Cl, Cn, Hk, Mo, Cx, Cc, Co, Km, Cg, Cd, Ck, Cr, Ci, Hr, Cu, Cy, Cz, Dk, Dj, Dm, Do, Ec, Eg, Sv, Gq, Er, Ee, Et, Fk, Fo, Fj, Fi, Fr, Gf, Pf, Tf, Ga, Gm, Ge, De, Gh, Gi, Gr, Gl, Gd, Gp, Gu, Gt, Gg, Gn, Gw, Gy, Ht, Hm, Va, Hn, Hu, Is, In, Id, Ir, Iq, Ie, Im, Il, It, Jm, Jp, Je, Jo, Kz, Ke, Ki, Kp, Kr, Kw, Kg, La, Lv, Lb, Ls, Lr, Ly, Li, Lt, Lu, Mk, Mg, Mw, My, Mv, Ml, Mt, Mh, Mq, Mr, Mu, Yt, Mx, Fm, Md, Mc, Mn, Me, Ms, Ma, Mz, Mm, Na, Nr, Np, Nl, An, Nc, Nz, Ni, Ne, Ng, Nu, Nf, Mp, No, Om, Pk, Pw, Ps, Pa, Pg, Py, Pe, Ph, Pn, Pl, Pt, Pr, Qa, Re, Ro, Ru, Rw, Bl, Sh, Kn, Lc, Mf, Pm, Vc, Ws, Sm, St, Sa, Sn, Rs, Sc, Sl, Sg, Sk, Si, Sb, So, Za, Gs, Ss, Es, Lk, Sd, Sr, Sj, Sz, Se, Ch, Sy, Tw, Tj, Tz, Th, Tl, Tg, Tk, To, Tt, Tn, Tr, Tm, Tc, Tv, Ug, Ua, Ae, Gb, Us, Um, Uy, Uz, Vu, Ve, Vn, Vi, Wf, Eh, Ye, Zm, Zw, Bq, Cw, Sx', - ], - ], - [ - 'Accredited without `scope`', - { - type: ClaimType.Accredited, - }, - ['scope must be a non-empty object'], - ], - [ - 'Affiliate with bad `scope`', - { - type: ClaimType.Affiliate, - scope: { type: 'Wrong', value: 123 }, - }, - ['scope.type must be one of the following values: Identity, Ticker, Custom'], - ], - [ - 'CustomerDueDiligence without `cddId`', - { - type: ClaimType.CustomerDueDiligence, - }, - [ - 'cddId must be a hexadecimal number', - 'cddId must start with "0x"', - 'cddId must be 34 characters long', - ], - ], - [ - 'Accredited with bad ClaimType in `issuers`', - { - type: ClaimType.Accredited, - scope, - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: ['Bad Claims'], - }, - ], - }, - [ - 'trustedClaimIssuers.0.each value in trustedFor must be one of the following values: Accredited, Affiliate, BuyLockup, SellLockup, CustomerDueDiligence, KnowYourCustomer, Jurisdiction, Exempted, Blocked, Custom', - ], - ], - ]; - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/claims/dto/claim.dto.ts b/src/claims/dto/claim.dto.ts deleted file mode 100644 index 6d4ec79c..00000000 --- a/src/claims/dto/claim.dto.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { isCddClaim } from '@polymeshassociation/polymesh-sdk/utils'; -import { Type } from 'class-transformer'; -import { IsEnum, IsNotEmptyObject, IsOptional, ValidateIf, ValidateNested } from 'class-validator'; - -import { IsCddId } from '~/claims/decorators/validation'; -import { ScopeDto } from '~/claims/dto/scope.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class ClaimDto { - @ApiProperty({ - description: 'The type of Claim. Note that different types require different fields', - enum: ClaimType, - example: ClaimType.Accredited, - }) - @IsEnum(ClaimType) - type: ClaimType; - - @ApiPropertyOptional({ - description: - 'The scope of the Claim. Required for most types except for `CustomerDueDiligence`, `InvestorUniquenessV2` and `NoData`', - type: ScopeDto, - }) - @ValidateIf(claim => !isCddClaim(claim)) - @ValidateNested() - @Type(() => ScopeDto) - @IsNotEmptyObject() - scope?: ScopeDto; - - @ApiPropertyOptional({ - description: 'Country code for `Jurisdiction` type Claims', - enum: CountryCode, - example: CountryCode.Ca, - }) - @ValidateIf(({ type }) => type === ClaimType.Jurisdiction) - @IsEnum(CountryCode) - code?: CountryCode; - - @ApiPropertyOptional({ - description: 'CustomClaimType Id', - example: '1', - }) - @ValidateIf(({ type }) => type === ClaimType.Custom) - @ToBigNumber() - customClaimTypeId?: BigNumber; - - @ApiPropertyOptional({ - description: 'cddId for `CustomerDueDiligence` and `InvestorUniqueness` type Claims', - example: '0x60000000000000000000000000000000', - }) - @ValidateIf(({ type }) => [ClaimType.CustomerDueDiligence].includes(type)) - @IsCddId() - cddId?: string; - - @ApiPropertyOptional({ - description: 'Optional Identities to trust for this Claim. Defaults to all', - isArray: true, - type: TrustedClaimIssuerDto, - }) - @ValidateNested({ each: true }) - @IsOptional() - @Type(() => TrustedClaimIssuerDto) - trustedClaimIssuers?: TrustedClaimIssuerDto[]; -} diff --git a/src/claims/dto/claims-filter.dto.ts b/src/claims/dto/claims-filter.dto.ts deleted file mode 100644 index 1cf493ea..00000000 --- a/src/claims/dto/claims-filter.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IncludeExpiredFilterDto } from '~/common/dto/params.dto'; - -export class ClaimsFilterDto extends IncludeExpiredFilterDto { - @IsEnum(ClaimType, { each: true }) - @IsOptional() - readonly claimTypes?: ClaimType[]; -} diff --git a/src/claims/dto/get-custom-claim-type.dto.ts b/src/claims/dto/get-custom-claim-type.dto.ts deleted file mode 100644 index caf5a4f3..00000000 --- a/src/claims/dto/get-custom-claim-type.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; - -export class GetCustomClaimTypeDto { - @ApiProperty({ - description: 'The ID or Name of the CustomClaimType', - example: '1', - }) - @ToBigNumber() - readonly identifier: BigNumber | string; -} diff --git a/src/claims/dto/get-custom-claim-types.dto.ts b/src/claims/dto/get-custom-claim-types.dto.ts deleted file mode 100644 index 5a56b86d..00000000 --- a/src/claims/dto/get-custom-claim-types.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; - -export class GetCustomClaimTypesDto extends PaginatedParamsDto { - @ApiPropertyOptional({ - description: - 'Filter CustomClaimTypes by DIDs that registered the CustomClaimType.
If none specified, returns all CustomClaimTypes ordered by ID', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsOptional() - @IsDid({ each: true }) - readonly dids?: string[]; -} diff --git a/src/claims/dto/modify-claims.dto.ts b/src/claims/dto/modify-claims.dto.ts deleted file mode 100644 index 7bc3e8b8..00000000 --- a/src/claims/dto/modify-claims.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { ClaimTargetDto } from '~/claims/dto/claim-target.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyClaimsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'An array of Claims. Note that different types of Claims require different fields', - isArray: true, - type: ClaimTargetDto, - }) - @Type(() => ClaimTargetDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - claims: ClaimTargetDto[]; -} diff --git a/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts b/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts deleted file mode 100644 index b39c7911..00000000 --- a/src/claims/dto/proof-scope-id-cdd-id-match.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsString } from 'class-validator'; - -export class ProofScopeIdCddIdMatchDto { - @ApiProperty({ - type: 'string', - isArray: true, - required: true, - description: 'Challenge responses', - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0700000000000000000000000000000000000000000000000000000000000000', - ], - }) - @IsArray() - @ArrayMinSize(2) - @ArrayMaxSize(2) - @IsString({ each: true }) - readonly challengeResponses: [string, string]; - - @ApiProperty({ - type: 'string', - description: 'The subtracted expressions result', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly subtractExpressionsRes: string; - - @ApiProperty({ - type: 'string', - description: 'The blinded scope DID hash', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - required: true, - }) - @IsString() - readonly blindedScopeDidHash: string; -} diff --git a/src/claims/dto/register-custom-claim-type.dto.ts b/src/claims/dto/register-custom-claim-type.dto.ts deleted file mode 100644 index fd5f9654..00000000 --- a/src/claims/dto/register-custom-claim-type.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RegisterCustomClaimTypeDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the CustomClaimType to be registered', - example: 'Can Buy Asset', - }) - @IsString() - readonly name: string; -} diff --git a/src/claims/dto/scope-claim-proof.dto.ts b/src/claims/dto/scope-claim-proof.dto.ts deleted file mode 100644 index 17ec59eb..00000000 --- a/src/claims/dto/scope-claim-proof.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateIf } from 'class-validator'; - -import { ProofScopeIdCddIdMatchDto } from '~/claims/dto/proof-scope-id-cdd-id-match.dto'; -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; - -@ApiExtraModels(ProofScopeIdCddIdMatchDto) -export class ScopeClaimProofDto { - @ApiProperty({ - description: 'The proof scope Id of the claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsString() - readonly proofScopeIdWellFormed: string; - - @ApiPropertyOneOf({ - description: 'The proof scope Id of the claim', - union: [ - { - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ProofScopeIdCddIdMatchDto, - ], - }) - @ValidateIf(({ proofScopeIdCddIdMatch }) => typeof proofScopeIdCddIdMatch !== 'string') - @Type(() => ProofScopeIdCddIdMatchDto) - readonly proofScopeIdCddIdMatch: string | ProofScopeIdCddIdMatchDto; -} diff --git a/src/claims/dto/scope.dto.ts b/src/claims/dto/scope.dto.ts deleted file mode 100644 index 02779d2b..00000000 --- a/src/claims/dto/scope.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsValidScopeValue } from '~/claims/decorators/validation'; - -export class ScopeDto { - @ApiProperty({ - description: - 'The type of Scope. If `Identity` then `value` should be a DID. If `Ticker` then `value` should be a Ticker', - enum: ScopeType, - example: ScopeType.Identity, - }) - @IsEnum(ScopeType) - readonly type: ScopeType; - - @ApiProperty({ - description: - 'The value of the Scope. This is a hex prefixed 64 character string for `Identity`, 12 uppercase letters for Ticker', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsValidScopeValue('type') - readonly value: string; -} diff --git a/src/claims/models/cdd-claim.model.ts b/src/claims/models/cdd-claim.model.ts deleted file mode 100644 index 0a7a616a..00000000 --- a/src/claims/models/cdd-claim.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -export class CddClaimModel { - @ApiProperty({ - type: 'string', - description: 'Claim type', - example: 'CustomerDueDiligence', - }) - readonly type: ClaimType.CustomerDueDiligence; - - @ApiProperty({ - type: 'string', - description: 'ID of the Claim', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly id: string; - - constructor(model: CddClaimModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/claim-scope.model.ts b/src/claims/models/claim-scope.model.ts deleted file mode 100644 index bd500b8a..00000000 --- a/src/claims/models/claim-scope.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { ScopeModel } from '~/claims/models/scope.model'; - -export class ClaimScopeModel { - @ApiProperty({ - description: 'The scope that has been assigned to Identity', - nullable: true, - type: ScopeModel, - }) - @Type(() => ScopeModel) - readonly scope: ScopeModel | null; - - @ApiPropertyOptional({ - type: 'string', - description: 'The ticker to which the scope is valid for', - example: 'TICKER', - }) - readonly ticker?: string; - - constructor(model: ClaimScopeModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/claim.model.ts b/src/claims/models/claim.model.ts deleted file mode 100644 index a6c6339e..00000000 --- a/src/claims/models/claim.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Claim, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class ClaimModel { - @ApiProperty({ - type: 'string', - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly target: Identity; - - @ApiProperty({ - type: 'string', - description: 'DID of the issuer Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly issuer: Identity; - - @ApiProperty({ - type: 'string', - description: 'Date when the Claim was issued', - example: new Date('10/14/1987').toISOString(), - }) - readonly issuedAt: Date; - - @ApiProperty({ - type: 'string', - nullable: true, - description: 'Expiry date of the Claim', - example: new Date('10/14/1987').toISOString(), - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Details of the Claim containing type and scope', - example: { - type: 'Accredited', - scope: { - type: 'Identity', - value: '0x6'.padEnd(66, '1a'), - }, - }, - }) - readonly claim: T; - - constructor(model: ClaimModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/custom-claim-type-did.model.ts b/src/claims/models/custom-claim-type-did.model.ts deleted file mode 100644 index e7cda25c..00000000 --- a/src/claims/models/custom-claim-type-did.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; - -import { CustomClaimTypeModel } from '~/claims/models/custom-claim-type.model'; - -export class CustomClaimTypeWithDid extends CustomClaimTypeModel { - @ApiPropertyOptional({ - type: 'string', - description: 'The DID of identity that registered the CustomClaimType', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did?: string; - - constructor(model: CustomClaimTypeWithDid) { - super(model); - this.did = model.did; - } -} diff --git a/src/claims/models/custom-claim-type.model.ts b/src/claims/models/custom-claim-type.model.ts deleted file mode 100644 index 35032ed3..00000000 --- a/src/claims/models/custom-claim-type.model.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class CustomClaimTypeModel { - @ApiProperty({ - type: 'string', - description: 'CustomClaimType Id', - example: 1, - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'CustomClaimType name', - example: 'CustomClaimType', - }) - readonly name: string; - - constructor(model: CustomClaimTypeModel) { - Object.assign(this, model); - } -} diff --git a/src/claims/models/scope.model.ts b/src/claims/models/scope.model.ts deleted file mode 100644 index bbfc4c13..00000000 --- a/src/claims/models/scope.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { ScopeType } from '@polymeshassociation/polymesh-sdk/types'; - -export class ScopeModel { - @ApiProperty({ - description: - 'The type of Scope. If `Identity` then `value` should be a DID. If `Ticker` then `value` should be a Ticker', - enum: ScopeType, - example: ScopeType.Identity, - }) - readonly type: ScopeType; - - @ApiProperty({ - type: 'string', - example: '0x6'.padEnd(66, '1a'), - description: - 'The value of the Scope. This is a hex prefixed 64 character string for `Identity`, 12 uppercase letters for Ticker', - }) - readonly value: string; - - constructor(model: ScopeModel) { - Object.assign(this, model); - } -} diff --git a/src/commands/repl.ts b/src/commands/repl.ts index ec1f724c..f1777188 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -2,7 +2,8 @@ import { repl } from '@nestjs/core'; -import { AppModule } from '~/app.module'; +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './../app.module'; async function bootstrap(): Promise { await repl(AppModule); diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts index 00d4faba..78a276b7 100644 --- a/src/commands/write-swagger.ts +++ b/src/commands/write-swagger.ts @@ -2,9 +2,11 @@ import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { writeFileSync } from 'fs'; -import { AppModule } from '~/app.module'; import { swaggerDescription, swaggerTitle } from '~/common/utils'; +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './../app.module'; + const writeSwaggerSpec = async (): Promise => { const app = await NestFactory.create(AppModule, { logger: false }); await app.init(); diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts deleted file mode 100644 index 0d310547..00000000 --- a/src/common/decorators/swagger.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* istanbul ignore file */ - -import { applyDecorators, HttpStatus, Type } from '@nestjs/common'; -import { - ApiAcceptedResponse, - ApiBadRequestResponse, - ApiCreatedResponse, - ApiExtraModels, - ApiNotFoundResponse, - ApiOkResponse, - ApiProperty, - ApiPropertyOptions, - ApiResponseOptions, - ApiUnprocessableEntityResponse, - getSchemaPath, - OmitType, -} from '@nestjs/swagger'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; - -import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { Class } from '~/common/types'; - -export const ApiArrayResponse = ( - model: TModel, - { - paginated, - example, - examples, - description, - }: { - paginated: boolean; - example?: unknown; - examples?: unknown[] | Record; - description?: string; - } = { - paginated: true, - } -): ReturnType => { - const extraModels = []; - let items; - if (typeof model === 'string') { - items = { type: model }; - } else { - extraModels.push(model); - items = { $ref: getSchemaPath(model) }; - } - - return applyDecorators( - ApiOkResponse({ - description, - schema: { - allOf: [ - { $ref: getSchemaPath(paginated ? PaginatedResultsModel : ResultsModel) }, - { - properties: { - results: { - type: 'array', - items, - example, - examples, - description, - }, - }, - }, - ], - }, - }), - ApiExtraModels(PaginatedResultsModel, ResultsModel, ...extraModels) - ); -}; - -export const ApiArrayResponseReplaceModelProperties = ( - Model: Type, - { - paginated, - example, - examples, - description, - }: { - paginated: boolean; - example?: unknown; - examples?: unknown[] | Record; - description?: string; - } = { - paginated: true, - }, - extendItems: Record -): ReturnType => { - const extraModels = []; - const items: SchemaObject = {}; - const keys = Object.keys(extendItems) as K[]; - - const obj = new Model() as unknown as Type; - const name = `${obj.constructor.name}-Omit-${keys.join('-')}`; - - const intermediary = { - [name]: class extends OmitType( - Model as unknown as Class, - keys as unknown as readonly never[] - ) {}, - }; - - items.allOf = [{ $ref: getSchemaPath(intermediary[name]) }]; - extraModels.push(intermediary[name]); - - for (const [key, value] of Object.entries(extendItems)) { - if (typeof value === 'function') { - extraModels.push(value); - items.allOf.push({ - type: 'object', - properties: { - [key]: { $ref: getSchemaPath(value) }, - }, - }); - } - - if (typeof value === 'string') { - items.allOf.push({ - type: 'object', - properties: { - [key]: { type: value }, - }, - }); - } - } - - return applyDecorators( - ApiOkResponse({ - description, - schema: { - allOf: [ - { $ref: getSchemaPath(paginated ? PaginatedResultsModel : ResultsModel) }, - { - properties: { - results: { - type: 'array', - items, - example, - examples, - description, - }, - }, - }, - ], - }, - }), - ApiExtraModels(PaginatedResultsModel, ResultsModel, ...extraModels) - ); -}; - -type ApiPropertyOneOfOptions = Omit & { - union: (Omit | Type)[]; -}; - -/** - * Create a property decorator with `oneOf` attribute whose value is set to the SchemaObject or ReferenceObject of the items of `union` parameter - * - * @note Non-schema objects in `union` must be defined as extra models using the `ApiExtraModels` decorator(at the class-level) - */ -export const ApiPropertyOneOf = ({ - union, - ...apiPropertyOptions -}: ApiPropertyOneOfOptions): ReturnType => { - const oneOfItems: (SchemaObject | ReferenceObject)[] = []; - - union.forEach(item => { - if (typeof item === 'object') { - oneOfItems.push(item); - } else { - oneOfItems.push({ $ref: getSchemaPath(item) }); - } - }); - - return applyDecorators(ApiProperty({ ...apiPropertyOptions, oneOf: oneOfItems })); -}; - -/** - * A helper that functions like `ApiCreatedResponse`, that also adds an `ApiAccepted` response in case `webhookUrl` is passed - * - * @param options - these will be passed to the `ApiCreatedResponse` decorator - */ -export function ApiTransactionResponse( - options: ApiResponseOptions -): ReturnType { - return applyDecorators( - ApiCreatedResponse(options), - ApiAcceptedResponse({ - description: - 'Returned if `webhookUrl` is passed in the body. A response will be returned after the transaction has been validated. The result will be posted to the `webhookUrl` given when the transaction is completed', - type: NotificationPayloadModel, - }) - ); -} - -type SupportedHttpStatusCodes = - | HttpStatus.NOT_FOUND - | HttpStatus.BAD_REQUEST - | HttpStatus.UNPROCESSABLE_ENTITY; - -/** - * A helper that combines responses for SDK Errors like `BadRequestException`, `NotFoundException`, `UnprocessableEntityException` - * - * @param messages - key value map of HTTP response code to their description that will be passed to appropriate `MethodDecorator` - */ -export function ApiTransactionFailedResponse( - messages: Partial> -): ReturnType { - const decorators: MethodDecorator[] = []; - - Object.entries(messages).forEach(([statusCode, rawDescription]) => { - const description = - rawDescription.length > 1 - ? `
  • ${rawDescription.join('
  • ')}
` - : rawDescription[0]; - - switch (Number(statusCode)) { - case HttpStatus.NOT_FOUND: - decorators.push(ApiNotFoundResponse({ description })); - break; - case HttpStatus.BAD_REQUEST: - decorators.push(ApiBadRequestResponse({ description })); - break; - case HttpStatus.UNPROCESSABLE_ENTITY: - decorators.push(ApiUnprocessableEntityResponse({ description })); - break; - } - }); - - return applyDecorators(...decorators); -} diff --git a/src/common/decorators/transformation.ts b/src/common/decorators/transformation.ts deleted file mode 100644 index 36840fbf..00000000 --- a/src/common/decorators/transformation.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { isEntity } from '@polymeshassociation/polymesh-sdk/utils'; -import { Transform } from 'class-transformer'; -import { mapValues } from 'lodash'; - -import { Entity } from '~/common/types'; - -/** - * String -> BigNumber - */ -export function ToBigNumber() { - return applyDecorators( - Transform(({ value }: { value: string | Array }) => { - if (value instanceof Array) { - return value.map(val => new BigNumber(val)); - } else { - return new BigNumber(value); - } - }) - ); -} - -/** - * Entity -> POJO - */ -export function FromEntity() { - return applyDecorators(Transform(({ value }: { value: Entity }) => value?.toHuman())); -} - -/** - * Transform all SDK Entities in the object/array into their serialized versions, - * or serialize the value if it is an SDK Entity in - */ -export function FromEntityObject() { - return applyDecorators(Transform(({ value }: { value: unknown }) => toHumanObject(value))); -} - -function toHumanObject(obj: unknown): unknown { - if (isEntity(obj)) { - return obj.toHuman(); - } - - if (Array.isArray(obj)) { - return obj.map(toHumanObject); - } - - if (obj instanceof Date) { - return obj.toISOString(); - } - - if (obj instanceof BigNumber && !obj.isNaN()) { - return obj.toString(); - } - - if (obj && typeof obj === 'object') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return mapValues(obj as any, val => toHumanObject(val)); - } - return obj; -} - -/** - * BigNumber -> string - */ -export function FromBigNumber() { - return applyDecorators( - Transform(({ value }: { value: BigNumber | BigNumber[] }) => { - if (value instanceof Array) { - return value.map(val => val.toString()); - } else { - return value?.toString(); - } - }) - ); -} diff --git a/src/common/decorators/validation.ts b/src/common/decorators/validation.ts deleted file mode 100644 index 72a741a8..00000000 --- a/src/common/decorators/validation.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - IsHexadecimal, - IsUppercase, - Length, - Matches, - MaxLength, - registerDecorator, - ValidationArguments, - ValidationOptions, -} from 'class-validator'; - -import { MAX_TICKER_LENGTH } from '~/assets/assets.consts'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; -import { DID_LENGTH } from '~/identities/identities.consts'; - -export function IsDid(validationOptions?: ValidationOptions) { - return applyDecorators( - IsHexadecimal({ - ...validationOptions, - message: 'DID must be a hexadecimal number', - }), - Matches(/^0x.+/, { - ...validationOptions, - message: 'DID must start with "0x"', - }), - Length(DID_LENGTH, undefined, { - ...validationOptions, - message: `DID must be ${DID_LENGTH} characters long`, - }) - ); -} - -export function IsTicker(validationOptions?: ValidationOptions) { - return applyDecorators( - MaxLength(MAX_TICKER_LENGTH, validationOptions), - IsUppercase(validationOptions) - ); -} - -export function IsBigNumber( - numericValidations: { min?: number; max?: number } = {}, - validationOptions?: ValidationOptions -) { - const isDefined = (v: number | undefined): v is number => typeof v !== 'undefined'; - const { min, max } = numericValidations; - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isBigNumber', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - if (value instanceof Array) { - return value.every(val => val instanceof BigNumber); - } - if (!(value instanceof BigNumber)) { - return false; - } - if (value.isNaN()) { - return false; - } - if (isDefined(min) && value.lt(min)) { - return false; - } - if (isDefined(max) && value.gt(max)) { - return false; - } - - return true; - }, - defaultMessage(args: ValidationArguments) { - let message = `${args.property} must be a number`; - const hasMin = isDefined(min); - const hasMax = isDefined(max); - if (hasMin && hasMax) { - message += ` that is between ${min} and ${max}`; - } else if (hasMin) { - message += ` that is at least ${min}`; - } else if (hasMax) { - message += ` that is at most ${max}`; - } - return message; - }, - }, - }); - }; -} - -// TODO @prashantasdeveloper Reduce the below code from two decorators if possible - IsTxTag and IsTxTagOrModuleName -export function IsTxTag(validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isTxTag', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - return typeof value === 'string' && getTxTags().includes(value); - }, - defaultMessage(args: ValidationArguments) { - if (validationOptions?.each) { - return `${args.property} must have all valid enum values`; - } - return `${args.property} must be a valid enum value`; - }, - }, - }); - }; -} - -export function IsTxTagOrModuleName(validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isTxTagOrModuleName', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: unknown) { - return typeof value === 'string' && getTxTagsWithModuleNames().includes(value); - }, - defaultMessage(args: ValidationArguments) { - if (validationOptions?.each) { - return `${args.property} must have all valid enum values from "ModuleName" or "TxTags"`; - } - return `${args.property} must be a valid enum value from "ModuleName" or "TxTags"`; - }, - }, - }); - }; -} diff --git a/src/common/dto/id-params.dto.ts b/src/common/dto/id-params.dto.ts deleted file mode 100644 index ddbb5796..00000000 --- a/src/common/dto/id-params.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class IdParamsDto { - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; -} diff --git a/src/common/dto/paginated-params.dto.ts b/src/common/dto/paginated-params.dto.ts deleted file mode 100644 index e4bf393a..00000000 --- a/src/common/dto/paginated-params.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsOptional, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class PaginatedParamsDto { - @ValidateIf(({ start }: PaginatedParamsDto) => !!start) - @IsBigNumber({ - max: 30, - }) - @ToBigNumber() - readonly size: BigNumber = new BigNumber(10); - - @IsOptional() - readonly start?: string | BigNumber; -} diff --git a/src/common/dto/params.dto.ts b/src/common/dto/params.dto.ts deleted file mode 100644 index 015fbe18..00000000 --- a/src/common/dto/params.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** istanbul ignore file */ - -import { IsBoolean, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class DidDto { - @IsDid() - readonly did: string; -} - -export class IncludeExpiredFilterDto { - @IsBoolean() - @IsOptional() - readonly includeExpired?: boolean = true; -} diff --git a/src/common/dto/transaction-base-dto.ts b/src/common/dto/transaction-base-dto.ts deleted file mode 100644 index f2301271..00000000 --- a/src/common/dto/transaction-base-dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; - -export class TransactionBaseDto { - @ApiProperty({ - description: 'An identifier for the account that should sign the transaction', - example: 'alice', - }) - @IsString() - readonly signer: string; - - // Hide the property so the interactive examples work without additional setup - @ApiHideProperty() - @IsOptional() - @IsString() - @IsUrl() - readonly webhookUrl?: string; - - @ApiProperty({ - description: - 'An optional property that when set to `true` will will verify the validity of the transaction without submitting it to the chain', - example: false, - }) - @IsBoolean() - @IsOptional() - readonly dryRun?: boolean; -} diff --git a/src/common/dto/transfer-ownership.dto.ts b/src/common/dto/transfer-ownership.dto.ts deleted file mode 100644 index 84057a1c..00000000 --- a/src/common/dto/transfer-ownership.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class TransferOwnershipDto extends TransactionBaseDto { - @ApiProperty({ - type: 'string', - description: 'DID of the target Identity', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiPropertyOptional({ - description: 'Date at which the authorization request for transfer expires', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/common/errors.ts b/src/common/errors.ts deleted file mode 100644 index ade8c80b..00000000 --- a/src/common/errors.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* istanbul ignore file */ - -export enum AppErrorCode { - NotFound = 'NotFound', - Conflict = 'Conflict', - Config = 'Config', - Validation = 'Validation', - Unauthorized = 'Unauthorized', - Unprocessable = 'Unprocessable', - Internal = 'Internal', -} - -export abstract class AppError extends Error { - public readonly code: AppErrorCode; -} - -export class AppNotFoundError extends AppError { - public readonly code = AppErrorCode.NotFound; - - constructor(id: string, resource: string) { - const identifierMessage = id !== '' ? `: with identifier: "${id}"` : ''; - - super(`Not Found: ${resource} was not found${identifierMessage}`); - } -} - -export class AppConflictError extends AppError { - public readonly code = AppErrorCode.Conflict; - - constructor(id: string, resource: string) { - super(`Conflict: ${resource} already exists with unique identifier: "${id}"`); - } -} - -export class AppConfigError extends AppError { - public readonly code = AppErrorCode.Config; - - constructor(key: string, message: string) { - super(`Config: ${key}: ${message}`); - } -} -export class AppValidationError extends AppError { - public readonly code = AppErrorCode.Validation; - - constructor(message: string) { - super(`Validation: ${message}`); - } -} - -export class AppUnauthorizedError extends AppError { - public readonly code = AppErrorCode.Unauthorized; - - constructor(message: string) { - super(`Unauthorized: ${message}`); - } -} - -export class AppUnprocessableError extends AppError { - public readonly code = AppErrorCode.Unprocessable; - - constructor(message: string) { - super(`Unprocessable: ${message}`); - } -} - -export class AppInternalError extends AppError { - public readonly code = AppErrorCode.Internal; - - constructor(message: string) { - super(`Internal: ${message}`); - } -} - -export function isAppError(err: unknown): err is AppError { - return err instanceof AppError; -} diff --git a/src/common/filters/app-error-to-http-response.filter.spec.ts b/src/common/filters/app-error-to-http-response.filter.spec.ts deleted file mode 100644 index fb8097b5..00000000 --- a/src/common/filters/app-error-to-http-response.filter.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ArgumentsHost, HttpStatus } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; - -import { - AppConfigError, - AppConflictError, - AppError, - AppInternalError, - AppNotFoundError, - AppUnauthorizedError, - AppUnprocessableError, -} from '~/common/errors'; -import { AppErrorToHttpResponseFilter } from '~/common/filters/app-error-to-http-response.filter'; -import { testValues } from '~/test-utils/consts'; - -const { resource } = testValues; - -type ExpectedReplyArgs = [{ message: string; statusCode: number }, HttpStatus]; -type Case = [AppError, ExpectedReplyArgs]; - -describe('AppErrorToHttpResponseFilter', () => { - const mockReplyFn = jest.fn(); - const mockHttpAdaptorHost = createMock(); - mockHttpAdaptorHost.httpAdapter.reply = mockReplyFn; - const mockHost = createMock(); - const errorToHttpResponseFilter = new AppErrorToHttpResponseFilter(mockHttpAdaptorHost); - - const notFoundError = new AppNotFoundError(resource.id, resource.type); - const conflictError = new AppConflictError(resource.id, resource.type); - const configError = new AppConfigError('TEST_CONFIG', 'is a test error'); - const unauthorizedError = new AppUnauthorizedError('test'); - const unprocessesableError = new AppUnprocessableError('test'); - const internalError = new AppInternalError('internal test'); - - const cases: Case[] = [ - [notFoundError, [{ message: notFoundError.message, statusCode: 404 }, HttpStatus.NOT_FOUND]], - [conflictError, [{ message: conflictError.message, statusCode: 409 }, HttpStatus.CONFLICT]], - [ - configError, - [{ message: 'Internal Server Error', statusCode: 500 }, HttpStatus.INTERNAL_SERVER_ERROR], - ], - [ - unauthorizedError, - [{ message: unauthorizedError.message, statusCode: 401 }, HttpStatus.UNAUTHORIZED], - ], - [ - unprocessesableError, - [{ message: unprocessesableError.message, statusCode: 422 }, HttpStatus.UNPROCESSABLE_ENTITY], - ], - [ - internalError, - [{ message: 'Internal Server Error', statusCode: 500 }, HttpStatus.INTERNAL_SERVER_ERROR], - ], - ]; - - test.each(cases)('should transform %p into %p', async (error, expected) => { - errorToHttpResponseFilter.catch(error, mockHost); - return expect(mockReplyFn).toHaveBeenCalledWith({}, ...expected); - }); - - it('should throw if an unknown Error is encountered', () => { - const unknownError = new Error('unknown error') as AppError; - return expect(() => errorToHttpResponseFilter.catch(unknownError, mockHost)).toThrow(); - }); -}); diff --git a/src/common/filters/app-error-to-http-response.filter.ts b/src/common/filters/app-error-to-http-response.filter.ts deleted file mode 100644 index 51981eb0..00000000 --- a/src/common/filters/app-error-to-http-response.filter.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; - -import { AppError, AppErrorCode } from '~/common/errors'; -import { UnreachableCaseError } from '~/common/utils'; - -/** - * Catches and converts AppErrors to the appropriate HTTP response - */ -@Catch(AppError) -export class AppErrorToHttpResponseFilter implements ExceptionFilter { - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - /** - * @note implementation adapted from: https://docs.nestjs.com/exception-filters#catch-everything - */ - catch({ code, message }: AppError, host: ArgumentsHost): void { - // In certain situations `httpAdapter` might not be available in the - // constructor method, thus we should resolve it here. - const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); - - const statusCode = this.appErrorCodeToHttpStatusCode(code); - if (statusCode >= 500) { - message = 'Internal Server Error'; - } - - const responseBody = { - statusCode, - message, - }; - - httpAdapter.reply(ctx.getResponse(), responseBody, statusCode); - } - - private appErrorCodeToHttpStatusCode(code: AppErrorCode): HttpStatus { - switch (code) { - case AppErrorCode.NotFound: - return HttpStatus.NOT_FOUND; - case AppErrorCode.Conflict: - return HttpStatus.CONFLICT; - case AppErrorCode.Config: - case AppErrorCode.Internal: - return HttpStatus.INTERNAL_SERVER_ERROR; - case AppErrorCode.Validation: - return HttpStatus.BAD_REQUEST; - case AppErrorCode.Unauthorized: - return HttpStatus.UNAUTHORIZED; - case AppErrorCode.Unprocessable: - return HttpStatus.UNPROCESSABLE_ENTITY; - default: - throw new UnreachableCaseError(code); - } - } -} diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts deleted file mode 100644 index 5f21def5..00000000 --- a/src/common/interceptors/logging.interceptor.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - CallHandler, - ExecutionContext, - HttpException, - HttpStatus, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@Injectable() -export class LoggingInterceptor implements NestInterceptor { - private readonly ctxPrefix = LoggingInterceptor.name; - - constructor(private readonly logger: PolymeshLogger) {} - - /** - * Intercept method, logs before and after the request being processed - * @param context details about the current request - * @param next implements the handle method that returns an Observable - */ - public intercept(context: ExecutionContext, next: CallHandler): Observable { - const req: Request = context.switchToHttp().getRequest(); - const { method, url, body } = req; - - const message = `Request: ${method} ${url}`; - - // TODO @prashantasdeveloper Log header values once API authentication is in place - this.logger.log( - { - message, - method, - url, - body, - }, - this.getLogContext(method, url) - ); - - return next.handle().pipe( - tap({ - next: (): void => { - this.logNext(context); - }, - error: (err: Error): void => { - this.logError(err, context); - }, - }) - ); - } - - /** - * Logs the request response in success cases - * - * @param context details about the current request - */ - private logNext(context: ExecutionContext): void { - const { method, url } = context.switchToHttp().getRequest(); - const { statusCode } = context.switchToHttp().getResponse(); - - const ctx = this.getLogContext(method, url); - const message = `Response: ${statusCode}`; - - this.logger.log({ message, method, url }, ctx); - } - - /** - * Logs the request response in success cases - * - * @param error Error object - * @param context details about the current request - */ - private logError(error: Error, context: ExecutionContext): void { - const { method, url, body } = context.switchToHttp().getRequest(); - const ctx = this.getLogContext(method, url); - - if (error instanceof HttpException) { - const statusCode = error.getStatus(); - const message = `Response: ${statusCode}`; - - if (statusCode >= HttpStatus.INTERNAL_SERVER_ERROR) { - this.logger.error( - { - message, - method, - url, - body, - statusCode, - error, - }, - error.stack, - ctx - ); - } else { - this.logger.warn( - { - message, - method, - url, - body, - statusCode, - error, - }, - ctx - ); - } - } else { - this.logger.error( - { - message: 'Error occurred', - }, - error.stack, - ctx - ); - } - } - - /** - * Get log context to be appended in logs - * - * @param method Method of the request - * @param url Request url - * @returns Formatted context template - */ - getLogContext(method: string, url: string): string { - return `${this.ctxPrefix} ${method} ${url}`; - } -} diff --git a/src/common/interceptors/webhook-response-code.interceptor.spec.ts b/src/common/interceptors/webhook-response-code.interceptor.spec.ts deleted file mode 100644 index a347e819..00000000 --- a/src/common/interceptors/webhook-response-code.interceptor.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ExecutionContext } from '@nestjs/common'; - -import { WebhookResponseCodeInterceptor } from '~/common/interceptors/webhook-response-code.interceptor'; - -const interceptor = new WebhookResponseCodeInterceptor(); - -const makeMockExecutionContext = ( - body: Record, - mockResponse: Record -): ExecutionContext => - ({ - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - body, - }), - getResponse: jest.fn().mockReturnValue(mockResponse), - }), - } as unknown as ExecutionContext); - -describe('webHookResponseCodeInterceptor', () => { - it('should be defined', () => { - expect(interceptor).toBeDefined(); - }); - - it('when a response is for a webhook the status code should be 202', () => { - const mockResponse = { statusCode: 201 }; - const executionContext = makeMockExecutionContext( - { webhookUrl: 'http://example.com' }, - mockResponse - ); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext, next); - - expect(mockResponse.statusCode).toEqual(202); - expect(next.handle).toHaveBeenCalled(); - }); - - it('should not alter a non 201 response', () => { - const mockResponse = { statusCode: 400 }; - const executionContext = makeMockExecutionContext( - { webhookUrl: 'http://example.com' }, - mockResponse - ); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext as unknown as ExecutionContext, next); - - expect(mockResponse.statusCode).toEqual(400); - expect(next.handle).toHaveBeenCalled(); - }); - - it('when a response is a non-webhook the status code should be unaltered', () => { - const mockResponse = { statusCode: 201 }; - const executionContext = makeMockExecutionContext({}, mockResponse); - - const next = { handle: jest.fn() }; - interceptor.intercept(executionContext as unknown as ExecutionContext, next); - - expect(mockResponse.statusCode).toEqual(201); - expect(next.handle).toHaveBeenCalled(); - }); -}); diff --git a/src/common/interceptors/webhook-response-code.interceptor.ts b/src/common/interceptors/webhook-response-code.interceptor.ts deleted file mode 100644 index 546840ce..00000000 --- a/src/common/interceptors/webhook-response-code.interceptor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { Observable } from 'rxjs'; - -@Injectable() -export class WebhookResponseCodeInterceptor implements NestInterceptor { - /** - * Intercept method, checks the response, and overrides the response code from 201 (Created) to 202 (Accepted) if the response is for a webhook - * @param context details about the current request - * @param next implements the handle method that returns an Observable - */ - public intercept(context: ExecutionContext, next: CallHandler): Observable { - const httpCtx = context.switchToHttp(); - const req: Request = httpCtx.getRequest(); - const res: Response = httpCtx.getResponse(); - - if (res.statusCode === 201 && req.body.webhookUrl) { - res.statusCode = 202; - } - - return next.handle(); - } -} diff --git a/src/common/models/batch-transaction.model.ts b/src/common/models/batch-transaction.model.ts deleted file mode 100644 index 8ce8a0cd..00000000 --- a/src/common/models/batch-transaction.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -export class BatchTransactionModel extends TransactionIdentifierModel { - @ApiProperty({ - description: - 'List of Transaction type identifier (for UI purposes). The format for each identifier is .', - type: 'string', - isArray: true, - example: 'asset.registerTicker', - }) - readonly transactionTags?: string[]; - - declare readonly type: TransactionType.Batch; - - constructor(model: Omit) { - const { transactionTags, ...rest } = model; - super({ ...rest, type: TransactionType.Batch }); - this.transactionTags = transactionTags; - } -} diff --git a/src/common/models/event-identifier.model.ts b/src/common/models/event-identifier.model.ts deleted file mode 100644 index 960092cd..00000000 --- a/src/common/models/event-identifier.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class EventIdentifierModel { - @ApiProperty({ - description: 'Number of the block where the event resides', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Hash of the block where the event resides', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Date when the block was finalized', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly blockDate: Date; - - @ApiProperty({ - description: 'Index of the event in the block', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly eventIndex: BigNumber; - - constructor(model: EventIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/extrinsic.model.ts b/src/common/models/extrinsic.model.ts deleted file mode 100644 index ac5082bf..00000000 --- a/src/common/models/extrinsic.model.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ExtrinsicData, TxTag, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { getTxTags } from '~/common/utils'; - -export class ExtrinsicModel { - @ApiProperty({ - description: 'Hash of the block where the transaction resides', - type: 'string', - example: '0x9d05973b0bacdbf26b705358fbcb7085354b1b7836ee1cc54e824810479dccf6', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Number of the block where the transaction resides', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Index of the transaction in the block', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly extrinsicIdx: BigNumber; - - @ApiProperty({ - description: - 'Public key of the signer. Unsigned transactions have no signer, in which case this value is null (example: an enacted governance proposal)', - type: 'string', - nullable: true, - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string | null; - - @ApiProperty({ - description: 'Nonce of the transaction. Null for unsigned transactions where address is null', - type: 'string', - nullable: true, - example: '123456', - }) - @FromBigNumber() - readonly nonce: BigNumber | null; - - @ApiProperty({ - description: - 'Transaction type identifier (for UI purposes). The format is .', - type: 'string', - enum: getTxTags(), - example: TxTags.asset.RegisterTicker, - }) - readonly transactionTag: TxTag; - - @ApiProperty({ - description: 'List of parameters associated with the transaction', - isArray: true, - example: [ - { - name: 'ticker', - value: 'TICKER', - }, - ], - }) - readonly params: Record[]; - - @ApiProperty({ - description: 'Indicates whether the transaction was successful or not', - type: 'boolean', - example: true, - }) - readonly success: boolean; - - @ApiProperty({ - description: 'Spec version of the chain', - type: 'string', - example: '3002', - }) - @FromBigNumber() - readonly specVersionId: BigNumber; - - @ApiProperty({ - description: 'Hash of the transaction', - type: 'string', - example: '44b8a09e9647b34d81d9eb40f26c5bb35ea216610a03df71978558ec939d5120', - }) - readonly extrinsicHash: string; - - constructor(data: ExtrinsicData) { - const { txTag: transactionTag, ...rest } = data; - Object.assign(this, { ...rest, transactionTag }); - } -} diff --git a/src/common/models/fees.model.ts b/src/common/models/fees.model.ts deleted file mode 100644 index 56179bbd..00000000 --- a/src/common/models/fees.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class FeesModel { - @ApiProperty({ - type: 'string', - description: 'The amount of POLYX that will be charged for the transaction as protocol fee', - example: '0.5', - }) - @FromBigNumber() - readonly protocol: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'The amount of POLYX that will be charged for the transaction as GAS fee', - example: '0.5', - }) - @FromBigNumber() - readonly gas: BigNumber; - - @ApiProperty({ - type: 'string', - description: 'The total amount of POLYX that will be charged for the transaction', - example: '1', - }) - @FromBigNumber() - readonly total: BigNumber; - - constructor(model: FeesModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/notification-payload-model.ts b/src/common/models/notification-payload-model.ts deleted file mode 100644 index 894f9e43..00000000 --- a/src/common/models/notification-payload-model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { EventType, TransactionUpdatePayload } from '~/events/types'; - -export class NotificationPayloadModel { - @ApiProperty({ - description: - 'The ID of the subscription. Events related to the transaction will contain this ID in the payload', - example: 1, - }) - readonly subscriptionId: number; - - @ApiProperty({ - description: 'The nonce for the subscription', - example: 0, - }) - readonly nonce: number; - - @ApiProperty({ - description: 'The type of event', - enum: EventType, - }) - readonly type: EventType; - - @ApiProperty({ - description: 'The payload of the transaction subscribed too', - }) - readonly payload: TransactionUpdatePayload; - - constructor(model: NotificationPayloadModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/paginated-results.model.ts b/src/common/models/paginated-results.model.ts deleted file mode 100644 index f1ebcb85..00000000 --- a/src/common/models/paginated-results.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { ResultsModel } from '~/common/models/results.model'; - -export class PaginatedResultsModel extends ResultsModel { - @ApiProperty({ - type: 'string', - description: 'Total number of results possible for paginated output', - example: '10', - }) - @FromBigNumber() - readonly total?: BigNumber; - - @ApiProperty({ - type: 'string', - description: - 'Offset start value for the next set of paginated data (null means there is no more data to fetch)', - nullable: true, - }) - @FromBigNumber() - readonly next: string | BigNumber | null; - - constructor(model: PaginatedResultsModel) { - const { results, ...rest } = model; - super({ results }); - - Object.assign(this, rest); - } -} diff --git a/src/common/models/paying-account.model.ts b/src/common/models/paying-account.model.ts deleted file mode 100644 index 36b41428..00000000 --- a/src/common/models/paying-account.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { PayingAccountType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class PayingAccountModel { - @ApiProperty({ - type: 'string', - description: 'The balance of the paying account', - example: '29996999.366176', - }) - @FromBigNumber() - readonly balance: BigNumber; - - @ApiProperty({ - description: 'Paying account type', - enum: PayingAccountType, - example: PayingAccountType.Caller, - }) - readonly type: string; - - @ApiProperty({ - type: 'string', - description: 'The paying account address', - example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - }) - readonly address: string; - - constructor(model: PayingAccountModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/results.model.ts b/src/common/models/results.model.ts deleted file mode 100644 index 888f10a5..00000000 --- a/src/common/models/results.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class ResultsModel { - @ApiProperty({ type: 'generic array' }) - @FromEntityObject() - readonly results: DataType[]; - - constructor(model: ResultsModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-details.model.ts b/src/common/models/transaction-details.model.ts deleted file mode 100644 index c1709a59..00000000 --- a/src/common/models/transaction-details.model.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FeesModel } from '~/common/models/fees.model'; -import { PayingAccountModel } from '~/common/models/paying-account.model'; - -export class TransactionDetailsModel { - @ApiProperty({ - description: 'Transaction status', - enum: TransactionStatus, - example: TransactionStatus.Idle, - }) - readonly status: string; - - @ApiProperty({ description: 'Transaction fees', type: FeesModel }) - @Type(() => FeesModel) - readonly fees: FeesModel; - - @ApiProperty({ - type: 'boolean', - example: true, - description: 'Indicates if the transaction can be subsidized', - }) - readonly supportsSubsidy: boolean; - - @ApiProperty({ - description: 'Paying account details', - type: PayingAccountModel, - }) - @Type(() => PayingAccountModel) - readonly payingAccount: PayingAccountModel; - - constructor(model: TransactionDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-identifier.model.ts b/src/common/models/transaction-identifier.model.ts deleted file mode 100644 index 371451c6..00000000 --- a/src/common/models/transaction-identifier.model.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { TransactionType } from '~/common/types'; - -export class TransactionIdentifierModel { - @ApiProperty({ - description: - 'Number of the block where the transaction resides (status: `Succeeded`, `Failed`)', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Hash of the block', - type: 'string', - example: '0x0372a35b1ae2f622142aa8519ce70b0980fb35727fd0348d204dfa280f2f5987', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Hash of the transaction', - type: 'string', - example: '0xe0346b494edcca5a30b12f3ef128e54dfce412dbf5a0202b3e69c926267d1473', - }) - readonly transactionHash: string; - - @ApiProperty({ - description: 'Indicator to know if the transaction is a batch transaction or not', - enum: TransactionType, - type: 'string', - example: TransactionType.Single, - }) - readonly type: TransactionType; - - constructor(model: TransactionIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction-queue.model.ts b/src/common/models/transaction-queue.model.ts deleted file mode 100644 index abf3a6c3..00000000 --- a/src/common/models/transaction-queue.model.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { TransactionDetailsModel } from '~/common/models/transaction-details.model'; -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -@ApiExtraModels(TransactionModel, BatchTransactionModel) -export class TransactionQueueModel { - @ApiPropertyOneOf({ - description: 'List of transactions', - isArray: true, - union: [TransactionModel, BatchTransactionModel], - }) - @Type(() => TransactionIdentifierModel, { - keepDiscriminatorProperty: true, - discriminator: { - property: 'type', - subTypes: [ - { - value: TransactionModel, - name: TransactionType.Single, - }, - { - value: BatchTransactionModel, - name: TransactionType.Batch, - }, - ], - }, - }) - transactions: (TransactionModel | BatchTransactionModel)[]; - - @ApiProperty({ - description: 'Transaction details', - isArray: true, - }) - @Type(() => TransactionDetailsModel) - details: TransactionDetailsModel; - - constructor(model: TransactionQueueModel) { - Object.assign(this, model); - } -} diff --git a/src/common/models/transaction.model.ts b/src/common/models/transaction.model.ts deleted file mode 100644 index 15ad0cf3..00000000 --- a/src/common/models/transaction.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { TransactionIdentifierModel } from '~/common/models/transaction-identifier.model'; -import { TransactionType } from '~/common/types'; - -export class TransactionModel extends TransactionIdentifierModel { - @ApiProperty({ - description: - 'Transaction type identifier (for UI purposes). The format is .', - type: 'string', - example: 'asset.registerTicker', - }) - readonly transactionTag: string; - - declare readonly type: TransactionType.Single; - - constructor(model: Omit) { - const { transactionTag, ...rest } = model; - super({ ...rest, type: TransactionType.Single }); - this.transactionTag = transactionTag; - } -} diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index a845c9f9..00000000 --- a/src/common/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -export interface Entity { - uuid: string; - - toHuman(): Serialized; -} - -// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -export type Class = new (...args: any[]) => T; - -export enum TransactionType { - Single = 'single', - Batch = 'batch', -} - -export enum CalendarUnit { - Second = 'Second', - Minute = 'Minute', - Hour = 'Hour', - Day = 'Day', - Week = 'Week', - Month = 'Month', - Year = 'Year', -} diff --git a/src/common/utils/consts.ts b/src/common/utils/consts.ts deleted file mode 100644 index f25de6f0..00000000 --- a/src/common/utils/consts.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const swaggerTitle = 'Polymesh REST API'; - -export const swaggerDescription = [ - 'The Polymesh REST API provides a developer friendly interface with the Polymesh blockchain', - 'Polymesh is an institutional-grade permissioned blockchain built specifically for regulated assets.', - '', - 'The API allows you to perform various operations such as:', - '', - '- Query and manage identity data', - '- Interact with corporate actions (e.g., dividends, capital distributions, and other events)', - '- Create and manage security tokens', - '- Manage asset ownership and transfers', - '', - 'With this API developers can build applications and integrations for the Polymesh chain in the programming language of their choice', -].join('\n'); diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts deleted file mode 100644 index 522bbdf6..00000000 --- a/src/common/utils/functions.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - FungibleLeg, - Leg, - ModuleName, - NftLeg, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { randomBytes } from 'crypto'; -import { flatten } from 'lodash'; -import { promisify } from 'util'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { NotificationPayloadModel } from '~/common/models/notification-payload-model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { EventType } from '~/events/types'; -import { NotificationPayload } from '~/notifications/types'; -import { TransactionResult } from '~/transactions/transactions.util'; - -/* istanbul ignore next */ -export function getTxTags(): string[] { - return flatten(Object.values(TxTags).map(txTag => Object.values(txTag))); -} - -/* istanbul ignore next */ -export function getTxTagsWithModuleNames(): string[] { - const txTags = getTxTags(); - const moduleNames = Object.values(ModuleName); - return [...moduleNames, ...txTags]; -} - -export type TransactionResponseModel = NotificationPayloadModel | TransactionQueueModel; - -/** - * A helper type that lets a service return a QueueResult or a Subscription Receipt - */ -export type ServiceReturn = Promise< - NotificationPayload | TransactionResult ->; - -/** - * A helper type that lets a controller return a Model or a Subscription Receipt if webhookUrl is being used - */ -export type TransactionResolver = ( - res: TransactionResult -) => Promise | TransactionQueueModel; - -/** - * A helper function that transforms a service result for a controller. A controller can pass a resolver for a detailed return model, otherwise the transaction details will be used as a default - */ -export const handleServiceResult = ( - result: NotificationPayloadModel | TransactionResult, - resolver: TransactionResolver = basicModelResolver -): NotificationPayloadModel | Promise | TransactionQueueModel => { - if ('transactions' in result) { - return resolver(result); - } - - return new NotificationPayloadModel(result); -}; - -/** - * A helper function for controllers when they should return a basic TransactionQueueModel - */ -const basicModelResolver: TransactionResolver = ({ transactions, details }) => { - return new TransactionQueueModel({ transactions, details }); -}; - -/** - * Generate base64 encoded, cryptographically random bytes - * - * @note random byte length given, not the encoded string length - */ -export const generateBase64Secret = async (byteLength: number): Promise => { - const buf = await promisify(randomBytes)(byteLength); - - return buf.toString('base64'); -}; - -/** - * Helper class to ensure a code path is unreachable. For example this can be used for ensuring switch statements are exhaustive - */ -export class UnreachableCaseError extends Error { - /** This should never be called */ - constructor(val: never) { - super(`Unreachable case: ${JSON.stringify(val)}`); - } -} - -export const extractTxBase = ( - params: T -): { - base: TransactionBaseDto; - args: Omit; -} => { - const { signer, webhookUrl, dryRun, ...args } = params; - return { - base: { signer, webhookUrl, dryRun }, - args, - }; -}; - -export const isNotNull = (item: T | null): item is T => item !== null; - -export function isFungibleLeg(leg: Leg): leg is FungibleLeg { - return 'amount' in leg; -} - -export function isNftLeg(leg: Leg): leg is NftLeg { - return 'nfts' in leg; -} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts deleted file mode 100644 index d67600e3..00000000 --- a/src/common/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '~/common/utils/consts'; -export * from '~/common/utils/functions'; diff --git a/src/compliance/compliance-requirements.controller.spec.ts b/src/compliance/compliance-requirements.controller.spec.ts deleted file mode 100644 index 695ab8fb..00000000 --- a/src/compliance/compliance-requirements.controller.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { when } from 'jest-when'; - -import { ComplianceRequirementsController } from '~/compliance/compliance-requirements.controller'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { mockComplianceRequirements } from '~/compliance/mocks/compliance-requirements.mock'; -import { ComplianceRequirementsModel } from '~/compliance/models/compliance-requirements.model'; -import { ComplianceStatusModel } from '~/compliance/models/compliance-status.model'; -import { testValues } from '~/test-utils/consts'; -import { createMockTransactionResult } from '~/test-utils/mocks'; -import { mockComplianceRequirementsServiceProvider } from '~/test-utils/service-mocks'; - -describe('ComplianceRequirementsController', () => { - let controller: ComplianceRequirementsController; - let mockService: ComplianceRequirementsService; - const { did, signer, txResult } = testValues; - - const ticker = 'TICKER'; - const validBody = { - signer, - requirements: [ - [ - { - target: 'Sender', - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ], - ], - }; - const txResponse = createMockTransactionResult({ ...txResult, transactions: [] }); - const id = new BigNumber(1); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ComplianceRequirementsController], - providers: [mockComplianceRequirementsServiceProvider], - }).compile(); - - mockService = - mockComplianceRequirementsServiceProvider.useValue as DeepMocked; - controller = module.get(ComplianceRequirementsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getComplianceRequirements', () => { - it('should return the list of all compliance requirements of an Asset', async () => { - when(mockService.findComplianceRequirements) - .calledWith(ticker) - .mockResolvedValue(mockComplianceRequirements); - - const result = await controller.getComplianceRequirements({ ticker }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(result).toEqual(new ComplianceRequirementsModel(mockComplianceRequirements as any)); - }); - }); - - describe('setRequirements', () => { - it('should accept SetRulesDto and set new Asset Compliance Rules', async () => { - const response = createMockTransactionResult({ ...txResult, transactions: [] }); - - when(mockService.setRequirements) - .calledWith(ticker, validBody as SetRequirementsDto) - .mockResolvedValue(response); - - const result = await controller.setRequirements({ ticker }, validBody as SetRequirementsDto); - expect(result).toEqual(response); - }); - }); - - describe('pauseRequirements', () => { - it('should accept TransactionBaseDto and pause Asset Compliance Rules', async () => { - when(mockService.pauseRequirements) - .calledWith(ticker, validBody) - .mockResolvedValue(txResponse); - - const result = await controller.pauseRequirements({ ticker }, validBody); - expect(result).toEqual(txResponse); - }); - }); - - describe('unpauseRequirements', () => { - it('should accept TransactionBaseDto and unpause Asset Compliance Rules', async () => { - when(mockService.unpauseRequirements) - .calledWith(ticker, validBody) - .mockResolvedValue(txResponse); - - const result = await controller.pauseRequirements({ ticker }, validBody); - expect(result).toEqual(txResponse); - }); - }); - - describe('deleteRequirement', () => { - it('should accept TransactionBaseDto and compliance requirement ID and delete the corresponding Asset Compliance rule for the given ticker', async () => { - when(mockService.deleteOne).calledWith(ticker, id, validBody).mockResolvedValue(txResponse); - - const result = await controller.deleteRequirement({ ticker, id }, validBody); - - expect(result).toEqual(txResponse); - }); - }); - - describe('deleteRequirements', () => { - it('should accept TransactionBaseDto and delete all the Asset Compliance rules for the given ticker', async () => { - when(mockService.deleteAll).calledWith(ticker, validBody).mockResolvedValue(txResponse); - - const result = await controller.deleteRequirements({ ticker }, validBody); - - expect(result).toEqual(txResponse); - }); - }); - - describe('addRequirement', () => { - it('should accept RequirementDto and add an Asset Compliance rule', async () => { - const { requirements } = validBody; - - when(mockService.add) - .calledWith(ticker, { - signer, - conditions: requirements[0], - } as RequirementDto) - .mockResolvedValue(txResponse); - - const result = await controller.addRequirement({ ticker }, { - signer, - conditions: requirements[0], - } as RequirementDto); - expect(result).toEqual(txResponse); - }); - }); - - describe('modifyComplianceRequirement', () => { - it('should accept RequirementDto and modify the corresponding Asset Compliance rule', async () => { - const response = createMockTransactionResult({ ...txResult, transactions: [] }); - const { requirements } = validBody; - - when(mockService.modify) - .calledWith(ticker, id, { - signer, - conditions: requirements[0], - } as RequirementDto) - .mockResolvedValue(response); - - const result = await controller.modifyComplianceRequirement({ ticker, id }, { - signer, - conditions: requirements[0], - } as RequirementDto); - - expect(result).toEqual(response); - }); - }); - - describe('areRequirementsPaused', () => { - it('should return the result of arePaused method', async () => { - const response = false; - - when(mockService.arePaused).calledWith(ticker).mockResolvedValue(response); - - const result = await controller.areRequirementsPaused({ ticker }); - - expect(result).toEqual(new ComplianceStatusModel({ arePaused: response })); - }); - }); -}); diff --git a/src/compliance/compliance-requirements.controller.ts b/src/compliance/compliance-requirements.controller.ts deleted file mode 100644 index cc20429d..00000000 --- a/src/compliance/compliance-requirements.controller.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { RequirementParamsDto } from '~/compliance/dto/requirement-params.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { ComplianceRequirementsModel } from '~/compliance/models/compliance-requirements.model'; -import { ComplianceStatusModel } from '~/compliance/models/compliance-status.model'; -import { RequirementModel } from '~/compliance/models/requirement.model'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; - -@ApiTags('assets', 'compliance') -@Controller('assets/:ticker/compliance-requirements') -export class ComplianceRequirementsController { - constructor(private readonly complianceRequirementsService: ComplianceRequirementsService) {} - - @ApiOperation({ - summary: 'Fetch Compliance Requirements of an Asset', - description: - 'This endpoint will provide the list of all compliance requirements of an Asset along with Default Trusted Claim Issuers', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Compliance Requirements are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: - 'List of Compliance Requirements of the Asset along with Default Trusted Claim Issuers', - type: ComplianceRequirementsModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset was not found', - }) - @Get() - public async getComplianceRequirements( - @Param() { ticker }: TickerParamsDto - ): Promise { - const { requirements, defaultTrustedClaimIssuers } = - await this.complianceRequirementsService.findComplianceRequirements(ticker); - - return new ComplianceRequirementsModel({ - requirements: requirements.map( - ({ id, conditions }) => new RequirementModel({ id, conditions }) - ), - defaultTrustedClaimIssuers: defaultTrustedClaimIssuers.map( - ({ identity: { did }, trustedFor }) => new TrustedClaimIssuerModel({ did, trustedFor }) - ), - }); - } - - @ApiOperation({ - summary: 'Set Compliance requirements for an Asset', - description: - 'This endpoint sets Compliance rules for an Asset. This method will replace the current rules', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be set', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset was not found', - }) - @Post('set') - public async setRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetRequirementsDto - ): Promise { - const result = await this.complianceRequirementsService.setRequirements(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Pause compliance requirements for an Asset', - description: 'This endpoint pauses compliance rules for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be paused', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to perform transaction'], - }) - @Post('pause') - public async pauseRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.pauseRequirements( - ticker, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Unpause compliance requirements for an Asset', - description: 'This endpoint unpauses compliance rules for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be unpaused', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - }) - @Post('unpause') - public async unpauseRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.unpauseRequirements( - ticker, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Delete single compliance requirement for an Asset', - description: 'This endpoint deletes referenced compliance requirement for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset from whose compliance requirement is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the compliance requirement to be deleted', - type: 'string', - example: '123', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to perform transaction'], - }) - @Post(':id/delete') - public async deleteRequirement( - @Param() { id, ticker }: RequirementParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.deleteOne( - ticker, - id, - transactionBaseDto - ); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Delete all compliance requirements for an Asset', - description: 'This endpoint deletes all compliance requirements for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements are to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: [ - 'Returned if there are no existing compliance requirements for the Asset', - ], - }) - @Post('delete') - public async deleteRequirements( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.complianceRequirementsService.deleteAll(ticker, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Add a new compliance requirement for an Asset', - description: - "This endpoint adds a new compliance requirement to the specified Asset. This doesn't modify the existing requirements", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to which the compliance requirement is to be added', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: ['Returned if the transaction failed'], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Compliance Requirement complexity limit exceeded'], - }) - @Post('add') - public async addRequirement( - @Param() { ticker }: TickerParamsDto, - @Body() params: RequirementDto - ): Promise { - const result = await this.complianceRequirementsService.add(ticker, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify single compliance requirement for an Asset', - description: 'This endpoint modifies referenced compliance requirement for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the compliance requirement is to be modified', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The id of the compliance requirement to be modified', - type: 'string', - example: '123', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset or compliance requirement was not found'], - [HttpStatus.BAD_REQUEST]: ['Returned if there is no change in data'], - }) - @Post(':id/modify') - public async modifyComplianceRequirement( - @Param() { id, ticker }: RequirementParamsDto, - @Body() params: RequirementDto - ): Promise { - const result = await this.complianceRequirementsService.modify(ticker, id, params); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Check if the requirements are paused', - description: - 'This endpoint checks if the compliance requirements are paused for a given ticker', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose compliance requirements status are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Compliance Requirement status', - type: ComplianceStatusModel, - }) - @ApiNotFoundResponse({ - description: 'The Asset does not exist', - }) - @Get('status') - public async areRequirementsPaused( - @Param() { ticker }: TickerParamsDto - ): Promise { - const arePaused = await this.complianceRequirementsService.arePaused(ticker); - - return new ComplianceStatusModel({ arePaused }); - } -} diff --git a/src/compliance/compliance-requirements.service.spec.ts b/src/compliance/compliance-requirements.service.spec.ts deleted file mode 100644 index 3a79e5ea..00000000 --- a/src/compliance/compliance-requirements.service.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { MockComplianceRequirements } from '~/compliance/mocks/compliance-requirements.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockTransaction } from '~/test-utils/mocks'; -import { MockAssetService, mockTransactionsProvider } from '~/test-utils/service-mocks'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('ComplianceRequirementsService', () => { - let service: ComplianceRequirementsService; - const mockAssetsService = new MockAssetService(); - const mockTransactionsService = mockTransactionsProvider.useValue; - const { signer } = testValues; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AssetsService, ComplianceRequirementsService, mockTransactionsProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(ComplianceRequirementsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findComplianceRequirements', () => { - it('should return the list of Asset compliance requirements', async () => { - const mockRequirements = new MockComplianceRequirements(); - - const mockAsset = new MockAsset(); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - mockAsset.compliance.requirements.get.mockResolvedValue(mockRequirements); - - const result = await service.findComplianceRequirements('TICKER'); - - expect(result).toEqual(mockRequirements); - }); - }); - - describe('setRequirements', () => { - it('should run a set rules procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.AddComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { requirements: [], signer, asSetAssetRequirementsParams: jest.fn() }; - - const result = await service.setRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('pauseRequirements', () => { - it('should run a pause requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.PauseAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.pauseRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('unpauseRequirements', () => { - it('should run a unpause requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ResumeAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.unpauseRequirements('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('deleteRequirement', () => { - it('should run the delete Requirement procedure and return the queue data', async () => { - const requirementId = new BigNumber(1); - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.RemoveComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.deleteOne('TICKER', requirementId, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('deleteRequirements', () => { - it('should run the delete all Requirements procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ResetAssetCompliance, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { signer }; - - const result = await service.deleteAll('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('addRequirement', () => { - it('should run the add Requirement procedure and return the queue data', async () => { - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.AddComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { conditions: [], signer }; - - const result = await service.add('TICKER', body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('editRequirement', () => { - it('should run the modify Requirements procedure and return the queue data', async () => { - const requirementId = new BigNumber(1); - const mockAsset = new MockAsset(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.complianceManager.ChangeComplianceRequirement, - }; - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const body = { conditions: [], signer }; - - const result = await service.modify('TICKER', requirementId, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('arePaused', () => { - it('should return the Asset compliance requirement state', async () => { - const mockAsset = new MockAsset(); - const arePaused = true; - mockAsset.compliance.requirements.arePaused.mockReturnValue(arePaused); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.arePaused('TICKER'); - - expect(result).toEqual(arePaused); - }); - }); -}); diff --git a/src/compliance/compliance-requirements.service.ts b/src/compliance/compliance-requirements.service.ts deleted file mode 100644 index a85e4c32..00000000 --- a/src/compliance/compliance-requirements.service.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AddAssetRequirementParams, - ComplianceRequirements, - ModifyComplianceRequirementParams, - SetAssetRequirementsParams, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { RequirementDto } from '~/compliance/dto/requirement.dto'; -import { SetRequirementsDto } from '~/compliance/dto/set-requirements.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class ComplianceRequirementsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService - ) {} - - public async findComplianceRequirements(ticker: string): Promise { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.requirements.get(); - } - - public async setRequirements(ticker: string, params: SetRequirementsDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.set, - args as SetAssetRequirementsParams, - base - ); - } - - public async pauseRequirements( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findOne(ticker); - return this.transactionsService.submit( - asset.compliance.requirements.pause, - {}, - transactionBaseDto - ); - } - - public async unpauseRequirements( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.unpause, - {}, - transactionBaseDto - ); - } - - public async deleteOne( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.remove, - { requirement: id }, - transactionBaseDto - ); - } - - public async deleteAll( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.reset, - undefined, - transactionBaseDto - ); - } - - public async add(ticker: string, params: RequirementDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.add, - args as AddAssetRequirementParams, - base - ); - } - - public async modify(ticker: string, id: BigNumber, params: RequirementDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit( - asset.compliance.requirements.modify, - { id, ...args } as ModifyComplianceRequirementParams, - base - ); - } - - public async arePaused(ticker: string): Promise { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.requirements.arePaused(); - } -} diff --git a/src/compliance/compliance.module.ts b/src/compliance/compliance.module.ts deleted file mode 100644 index bda672fa..00000000 --- a/src/compliance/compliance.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { ComplianceRequirementsController } from '~/compliance/compliance-requirements.controller'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersController } from '~/compliance/trusted-claim-issuers.controller'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [forwardRef(() => AssetsModule), IdentitiesModule, TransactionsModule], - providers: [ComplianceRequirementsService, TrustedClaimIssuersService], - exports: [ComplianceRequirementsService, TrustedClaimIssuersService], - controllers: [ComplianceRequirementsController, TrustedClaimIssuersController], -}) -export class ComplianceModule {} diff --git a/src/compliance/dto/condition.dto.spec.ts b/src/compliance/dto/condition.dto.spec.ts deleted file mode 100644 index 914eb1d2..00000000 --- a/src/compliance/dto/condition.dto.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { - ClaimType, - ConditionTarget, - ConditionType, - ScopeType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const address = '0x0600000000000000000000000000000000000000000000000000000000000000'; -const validClaim: ClaimDto = { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: address, - }, -}; -const invalidClaim: ClaimDto = { - type: ClaimType.Accredited, -}; - -describe('conditionDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: ConditionDto, - data: '', - }; - describe('valid ConditionDtos', () => { - const cases: ValidCase[] = [ - [ - 'IsPresent', - { type: ConditionType.IsPresent, target: ConditionTarget.Both, claim: validClaim }, - ], - [ - 'IsNone', - { type: ConditionType.IsAbsent, target: ConditionTarget.Receiver, claim: validClaim }, - ], - [ - 'IsAnyOf', - { type: ConditionType.IsAnyOf, target: ConditionTarget.Receiver, claims: [validClaim] }, - ], - [ - 'IsNoneOf', - { type: ConditionType.IsNoneOf, target: ConditionTarget.Sender, claims: [validClaim] }, - ], - [ - 'IsIdentity', - { - type: ConditionType.IsIdentity, - target: ConditionTarget.Sender, - identity: address, - }, - ], - [ - 'IsPresent with trustedClaimIssuers', - { - type: ConditionType.IsPresent, - target: ConditionTarget.Both, - claim: validClaim, - trustedClaimIssuers: [ - { - identity: address, - }, - ], - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not get any errors. Received: ${err.getResponse().message}`); - }); - }); - }); - - describe('invalid ConditionDtos', () => { - const cases: InvalidCase[] = [ - [ - 'IsPresent without `target`', - { type: ConditionType.IsPresent, claim: validClaim }, - ['target must be one of the following values: Sender, Receiver, Both'], - ], - [ - 'IsPresent without `claim`', - { type: ConditionType.IsPresent, target: ConditionTarget.Both }, - ['claim must be a non-empty object'], - ], - [ - 'IsAnyOf without `claims`', - { type: ConditionType.IsAnyOf, target: ConditionTarget.Receiver }, - ['claims should not be empty'], - ], - [ - 'IsNoneOf with an invalid claim in `claims`', - { type: ConditionType.IsNoneOf, target: ConditionTarget.Both, claims: [invalidClaim] }, - ['claims.0.scope must be a non-empty object'], - ], - [ - 'IsIdentity without `identity`', - { type: ConditionType.IsIdentity, target: ConditionTarget.Receiver }, - [ - 'DID must be a hexadecimal number', - 'DID must start with "0x"', - 'DID must be 66 characters long', - ], - ], - [ - 'IsPresent with invalid `identity` in `trustedClaimIssuers`', - { - type: ConditionType.IsPresent, - target: ConditionTarget.Both, - claim: validClaim, - trustedClaimIssuers: [ - { - identity: 123, - }, - ], - }, - [ - 'trustedClaimIssuers.0.DID must be a hexadecimal number', - 'trustedClaimIssuers.0.DID must start with "0x"', - 'trustedClaimIssuers.0.DID must be 66 characters long', - ], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - await target.transform(input, metadata).catch(err => { - expect(err.getResponse().message).toEqual(expected); - }); - }); - }); -}); diff --git a/src/compliance/dto/condition.dto.ts b/src/compliance/dto/condition.dto.ts deleted file mode 100644 index 06b2c7b5..00000000 --- a/src/compliance/dto/condition.dto.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ConditionTarget, ConditionType } from '@polymeshassociation/polymesh-sdk/types'; -import { - isMultiClaimCondition, - isSingleClaimCondition, -} from '@polymeshassociation/polymesh-sdk/utils'; -import { Type } from 'class-transformer'; -import { IsEnum, IsNotEmpty, IsNotEmptyObject, ValidateIf, ValidateNested } from 'class-validator'; - -import { ClaimDto } from '~/claims/dto/claim.dto'; -import { IsDid } from '~/common/decorators/validation'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class ConditionDto { - @ApiProperty({ - description: 'Whether the Condition applies to the sender, the receiver, or both', - enum: ConditionTarget, - example: ConditionTarget.Both, - }) - @IsEnum(ConditionTarget) - readonly target: ConditionTarget; - - @ApiProperty({ - description: - 'The type of Condition. "IsPresent" requires the target(s) to have a specific Claim. "IsAbsent" is the opposite. "IsAnyOf" requires the target(s) to have at least one of a list of Claims. "IsNoneOf" is the opposite. "IsIdentity" requires the target(s) to be a specific Identity', - enum: ConditionType, - example: ConditionType.IsNoneOf, - }) - @IsEnum(ConditionType) - readonly type: ConditionType; - - @ApiPropertyOptional({ - description: 'Optional Trusted Claim Issuer for this Condition. Defaults to all', - isArray: true, - type: TrustedClaimIssuerDto, - }) - @ValidateNested({ each: true }) - @Type(() => TrustedClaimIssuerDto) - readonly trustedClaimIssuers?: TrustedClaimIssuerDto[]; - - @ApiPropertyOptional({ - description: 'The Claim for "IsPresent" or "IsAbsent" Conditions', - type: ClaimDto, - }) - @ValidateIf(isSingleClaimCondition) - @ValidateNested() - @Type(() => ClaimDto) - @IsNotEmptyObject() - readonly claim?: ClaimDto; - - @ApiPropertyOptional({ - description: 'Claims for "IsAnyOf" or "IsNoneOf" Conditions', - isArray: true, - type: ClaimDto, - }) - @ValidateIf(isMultiClaimCondition) - @ValidateNested({ each: true }) - @IsNotEmpty() - @Type(() => ClaimDto) - readonly claims?: ClaimDto[]; - - @ApiPropertyOptional({ - description: 'The DID of the Identity for "IsIdentity" Conditions', - type: 'string', - }) - @ValidateIf(({ type }) => type === ConditionType.IsIdentity) - @IsDid() - readonly identity?: string; -} diff --git a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts b/src/compliance/dto/remove-trusted-claim-issuers.dto.ts deleted file mode 100644 index 75a32525..00000000 --- a/src/compliance/dto/remove-trusted-claim-issuers.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RemoveTrustedClaimIssuersDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The list of Claim issuer identities that should be removed', - isArray: true, - example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], - }) - @IsNotEmpty() - @IsDid({ each: true }) - readonly claimIssuers: string[]; -} diff --git a/src/compliance/dto/requirement-params.dto.ts b/src/compliance/dto/requirement-params.dto.ts deleted file mode 100644 index 80797773..00000000 --- a/src/compliance/dto/requirement-params.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class RequirementParamsDto extends TickerParamsDto { - @ApiProperty({ - description: 'Requirement ID', - type: 'string', - example: '1', - }) - @ToBigNumber() - @IsBigNumber() - readonly id: BigNumber; -} diff --git a/src/compliance/dto/requirement.dto.ts b/src/compliance/dto/requirement.dto.ts deleted file mode 100644 index 01f9256e..00000000 --- a/src/compliance/dto/requirement.dto.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; - -export class RequirementDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Asset transfers must comply with all of the rules in one of the top level elements. Essentially each outer array element has an *or* between them, while the inner elements have an *and* between them', - type: ConditionDto, - example: [ - { - target: 'Both', - type: 'IsNoneOf', - claims: [ - { - type: 'Blocked', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - { - type: 'Jurisdiction', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - code: CountryCode.Us, - }, - ], - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Blocked], - }, - ], - }, - ], - }) - @Type(() => ConditionDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - readonly conditions: ConditionDto[]; -} diff --git a/src/compliance/dto/set-requirements.dto.ts b/src/compliance/dto/set-requirements.dto.ts deleted file mode 100644 index e5ed2533..00000000 --- a/src/compliance/dto/set-requirements.dto.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType, CountryCode } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsArray, IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ConditionDto } from '~/compliance/dto/condition.dto'; - -export class SetRequirementsDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Asset transfers must comply with all of the rules in one of the top level elements. Essentially each outer array element has an *or* between them, while the inner elements have an *and* between them', - isArray: true, - type: ConditionDto, - example: [ - [ - { - target: 'Both', - type: 'IsNoneOf', - claims: [ - { - type: 'Blocked', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - { - type: 'Jurisdiction', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - code: CountryCode.Us, - }, - ], - trustedClaimIssuers: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Blocked], - }, - ], - }, - ], - [ - { - target: 'Sender', - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Ticker', - value: 'TICKER', - }, - }, - }, - ], - [ - { - target: 'Receiver', - type: 'IsIdentity', - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - ], - ], - }) - @Type(() => ConditionDto) - @IsNotEmpty() - @IsArray({ each: true }) - @ValidateNested({ each: true }) - readonly requirements: ConditionDto[][]; -} diff --git a/src/compliance/dto/set-trusted-claim-issuers.dto.ts b/src/compliance/dto/set-trusted-claim-issuers.dto.ts deleted file mode 100644 index 3a01314f..00000000 --- a/src/compliance/dto/set-trusted-claim-issuers.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TrustedClaimIssuerDto } from '~/compliance/dto/trusted-claim-issuer.dto'; - -export class SetTrustedClaimIssuersDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'The list of Claim Issuers that will be trusted to issue Claims of the specified type', - isArray: true, - type: TrustedClaimIssuerDto, - example: [ - { - identity: '0x0600000000000000000000000000000000000000000000000000000000000000', - trustedFor: [ClaimType.Accredited, ClaimType.KnowYourCustomer], - }, - ], - }) - @Type(() => TrustedClaimIssuerDto) - @IsNotEmpty() - @ValidateNested({ each: true }) - readonly claimIssuers: TrustedClaimIssuerDto[]; -} diff --git a/src/compliance/dto/trusted-claim-issuer.dto.ts b/src/compliance/dto/trusted-claim-issuer.dto.ts deleted file mode 100644 index a686bf5a..00000000 --- a/src/compliance/dto/trusted-claim-issuer.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class TrustedClaimIssuerDto { - @ApiPropertyOptional({ - description: - 'List of Claim types for which an Identity is trusted for verifying. Defaults to all types', - enum: ClaimType, - isArray: true, - nullable: true, - default: null, - }) - @IsOptional() - @IsEnum(ClaimType, { each: true }) - readonly trustedFor: ClaimType[] | null; - - @ApiPropertyOptional({ - description: 'The Identity of the Claim Issuer', - type: 'string', - }) - @IsOptional() - @IsDid() - readonly identity: string; -} diff --git a/src/compliance/mocks/compliance-requirements.mock.ts b/src/compliance/mocks/compliance-requirements.mock.ts deleted file mode 100644 index 0df60788..00000000 --- a/src/compliance/mocks/compliance-requirements.mock.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ClaimType, - ComplianceRequirements, - ConditionTarget, - ConditionType, - ScopeType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { testValues } from '~/test-utils/consts'; - -export class MockComplianceRequirements { - requirements = [ - { - id: new BigNumber(1), - conditions: [ - { - type: ConditionType.IsPresent, - claim: { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: did, - }, - }, - target: 'Receiver', - trustedClaimIssuers: [], - }, - ], - }, - ]; - - defaultTrustedClaimIssuers = []; -} - -const { did } = testValues; - -export const mockComplianceRequirements = createMock({ - requirements: [ - { - id: new BigNumber(1), - conditions: [ - { - type: ConditionType.IsPresent, - claim: { - type: ClaimType.Accredited, - scope: { - type: ScopeType.Identity, - value: did, - }, - }, - target: ConditionTarget.Receiver, - trustedClaimIssuers: [{ identity: { did } }], - }, - ], - }, - ], - defaultTrustedClaimIssuers: [], -}); diff --git a/src/compliance/models/compliance-requirements.model.ts b/src/compliance/models/compliance-requirements.model.ts deleted file mode 100644 index 82d35333..00000000 --- a/src/compliance/models/compliance-requirements.model.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { RequirementModel } from '~/compliance/models/requirement.model'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; - -export class ComplianceRequirementsModel { - @ApiProperty({ - description: "List of an Asset's compliance requirements", - type: RequirementModel, - isArray: true, - }) - @Type(() => RequirementModel) - readonly requirements: RequirementModel[]; - - @ApiProperty({ - description: - 'List of default Trusted Claim Issuers. This is used for conditions where no trusted Claim issuers were specified (i.e. where `trustedClaimIssuers` is undefined)', - type: TrustedClaimIssuerModel, - isArray: true, - }) - @Type(() => TrustedClaimIssuerModel) - readonly defaultTrustedClaimIssuers: TrustedClaimIssuerModel[]; - - constructor(model: ComplianceRequirementsModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/compliance-status.model.ts b/src/compliance/models/compliance-status.model.ts deleted file mode 100644 index 74f39bcf..00000000 --- a/src/compliance/models/compliance-status.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; - -export class ComplianceStatusModel { - @ApiProperty({ - description: 'Indicator to know if compliance requirements are paused or not', - type: 'boolean', - example: true, - }) - readonly arePaused: boolean; - - constructor(model: ComplianceStatusModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/requirement.model.ts b/src/compliance/models/requirement.model.ts deleted file mode 100644 index 07ca2075..00000000 --- a/src/compliance/models/requirement.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Condition } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntityObject } from '~/common/decorators/transformation'; - -export class RequirementModel { - @ApiProperty({ - description: 'Unique ID of the Requirement', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'List of Conditions', - isArray: true, - example: [ - { - type: 'IsPresent', - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: '0x0600000000000000000000000000000000000000000000000000000000000000', - }, - }, - target: 'Receiver', - trustedClaimIssuers: [], - }, - ], - }) - @FromEntityObject() - readonly conditions: Condition[]; - - constructor(model: RequirementModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/models/trusted-claim-issuer.model.ts b/src/compliance/models/trusted-claim-issuer.model.ts deleted file mode 100644 index d5e71aa9..00000000 --- a/src/compliance/models/trusted-claim-issuer.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ClaimType } from '@polymeshassociation/polymesh-sdk/types'; - -export class TrustedClaimIssuerModel { - @ApiProperty({ - description: 'DID of the Claim Issuer', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiPropertyOptional({ - description: - 'List of Claim types for which this Claim Issuer is trusted. A null value means that the issuer is trusted for all Claim types', - type: 'string', - enum: ClaimType, - isArray: true, - example: [ClaimType.Accredited, ClaimType.Affiliate], - nullable: true, - }) - readonly trustedFor: ClaimType[] | null; - - constructor(model: TrustedClaimIssuerModel) { - Object.assign(this, model); - } -} diff --git a/src/compliance/trusted-claim-issuers.controller.spec.ts b/src/compliance/trusted-claim-issuers.controller.spec.ts deleted file mode 100644 index 48228324..00000000 --- a/src/compliance/trusted-claim-issuers.controller.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TrustedClaimIssuersController } from '~/compliance/trusted-claim-issuers.controller'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { createMockTxResult, mockTrustedClaimIssuer } from '~/test-utils/mocks'; -import { mockTrustedClaimIssuersServiceProvider } from '~/test-utils/service-mocks'; - -describe('TrustedClaimIssuersController', () => { - const mockParams = { ticker: 'TICKER' }; - let controller: TrustedClaimIssuersController; - let mockService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TrustedClaimIssuersController], - providers: [mockTrustedClaimIssuersServiceProvider], - }).compile(); - - mockService = - mockTrustedClaimIssuersServiceProvider.useValue as DeepMocked; - controller = module.get(TrustedClaimIssuersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getTrustedClaimIssuers', () => { - it('should return the list of all trusted Claim Issuers of an Asset', async () => { - when(mockService.find) - .calledWith(mockParams.ticker) - .mockResolvedValue([mockTrustedClaimIssuer]); - - const result = await controller.getTrustedClaimIssuers(mockParams); - - expect(result).toEqual({ - results: [ - { - did: mockTrustedClaimIssuer.identity.did, - trustedFor: mockTrustedClaimIssuer.trustedFor, - }, - ], - }); - }); - }); - - describe('setTrustedClaimIssuers', () => { - it('should accept SetTrustedClaimIssuersDto and set Asset trusted claim issuers', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.AddDefaultTrustedClaimIssuer - ); - const mockPayload: SetTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.set) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.setTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('addTrustedClaimIssuers', () => { - it('should accept SetTrustedClaimIssuersDto and add Asset trusted claim issuers', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.AddDefaultTrustedClaimIssuer - ); - const mockPayload: SetTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.add) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.addTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('removeTrustedClaimIssuers', () => { - it('should accept RemoveTrustedClaimIssuersDto and remove trusted claim issuers for Asset', async () => { - const testTxResult = createMockTxResult( - TxTags.complianceManager.RemoveDefaultTrustedClaimIssuer - ); - - const mockPayload: RemoveTrustedClaimIssuersDto = { - claimIssuers: [], - signer: 'Alice', - }; - - when(mockService.remove) - .calledWith(mockParams.ticker, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.removeTrustedClaimIssuers({ ticker: 'TICKER' }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/compliance/trusted-claim-issuers.controller.ts b/src/compliance/trusted-claim-issuers.controller.ts deleted file mode 100644 index ada7e264..00000000 --- a/src/compliance/trusted-claim-issuers.controller.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TrustedClaimIssuerModel } from '~/compliance/models/trusted-claim-issuer.model'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; - -@ApiTags('assets', 'compliance') -@Controller('assets/:ticker/trusted-claim-issuers') -export class TrustedClaimIssuersController { - constructor(private readonly trustedClaimIssuersService: TrustedClaimIssuersService) {} - - @ApiOperation({ - summary: 'Fetch trusted Claim Issuers of an Asset', - description: - 'This endpoint will provide the list of all default trusted Claim Issuers of an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(TrustedClaimIssuerModel, { - description: 'List of trusted Claim Issuers of the Asset', - paginated: false, - }) - @Get('') - public async getTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const results = await this.trustedClaimIssuersService.find(ticker); - return new ResultsModel({ - results: results.map( - ({ identity: { did }, trustedFor }) => new TrustedClaimIssuerModel({ did, trustedFor }) - ), - }); - } - - @ApiOperation({ - summary: 'Set trusted Claim Issuers of an Asset', - description: - 'This endpoint will assign a new default list of trusted Claim Issuers to the Asset by replacing the existing ones', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be set', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.BAD_REQUEST]: ['The supplied claim issuer list is equal to the current one'], - }) - @Post('set') - public async setTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.set(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Add trusted Claim Issuers of an Asset', - description: - "This endpoint will add the supplied Identities to the Asset's list of trusted claim issuers", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be added', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'One or more of the supplied Identities already are Trusted Claim Issuers', - ], - }) - @Post('add') - public async addTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: SetTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.add(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Remove trusted Claim Issuers of an Asset', - description: - "This endpoint will remove the supplied Identities from the Asset's list of trusted claim issuers", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose trusted Claim Issuers are to be removed', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['Asset was not found', 'Some of the supplied Identities do not exist'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'One or more of the supplied Identities are not Trusted Claim Issuers', - ], - }) - @Post('remove') - public async removeTrustedClaimIssuers( - @Param() { ticker }: TickerParamsDto, - @Body() params: RemoveTrustedClaimIssuersDto - ): Promise { - const result = await this.trustedClaimIssuersService.remove(ticker, params); - - return handleServiceResult(result); - } -} diff --git a/src/compliance/trusted-claim-issuers.service.spec.ts b/src/compliance/trusted-claim-issuers.service.spec.ts deleted file mode 100644 index 227e497f..00000000 --- a/src/compliance/trusted-claim-issuers.service.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ClaimType, FungibleAsset, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; -import { TransactionType } from '~/common/types'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { testValues } from '~/test-utils/consts'; -import { createMockTransactionResult, MockAsset } from '~/test-utils/mocks'; -import { - MockAssetService, - MockComplianceRequirementsService, - mockTransactionsProvider, -} from '~/test-utils/service-mocks'; - -describe('TrustedClaimIssuersService', () => { - let service: TrustedClaimIssuersService; - const mockAssetsService = new MockAssetService(); - const mockComplianceRequirementsService = new MockComplianceRequirementsService(); - const mockTransactionsService = mockTransactionsProvider.useValue; - const getMockTransaction = ( - transactionTag: string - ): TransactionModel | BatchTransactionModel => ({ - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag, - }); - const { txResult, signer } = testValues; - - const mockClaimIssuers = [ - { - identity: 'Ox6'.padEnd(66, '0'), - trustedFor: [ClaimType.Accredited, ClaimType.Affiliate], - }, - ]; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AssetsService, - ComplianceRequirementsService, - TrustedClaimIssuersService, - mockTransactionsProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .overrideProvider(ComplianceRequirementsService) - .useValue(mockComplianceRequirementsService) - .compile(); - - service = module.get(TrustedClaimIssuersService); - }); - - afterEach(() => { - mockTransactionsService.submit.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('find', () => { - it('should return the list of trusted Claim Issuers of an Asset', async () => { - const mockAsset = new MockAsset(); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - mockAsset.compliance.trustedClaimIssuers.get.mockResolvedValue(mockClaimIssuers); - - const result = await service.find('TICKER'); - - expect(result).toEqual(mockClaimIssuers); - }); - }); - - describe('set', () => { - it('should set trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [getMockTransaction(TxTags.complianceManager.AddDefaultTrustedClaimIssuer)], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.set('TICKER', { signer, claimIssuers: mockClaimIssuers }); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('add', () => { - it('should add trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [getMockTransaction(TxTags.complianceManager.AddDefaultTrustedClaimIssuer)], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.add('TICKER', { signer, claimIssuers: mockClaimIssuers }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.compliance.trustedClaimIssuers.add, - { claimIssuers: mockClaimIssuers }, - { signer } - ); - expect(result).toEqual(testTxResult); - }); - }); - - describe('remove', () => { - it('should remove trusted Claim Issuers for an Asset', async () => { - const mockAsset = new MockAsset(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [ - getMockTransaction(TxTags.complianceManager.RemoveDefaultTrustedClaimIssuer), - ], - }); - - mockTransactionsService.submit.mockResolvedValue(testTxResult); - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.remove('TICKER', { - signer, - claimIssuers: [mockClaimIssuers[0].identity], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.compliance.trustedClaimIssuers.remove, - { claimIssuers: [mockClaimIssuers[0].identity] }, - { signer } - ); - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/compliance/trusted-claim-issuers.service.ts b/src/compliance/trusted-claim-issuers.service.ts deleted file mode 100644 index 1fb1111a..00000000 --- a/src/compliance/trusted-claim-issuers.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TrustedClaimIssuer } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { RemoveTrustedClaimIssuersDto } from '~/compliance/dto/remove-trusted-claim-issuers.dto'; -import { SetTrustedClaimIssuersDto } from '~/compliance/dto/set-trusted-claim-issuers.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class TrustedClaimIssuersService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionsService: TransactionsService - ) {} - - public async find(ticker: string): Promise[]> { - const asset = await this.assetsService.findOne(ticker); - - return asset.compliance.trustedClaimIssuers.get(); - } - - public async set(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.set, args, base); - } - - public async add(ticker: string, params: SetTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.add, args, base); - } - - public async remove(ticker: string, params: RemoveTrustedClaimIssuersDto): ServiceReturn { - const { base, args } = extractTxBase(params); - const asset = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(asset.compliance.trustedClaimIssuers.remove, args, base); - } -} diff --git a/src/confidential-accounts/confidential-accounts.controller.spec.ts b/src/confidential-accounts/confidential-accounts.controller.spec.ts new file mode 100644 index 00000000..71d4c989 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.controller.spec.ts @@ -0,0 +1,194 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAccount, + ConfidentialAssetHistoryEntry, + EventIdEnum, + ResultSet, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialAsset, createMockIdentity } from '~/test-utils/mocks'; +import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; + +describe('ConfidentialAccountsController', () => { + let controller: ConfidentialAccountsController; + let mockConfidentialAccountsService: DeepMocked; + const confidentialAccount = 'SOME_PUBLIC_KEY'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAccountsController], + providers: [mockConfidentialAccountsServiceProvider], + }).compile(); + + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + + controller = module.get(ConfidentialAccountsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('linkAccount', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialAccountsService.linkConfidentialAccount.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.linkAccount({ confidentialAccount }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('getOwner', () => { + it('should get the owner of a Confidential Account', async () => { + mockConfidentialAccountsService.fetchOwner.mockResolvedValue( + createMockIdentity({ did: 'OWNER_DID' }) + ); + + const result = await controller.getOwner({ confidentialAccount }); + + expect(result).toEqual(expect.objectContaining({ did: 'OWNER_DID' })); + }); + }); + + describe('getAllBalances and getAllIncomingBalances', () => { + it('should get all confidential asset balances', async () => { + const confidentialAsset = createMockConfidentialAsset(); + const balance = '0xsomebalance'; + const mockResult = [ + { + confidentialAsset, + balance, + }, + ]; + mockConfidentialAccountsService.getAllBalances.mockResolvedValue(mockResult); + + let result = await controller.getAllBalances({ confidentialAccount }); + + expect(result).toEqual( + expect.arrayContaining([{ confidentialAsset: confidentialAsset.id, balance }]) + ); + + mockConfidentialAccountsService.getAllIncomingBalances.mockResolvedValue(mockResult); + + result = await controller.getAllIncomingBalances({ confidentialAccount }); + + expect(result).toEqual( + expect.arrayContaining([{ confidentialAsset: confidentialAsset.id, balance }]) + ); + }); + }); + + describe('getConfidentialAssetBalance and getIncomingConfidentialAssetBalance', () => { + it('should get all confidential asset balances', async () => { + const confidentialAssetId = 'SOME_ASSET_ID'; + const balance = '0xsomebalance'; + mockConfidentialAccountsService.getAssetBalance.mockResolvedValue({ + confidentialAsset: confidentialAssetId, + balance, + }); + + let result = await controller.getConfidentialAssetBalance({ + confidentialAccount, + confidentialAssetId, + }); + + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); + + mockConfidentialAccountsService.getIncomingAssetBalance.mockResolvedValue({ + balance, + confidentialAsset: confidentialAssetId, + }); + + result = await controller.getIncomingConfidentialAssetBalance({ + confidentialAccount, + confidentialAssetId, + }); + + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); + }); + }); + + describe('applyAllIncomingAssetBalances', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialAccountsService.applyAllIncomingAssetBalances.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.applyAllIncomingAssetBalances({ confidentialAccount }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('getTransactionHistory', () => { + const mockTransactionHistories: ResultSet = { + data: [ + { + asset: createMockConfidentialAsset({ id: '0xassetId' }), + amount: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + eventId: EventIdEnum.AccountDeposit, + createdAt: { + blockHash: '0xblockhash', + blockNumber: new BigNumber(1), + blockDate: new Date('05/23/2021'), + eventIndex: new BigNumber(1), + }, + }, + ], + next: '0', + count: new BigNumber(1), + }; + + it('should call the service and return the results', async () => { + const input = { + confidentialAccount, + size: new BigNumber(10), + }; + + mockConfidentialAccountsService.getTransactionHistory.mockResolvedValue( + mockTransactionHistories + ); + + const expectedResults = mockTransactionHistories.data.map( + ({ amount, eventId, asset, createdAt }) => { + return new ConfidentialTransactionHistoryModel({ + assetId: asset.toHuman(), + amount, + eventId, + createdAt: createdAt?.blockDate, + }); + } + ); + + const result = await controller.getTransactionHistory({ confidentialAccount }, input); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expectedResults, + total: new BigNumber(mockTransactionHistories.count as BigNumber), + next: mockTransactionHistories.next, + }) + ); + }); + }); +}); diff --git a/src/confidential-accounts/confidential-accounts.controller.ts b/src/confidential-accounts/confidential-accounts.controller.ts new file mode 100644 index 00000000..c8c42684 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.controller.ts @@ -0,0 +1,316 @@ +import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { TransactionHistoryParamsDto } from '~/confidential-accounts/dto/transaction-history-params.dto'; +import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { ConfidentialTransactionHistoryModel } from '~/confidential-accounts/models/confidential-transaction-history.model'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { + ApiArrayResponse, + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; + +@ApiTags('confidential-accounts') +@Controller('confidential-accounts') +export class ConfidentialAccountsController { + constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + + @Post(':confidentialAccount/link') + @ApiOperation({ + summary: 'Links a Confidential Account to an Identity', + description: 'This endpoint links a given confidential Account to the signer on chain', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'The given Confidential Account is already linked to an Identity', + ], + }) + public async linkAccount( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: TransactionBaseDto + ): Promise { + const result = await this.confidentialAccountsService.linkConfidentialAccount( + confidentialAccount, + params + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Get owner of a Confidential Account', + description: + 'This endpoint retrieves the DID to which a Confidential Account is linked to on chain', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'DID of the owner of the Confidential Account', + type: IdentityModel, + }) + @ApiNotFoundResponse({ + description: 'No owner exists for the Confidential Account', + }) + @Get(':confidentialAccount/owner') + public async getOwner( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const { did } = await this.confidentialAccountsService.fetchOwner(confidentialAccount); + + return new IdentityModel({ did }); + } + + @ApiOperation({ + summary: 'Get all Confidential Asset balances', + description: + 'This endpoint retrieves the balances of all the Confidential Assets held by a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'List of all incoming Confidential Asset balances', + type: ConfidentialAssetBalanceModel, + isArray: true, + }) + @Get(':confidentialAccount/balances') + public async getAllBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const results = await this.confidentialAccountsService.getAllBalances(confidentialAccount); + + return results.map( + ({ confidentialAsset: { id: confidentialAsset }, balance }) => + new ConfidentialAssetBalanceModel({ confidentialAsset, balance }) + ); + } + + @ApiOperation({ + summary: 'Get the balance of a specific Confidential Asset', + description: + 'This endpoint retrieves the existing balance of a specific Confidential Asset in the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset whose balance is to be fetched', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'The encrypted balance of the Confidential Asset', + type: ConfidentialAssetBalanceModel, + }) + @ApiNotFoundResponse({ + description: 'No balance was found for the given Confidential Asset', + }) + @Get(':confidentialAccount/balances/:confidentialAssetId') + public async getConfidentialAssetBalance( + @Param() + { + confidentialAccount, + confidentialAssetId, + }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto + ): Promise { + return this.confidentialAccountsService.getAssetBalance( + confidentialAccount, + confidentialAssetId + ); + } + + @ApiOperation({ + summary: 'Get all incoming Confidential Asset balances', + description: + 'This endpoint retrieves the incoming balances of all the Confidential Assets held by a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'List of all incoming Confidential Asset balances', + type: ConfidentialAssetBalanceModel, + isArray: true, + }) + @Get(':confidentialAccount/incoming-balances') + public async getAllIncomingBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto + ): Promise { + const results = await this.confidentialAccountsService.getAllIncomingBalances( + confidentialAccount + ); + + return results.map( + ({ confidentialAsset: { id: confidentialAsset }, balance }) => + new ConfidentialAssetBalanceModel({ confidentialAsset, balance }) + ); + } + + @ApiOperation({ + summary: 'Get incoming balance of a specific Confidential Asset', + description: + 'This endpoint retrieves the incoming balance of a specific Confidential Asset in the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset for which the incoming balance is to be fetched', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + type: 'string', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @ApiOkResponse({ + description: 'Encrypted incoming balance of the Confidential Asset', + type: ConfidentialAssetBalanceModel, + }) + @ApiNotFoundResponse({ + description: 'No incoming balance is found for the given Confidential Asset', + }) + @Get(':confidentialAccount/incoming-balances/:confidentialAssetId') + public async getIncomingConfidentialAssetBalance( + @Param() + { + confidentialAccount, + confidentialAssetId, + }: ConfidentialAccountParamsDto & ConfidentialAssetIdParamsDto + ): Promise { + return this.confidentialAccountsService.getIncomingAssetBalance( + confidentialAccount, + confidentialAssetId + ); + } + + @Post(':confidentialAccount/incoming-balances/apply') + @ApiOperation({ + summary: 'Deposit all incoming balances for a Confidential Account', + description: 'This endpoint deposit all the incoming balances for a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'The Signing Identity cannot apply incoming balances in the specified Confidential Account', + ], + [HttpStatus.NOT_FOUND]: ['No incoming balance for the given the Confidential Account'], + }) + public async applyAllIncomingAssetBalances( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: TransactionBaseDto + ): Promise { + const result = await this.confidentialAccountsService.applyAllIncomingAssetBalances( + confidentialAccount, + params + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Get transaction history of a specific Confidential Account', + description: + 'This endpoint retrieves the transaction history for the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @ApiQuery({ + name: 'size', + description: 'The number of transaction history entries to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start index from which transaction history entries are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'No Confidential Account was found', + }) + @ApiArrayResponse(ConfidentialTransactionHistoryModel) + @Get(':confidentialAccount/transaction-history') + public async getTransactionHistory( + @Param() + { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start, assetId, eventId }: TransactionHistoryParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.getTransactionHistory( + confidentialAccount, + { size, start: new BigNumber(start || 0), assetId, eventId } + ); + + return new PaginatedResultsModel({ + results: data?.map( + ({ asset, amount, eventId: event, createdAt }) => + new ConfidentialTransactionHistoryModel({ + assetId: asset.toHuman(), + amount, + eventId: event, + createdAt: createdAt?.blockDate, + }) + ), + total: count, + next, + }); + } +} diff --git a/src/confidential-accounts/confidential-accounts.module.ts b/src/confidential-accounts/confidential-accounts.module.ts new file mode 100644 index 00000000..e7cc85ad --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.module.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { Module } from '@nestjs/common'; + +import { ConfidentialAccountsController } from '~/confidential-accounts/confidential-accounts.controller'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [PolymeshModule, TransactionsModule], + controllers: [ConfidentialAccountsController], + providers: [ConfidentialAccountsService], + exports: [ConfidentialAccountsService], +}) +export class ConfidentialAccountsModule {} diff --git a/src/confidential-accounts/confidential-accounts.service.spec.ts b/src/confidential-accounts/confidential-accounts.service.spec.ts new file mode 100644 index 00000000..f016a236 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.service.spec.ts @@ -0,0 +1,357 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAccount, + ConfidentialAssetBalance, + ConfidentialAssetHistoryEntry, + EventIdEnum, + ResultSet, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + createMockConfidentialTransaction, + MockPolymesh, + MockTransaction, +} from '~/test-utils/mocks'; +import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; + +describe('ConfidentialAccountsService', () => { + let service: ConfidentialAccountsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + const confidentialAccount = 'SOME_PUBLIC_KEY'; + + beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [PolymeshModule], + providers: [ConfidentialAccountsService, mockTransactionsProvider], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockTransactionsService = module.get(TransactionsService); + + service = module.get(ConfidentialAccountsService); + }); + + afterEach(async () => { + await polymeshService.close(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return a Confidential Account for a valid publicKey', async () => { + const account = createMockConfidentialAccount(); + mockPolymeshApi.confidentialAccounts.getConfidentialAccount.mockResolvedValue(account); + + const result = await service.findOne(confidentialAccount); + + expect(result).toEqual(account); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialAccounts.getConfidentialAccount.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(service.findOne(confidentialAccount)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('fetchOwner', () => { + it('should return the owner of Confidential Account', async () => { + const mockConfidentialAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockConfidentialAccount); + + const result = await service.fetchOwner(confidentialAccount); + + expect(result).toEqual(expect.objectContaining({ did: 'SOME_OWNER' })); + }); + + it('should throw an error if no owner exists', async () => { + const mockConfidentialAccount = createMockConfidentialAccount(); + mockConfidentialAccount.getIdentity.mockResolvedValue(null); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockConfidentialAccount); + + await expect(service.fetchOwner(confidentialAccount)).rejects.toThrow( + 'No owner exists for the Confidential Account' + ); + }); + }); + + describe('linkConfidentialAccount', () => { + it('should link a given public key to the signer', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateAccount, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAccount = createMockConfidentialAccount(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAccount, + transactions: [mockTransaction], + }); + + const result = await service.linkConfidentialAccount(confidentialAccount, input); + + expect(result).toEqual({ + result: mockAccount, + transactions: [mockTransaction], + }); + }); + }); + + describe('getAllBalances and getAllIncomingBalances', () => { + let account: DeepMocked; + let balances: ConfidentialAssetBalance[]; + + beforeEach(() => { + balances = [ + { + confidentialAsset: createMockConfidentialAsset(), + balance: '0xsomebalance', + }, + ]; + + account = createMockConfidentialAccount(); + }); + + describe('getAllBalances', () => { + it('should return all balances for a Confidential Account', async () => { + account.getBalances.mockResolvedValue(balances); + + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAllBalances(confidentialAccount); + + expect(result).toEqual(balances); + }); + }); + + describe('getAllIncomingBalances', () => { + it('should return all incoming balances for a Confidential Account', async () => { + account.getIncomingBalances.mockResolvedValue(balances); + + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAllIncomingBalances(confidentialAccount); + + expect(result).toEqual(balances); + }); + }); + }); + + describe('getAssetBalance and getIncomingAssetBalance', () => { + let account: DeepMocked; + let balance: string; + let confidentialAssetId: string; + + beforeEach(() => { + balance = '0xsomebalance'; + confidentialAssetId = 'SOME_ASSET_ID'; + + account = createMockConfidentialAccount(); + account.getBalance.mockResolvedValue(balance); + account.getIncomingBalance.mockResolvedValue(balance); + }); + + describe('getAssetBalance', () => { + it('should return balance for a specific Confidential Asset', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getAssetBalance(confidentialAccount, confidentialAssetId); + + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + account.getBalance.mockRejectedValue(mockError); + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect( + service.getAssetBalance(confidentialAccount, confidentialAssetId) + ).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('getIncomingAssetBalance', () => { + it('should return the incoming balance for a specific Confidential Asset', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const result = await service.getIncomingAssetBalance( + confidentialAccount, + confidentialAssetId + ); + + expect(result).toEqual({ balance, confidentialAsset: confidentialAssetId }); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + account.getIncomingBalance.mockRejectedValue(mockError); + jest.spyOn(service, 'findOne').mockResolvedValue(account); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect( + service.getIncomingAssetBalance(confidentialAccount, confidentialAssetId) + ).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + }); + + describe('applyAllIncomingAssetBalances', () => { + it('should deposit all incoming balances for a Confidential Account', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.ApplyIncomingBalances, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAccount = createMockConfidentialAccount(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAccount, + transactions: [mockTransaction], + }); + + const result = await service.applyAllIncomingAssetBalances(confidentialAccount, input); + + expect(result).toEqual({ + result: mockAccount, + transactions: [mockTransaction], + }); + }); + }); + + describe('findHeldAssets', () => { + it('should return the list of Confidential Assets held by an Confidential Account', async () => { + const mockAssets = { + data: [ + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getHeldAssets.mockResolvedValue(mockAssets); + + const result = await service.findHeldAssets( + 'SOME_PUBLIC_KEY', + new BigNumber(2), + new BigNumber(0) + ); + expect(result).toEqual(mockAssets); + }); + }); + + describe('getAssociatedTransactions', () => { + it('should return the list of transactions associated to a Confidential Account', async () => { + const mockTransactions = { + data: [ + createMockConfidentialTransaction({ id: new BigNumber(10) }), + createMockConfidentialTransaction({ id: new BigNumber(12) }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getTransactions.mockResolvedValue(mockTransactions); + + const result = await service.getAssociatedTransactions( + 'SOME_PUBLIC_KEY', + ConfidentialTransactionDirectionEnum.All, + new BigNumber(2), + new BigNumber(0) + ); + expect(result).toEqual(mockTransactions); + }); + }); + + describe('getTransactionHistory', () => { + it('should return the list of Transaction Histories for an Confidential Account', async () => { + const mockHistory: ResultSet = { + data: [ + { + asset: createMockConfidentialAsset({ id: '0xassetId' }), + amount: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + eventId: EventIdEnum.AccountDeposit, + createdAt: { + blockHash: '0xblockhash', + blockNumber: new BigNumber(1), + blockDate: new Date('05/23/2021'), + eventIndex: new BigNumber(1), + }, + }, + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + const mockAccount = createMockConfidentialAccount(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAccount); + + mockAccount.getTransactionHistory.mockResolvedValue(mockHistory); + + const result = await service.getTransactionHistory('SOME_PUBLIC_KEY', { + start: new BigNumber(0), + size: new BigNumber(10), + }); + + expect(result).toEqual(mockHistory); + }); + }); +}); diff --git a/src/confidential-accounts/confidential-accounts.service.ts b/src/confidential-accounts/confidential-accounts.service.ts new file mode 100644 index 00000000..2737db35 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.service.ts @@ -0,0 +1,154 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAccount, + ConfidentialAsset, + ConfidentialAssetBalance, + ConfidentialAssetHistoryEntry, + ConfidentialTransaction, + EventIdEnum, + Identity, + ResultSet, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAssetBalanceModel } from '~/confidential-accounts/models/confidential-asset-balance.model'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialAccountsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService + ) {} + + public async findOne(publicKey: string): Promise { + return await this.polymeshService.polymeshApi.confidentialAccounts + .getConfidentialAccount({ publicKey }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async fetchOwner(publicKey: string): Promise { + const account = await this.findOne(publicKey); + + const identity = await account.getIdentity(); + + if (!identity) { + throw new NotFoundException('No owner exists for the Confidential Account'); + } + + return identity; + } + + public async linkConfidentialAccount( + publicKey: string, + base: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(base); + const createConfidentialAccount = + this.polymeshService.polymeshApi.confidentialAccounts.createConfidentialAccount; + + return this.transactionsService.submit(createConfidentialAccount, { publicKey }, options); + } + + public async getAllBalances(confidentialAccount: string): Promise { + const account = await this.findOne(confidentialAccount); + + return account.getBalances(); + } + + public async getAssetBalance( + confidentialAccount: string, + asset: string + ): Promise { + const account = await this.findOne(confidentialAccount); + + const balance = await account.getBalance({ asset }).catch(error => { + throw handleSdkError(error); + }); + + return new ConfidentialAssetBalanceModel({ + confidentialAsset: asset, + balance, + }); + } + + public async getAllIncomingBalances( + confidentialAccount: string + ): Promise { + const account = await this.findOne(confidentialAccount); + + return account.getIncomingBalances(); + } + + public async getIncomingAssetBalance( + confidentialAccount: string, + asset: string + ): Promise { + const account = await this.findOne(confidentialAccount); + + const balance = await account.getIncomingBalance({ asset }).catch(error => { + throw handleSdkError(error); + }); + + return new ConfidentialAssetBalanceModel({ + balance, + confidentialAsset: asset, + }); + } + + public async applyAllIncomingAssetBalances( + confidentialAccount: string, + base: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(base); + const applyIncomingBalances = + this.polymeshService.polymeshApi.confidentialAccounts.applyIncomingBalances; + + return this.transactionsService.submit(applyIncomingBalances, { confidentialAccount }, options); + } + + public async findHeldAssets( + confidentialAccount: string, + size?: BigNumber, + start?: BigNumber + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getHeldAssets({ size, start }); + } + + public async getAssociatedTransactions( + confidentialAccount: string, + direction: ConfidentialTransactionDirectionEnum, + size: BigNumber, + start?: BigNumber + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getTransactions({ direction, size, start }); + } + + public async getTransactionHistory( + confidentialAccount: string, + filters: { + size?: BigNumber; + start?: BigNumber; + assetId?: string; + eventId?: + | EventIdEnum.AccountDeposit + | EventIdEnum.AccountWithdraw + | EventIdEnum.AccountDepositIncoming; + } + ): Promise> { + const account = await this.findOne(confidentialAccount); + + return account.getTransactionHistory(filters); + } +} diff --git a/src/confidential-accounts/confidential-accounts.util.ts b/src/confidential-accounts/confidential-accounts.util.ts new file mode 100644 index 00000000..02beb2f8 --- /dev/null +++ b/src/confidential-accounts/confidential-accounts.util.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ConfidentialAccount } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; + +/** + * Create a ConfidentialAccountModel from ConfidentialAccount + */ +export function createConfidentialAccountModel( + account: ConfidentialAccount +): ConfidentialAccountModel { + const { publicKey } = account; + return new ConfidentialAccountModel({ publicKey }); +} diff --git a/src/accounts/dto/account-params.dto.ts b/src/confidential-accounts/dto/confidential-account-params.dto.ts similarity index 51% rename from src/accounts/dto/account-params.dto.ts rename to src/confidential-accounts/dto/confidential-account-params.dto.ts index e15b75bf..58055372 100644 --- a/src/accounts/dto/account-params.dto.ts +++ b/src/confidential-accounts/dto/confidential-account-params.dto.ts @@ -2,7 +2,7 @@ import { IsString } from 'class-validator'; -export class AccountParamsDto { +export class ConfidentialAccountParamsDto { @IsString() - readonly account: string; + readonly confidentialAccount: string; } diff --git a/src/confidential-accounts/dto/transaction-history-params.dto.ts b/src/confidential-accounts/dto/transaction-history-params.dto.ts new file mode 100644 index 00000000..8fac5caf --- /dev/null +++ b/src/confidential-accounts/dto/transaction-history-params.dto.ts @@ -0,0 +1,36 @@ +/* istanbul ignore file */ + +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; +import { IsOptional } from 'class-validator'; + +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; + +export class TransactionHistoryParamsDto extends PaginatedParamsDto { + @ApiPropertyOptional({ + description: + 'Filter transaction history by Confidential Asset ID.
If none specified, returns all transaction history entries for Confidential Account', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @IsOptional() + @IsConfidentialAssetId() + readonly assetId?: string; + + @ApiPropertyOptional({ + description: + 'Filter transaction history by type.
If none specified, returns all transaction history entries for Confidential Account', + enum: [ + EventIdEnum.AccountDeposit, + EventIdEnum.AccountWithdraw, + EventIdEnum.AccountDepositIncoming, + ], + example: EventIdEnum.AccountDeposit, + }) + @IsOptional() + readonly eventId?: + | EventIdEnum.AccountDeposit + | EventIdEnum.AccountWithdraw + | EventIdEnum.AccountDepositIncoming; +} diff --git a/src/confidential-accounts/models/confidential-account.model.ts b/src/confidential-accounts/models/confidential-account.model.ts new file mode 100644 index 00000000..7557a4f4 --- /dev/null +++ b/src/confidential-accounts/models/confidential-account.model.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAccountModel { + @ApiProperty({ + description: 'The public key of the ElGamal key pair', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + readonly publicKey: string; + + constructor(model: ConfidentialAccountModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-accounts/models/confidential-asset-balance.model.ts b/src/confidential-accounts/models/confidential-asset-balance.model.ts new file mode 100644 index 00000000..8a40db91 --- /dev/null +++ b/src/confidential-accounts/models/confidential-asset-balance.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAssetBalanceModel { + @ApiProperty({ + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly confidentialAsset: string; + + @ApiProperty({ + description: 'Encrypted balance of the Confidential Asset', + type: 'string', + example: + '0x289ebc384a263acd5820e03988dd17a3cd49ee57d572f4131e116b6bf4c70a1594447bb5d1e2d9cc62f083d8552dd90ec09b23a519b361e458d7fe1e48882261', + }) + readonly balance: string; + + constructor(model: ConfidentialAssetBalanceModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-accounts/models/confidential-transaction-history.model.ts b/src/confidential-accounts/models/confidential-transaction-history.model.ts new file mode 100644 index 00000000..223f0d7a --- /dev/null +++ b/src/confidential-accounts/models/confidential-transaction-history.model.ts @@ -0,0 +1,41 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; + +export class ConfidentialTransactionHistoryModel { + @ApiProperty({ + description: 'The ID of the confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly assetId: string; + + @ApiProperty({ + description: 'The encrypted amount ', + type: 'string', + example: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + }) + readonly amount: string; + + @ApiProperty({ + description: + 'The type of transaction. Possible values "AccountWithdraw", "AccountDeposit", "AccountDepositIncoming"', + type: 'string', + example: 'AccountWithdraw', + }) + readonly eventId: EventIdEnum; + + @ApiProperty({ + description: 'Date at which the transaction was added to chain', + type: 'string', + example: new Date('05/23/2021').toISOString(), + nullable: true, + }) + readonly createdAt?: Date; + + constructor(model: ConfidentialTransactionHistoryModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/confidential-assets.consts.ts b/src/confidential-assets/confidential-assets.consts.ts new file mode 100644 index 00000000..50eb4a75 --- /dev/null +++ b/src/confidential-assets/confidential-assets.consts.ts @@ -0,0 +1,3 @@ +/* istanbul ignore file */ + +export const ASSET_ID_LENGTH = 32; diff --git a/src/confidential-assets/confidential-assets.controller.spec.ts b/src/confidential-assets/confidential-assets.controller.spec.ts new file mode 100644 index 00000000..2caf42b0 --- /dev/null +++ b/src/confidential-assets/confidential-assets.controller.spec.ts @@ -0,0 +1,236 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAsset, + ConfidentialAssetDetails, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { getMockTransaction, testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + createMockConfidentialVenue, + createMockIdentity, + createMockTransactionResult, +} from '~/test-utils/mocks'; +import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; + +describe('ConfidentialAssetsController', () => { + let controller: ConfidentialAssetsController; + let mockConfidentialAssetsService: DeepMocked; + const id = '76702175-d8cb-e3a5-5a19-734433351e25'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAssetsController], + providers: [mockConfidentialAssetsServiceProvider], + }).compile(); + + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); + controller = module.get(ConfidentialAssetsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getDetails', () => { + it('should return the details', async () => { + const mockAssetDetails = { + data: 'SOME_DATA', + owner: { + did: 'SOME_DID', + }, + totalSupply: new BigNumber(1), + } as ConfidentialAssetDetails; + const mockAuditorInfo = { + auditors: [createMockConfidentialAccount({ publicKey: 'SOME_AUDITOR' })], + mediators: [createMockIdentity({ did: 'MEDIATOR_DID' })], + }; + const mockConfidentialAsset = createMockConfidentialAsset(); + + mockConfidentialAsset.details.mockResolvedValue(mockAssetDetails); + mockConfidentialAsset.getAuditors.mockResolvedValue(mockAuditorInfo); + mockConfidentialAsset.isFrozen.mockResolvedValue(false); + + mockConfidentialAssetsService.findOne.mockResolvedValue(mockConfidentialAsset); + + const result = await controller.getDetails({ confidentialAssetId: id }); + + expect(result).toEqual({ + ...mockAssetDetails, + isFrozen: false, + auditors: expect.arrayContaining([expect.objectContaining({ publicKey: 'SOME_AUDITOR' })]), + mediators: expect.arrayContaining([expect.objectContaining({ did: 'MEDIATOR_DID' })]), + }); + }); + }); + + describe('createConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + data: 'SOME_DATA', + auditors: ['SOME_PUBLIC_KEY'], + mediators: [], + }; + + const mockConfidentialAsset = createMockConfidentialAsset(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateAsset); + + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockConfidentialAsset, + }); + + when(mockConfidentialAssetsService.createConfidentialAsset) + .calledWith(input) + .mockResolvedValue(testTxResult); + + const result = await controller.createConfidentialAsset(input); + expect(result).toEqual( + new CreatedConfidentialAssetModel({ + ...txResult, + transactions: [transaction], + confidentialAsset: mockConfidentialAsset, + }) + ); + }); + }); + + describe('issueConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + amount: new BigNumber(1000), + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + mockConfidentialAssetsService.issue.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.issueConfidentialAsset({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('getVenueFilteringDetails', () => { + it('should return the venue filtering details for a Confidential Asset', async () => { + mockConfidentialAssetsService.getVenueFilteringDetails.mockResolvedValueOnce({ + enabled: false, + }); + + let result = await controller.getVenueFilteringDetails({ confidentialAssetId: id }); + + expect(result).toEqual(expect.objectContaining({ enabled: false })); + + mockConfidentialAssetsService.getVenueFilteringDetails.mockResolvedValueOnce({ + enabled: true, + allowedConfidentialVenues: [createMockConfidentialVenue({ id: new BigNumber(1) })], + }); + + result = await controller.getVenueFilteringDetails({ confidentialAssetId: id }); + + expect(result).toEqual( + expect.objectContaining({ + enabled: true, + allowedConfidentialVenues: expect.arrayContaining([{ id: new BigNumber(1) }]), + }) + ); + }); + }); + + describe('toggleConfidentialVenueFiltering', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + enabled: true, + }; + mockConfidentialAssetsService.setVenueFilteringDetails.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + const result = await controller.toggleConfidentialVenueFiltering( + { confidentialAssetId: id }, + input + ); + expect(result).toEqual(txResult); + }); + }); + + describe('addAllowedVenues and removeAllowedVenues', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + confidentialVenues: [new BigNumber(1)], + }; + mockConfidentialAssetsService.setVenueFilteringDetails.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.addAllowedVenues({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + + result = await controller.removeAllowedVenues({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('freezeConfidentialAsset and unfreezeConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + mockConfidentialAssetsService.toggleFreezeConfidentialAsset.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.freezeConfidentialAsset({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + + result = await controller.unfreezeConfidentialAsset({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('freezeConfidentialAccount and unfreezeConfidentialAccount', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + mockConfidentialAssetsService.toggleFreezeConfidentialAccountAsset.mockResolvedValue( + txResult as unknown as ServiceReturn + ); + + let result = await controller.freezeConfidentialAccount({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + + result = await controller.unfreezeConfidentialAccount({ confidentialAssetId: id }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('isConfidentialAccountFrozen', () => { + it('should call the service and return the results', async () => { + mockConfidentialAssetsService.isConfidentialAccountFrozen.mockResolvedValue(true); + + const result = await controller.isConfidentialAccountFrozen({ + confidentialAssetId: 'SOME_ASSET_ID', + confidentialAccount: 'SOME_PUBLIC_KEY', + }); + expect(result).toEqual(true); + }); + }); +}); diff --git a/src/confidential-assets/confidential-assets.controller.ts b/src/confidential-assets/confidential-assets.controller.ts new file mode 100644 index 00000000..127209a9 --- /dev/null +++ b/src/confidential-assets/confidential-assets.controller.ts @@ -0,0 +1,393 @@ +import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, + ApiUnprocessableEntityResponse, +} from '@nestjs/swagger'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { createConfidentialAssetDetailsModel } from '~/confidential-assets/confidential-assets.util'; +import { AddAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/add-allowed-confidential-venues.dto'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; +import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { RemoveAllowedConfidentialVenuesDto } from '~/confidential-assets/dto/remove-allowed-confidential-venues.dto'; +import { SetConfidentialVenueFilteringParamsDto } from '~/confidential-assets/dto/set-confidential-venue-filtering-params.dto'; +import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; +import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; +import { ConfidentialVenueFilteringDetailsModel } from '~/confidential-assets/models/confidential-venue-filtering-details.model'; +import { CreatedConfidentialAssetModel } from '~/confidential-assets/models/created-confidential-asset.model'; +import { + ApiTransactionFailedResponse, + ApiTransactionResponse, +} from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { + handleServiceResult, + TransactionResolver, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; + +@ApiTags('confidential-assets') +@Controller('confidential-assets') +export class ConfidentialAssetsController { + constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} + + @ApiOperation({ + summary: 'Fetch Confidential Asset details', + description: + 'This endpoint will provide the basic details of an Confidential Asset along with the auditors information', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset whose details are to be fetched', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Basic details of the Asset', + type: ConfidentialAssetDetailsModel, + }) + @Get(':confidentialAssetId') + public async getDetails( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto + ): Promise { + const asset = await this.confidentialAssetsService.findOne(confidentialAssetId); + + return createConfidentialAssetDetailsModel(asset); + } + + @ApiOperation({ + summary: 'Create a Confidential Asset', + description: 'This endpoint allows for the creation of a new Confidential Asset', + }) + @ApiTransactionResponse({ + description: 'Details about the newly created Confidential Asset', + type: CreatedConfidentialAssetModel, + }) + @ApiUnprocessableEntityResponse({ + description: 'One or more auditors do not exists', + }) + @Post('create') + public async createConfidentialAsset( + @Body() params: CreateConfidentialAssetDto + ): Promise { + const result = await this.confidentialAssetsService.createConfidentialAsset(params); + + const resolver: TransactionResolver = ({ + result: confidentialAsset, + transactions, + details, + }) => + new CreatedConfidentialAssetModel({ + confidentialAsset, + details, + transactions, + }); + + return handleServiceResult(result, resolver); + } + + @ApiOperation({ + summary: 'Issue more of a Confidential Asset', + description: + 'This endpoint issues more of a given Confidential Asset into a specified Confidential Account', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset to be issued', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.UNPROCESSABLE_ENTITY]: [ + 'Amount is not greater than zero', + 'The signer cannot issue the Assets in the given account', + 'Issuance operation will total supply to exceed the supply limit', + ], + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':confidentialAssetId/issue') + public async issueConfidentialAsset( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: IssueConfidentialAssetDto + ): Promise { + const result = await this.confidentialAssetsService.issue(confidentialAssetId, params); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Get venue filtering details', + description: 'This endpoint will return the venue filtering details for a Confidential Asset', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Venue filtering details', + type: ConfidentialVenueFilteringDetailsModel, + }) + @Get(':confidentialAssetId/venue-filtering') + public async getVenueFilteringDetails( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto + ): Promise { + const details = await this.confidentialAssetsService.getVenueFilteringDetails( + confidentialAssetId + ); + + const { enabled, allowedConfidentialVenues } = { + allowedConfidentialVenues: undefined, + ...details, + }; + + return new ConfidentialVenueFilteringDetailsModel({ enabled, allowedConfidentialVenues }); + } + + @ApiOperation({ + summary: 'Enable/disable confidential Venue filtering', + description: + 'This endpoint enables/disables confidential venue filtering for a given Confidential Asset', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':confidentialAssetId/venue-filtering') + public async toggleConfidentialVenueFiltering( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: SetConfidentialVenueFilteringParamsDto + ): Promise { + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + params + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Add a list of Confidential Venues for Confidential Asset transactions', + description: + 'This endpoint adds additional Confidential Venues to existing list of Confidential Venues allowed to handle transfer of the given Confidential Asset', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':confidentialAssetId/venue-filtering/add-allowed-venues') + public async addAllowedVenues( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: AddAllowedConfidentialVenuesDto + ): Promise { + const { confidentialVenues: allowedVenues, ...rest } = params; + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + { + ...rest, + allowedVenues, + } + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Remove a list of Confidential Venues for Confidential Asset transactions', + description: + 'This endpoint removes the given list of Confidential Venues (if present), from the existing list of allowed Confidential Venues for Confidential Asset Transaction', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.NOT_FOUND]: ['The Confidential Asset does not exists'], + }) + @Post(':confidentialAssetId/venue-filtering/remove-allowed-venues') + public async removeAllowedVenues( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: RemoveAllowedConfidentialVenuesDto + ): Promise { + const { confidentialVenues: disallowedVenues, ...rest } = params; + + const result = await this.confidentialAssetsService.setVenueFilteringDetails( + confidentialAssetId, + { + ...rest, + disallowedVenues, + } + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Freeze all trading for a Confidential Asset', + description: + 'This endpoint freezes all trading for a Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Asset is already frozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':confidentialAssetId/freeze') + async freezeConfidentialAsset( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() body: TransactionBaseDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( + confidentialAssetId, + body, + true + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Resume (unfreeze) all trading for a Confidential Asset', + description: + 'This endpoint resumes all trading for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Asset is already unfrozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':confidentialAssetId/unfreeze') + async unfreezeConfidentialAsset( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() body: TransactionBaseDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAsset( + confidentialAssetId, + body, + false + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Freeze trading for a specific Confidential Account for a Confidential Asset', + description: + 'This endpoint freezes trading for a specific Confidential Account for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Account is already frozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':confidentialAssetId/freeze-account') + async freezeConfidentialAccount( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() body: ToggleFreezeConfidentialAccountAssetDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( + confidentialAssetId, + body, + true + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: + 'Resume (unfreeze) trading for a specific Confidential Account for a Confidential Asset', + description: + 'This endpoint resumes trading for a specific Confidential Account for a freezed Confidential Asset. Note, only the owner of the Confidential asset can perform this operation', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + }) + @ApiTransactionFailedResponse({ + [HttpStatus.BAD_REQUEST]: [ + 'Confidential Account is already unfrozen', + 'The signing identity is not the owner of the Confidential Asset', + ], + }) + @Post(':confidentialAssetId/unfreeze-account') + async unfreezeConfidentialAccount( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() body: ToggleFreezeConfidentialAccountAssetDto + ): Promise { + const result = await this.confidentialAssetsService.toggleFreezeConfidentialAccountAsset( + confidentialAssetId, + body, + false + ); + + return handleServiceResult(result); + } + + @ApiOperation({ + summary: + 'Check whether trading for a Confidential Asset is frozen for a specific Confidential Account', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Indicator to know if the Confidential Account is frozen or not', + type: 'boolean', + }) + @ApiNotFoundResponse({ + description: 'The Confidential Asset does not exists', + }) + @Get(':confidentialAssetId/freeze-account/:confidentialAccount') + async isConfidentialAccountFrozen( + @Param() + { + confidentialAssetId, + confidentialAccount, + }: ConfidentialAssetIdParamsDto & ConfidentialAccountParamsDto + ): Promise { + return this.confidentialAssetsService.isConfidentialAccountFrozen( + confidentialAssetId, + confidentialAccount + ); + } +} diff --git a/src/confidential-assets/confidential-assets.module.ts b/src/confidential-assets/confidential-assets.module.ts new file mode 100644 index 00000000..3db8ca67 --- /dev/null +++ b/src/confidential-assets/confidential-assets.module.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +import { forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; +import { ConfidentialAssetsController } from '~/confidential-assets/confidential-assets.controller'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [ + PolymeshModule, + TransactionsModule, + ConfidentialAccountsModule, + forwardRef(() => ConfidentialProofsModule.register()), + ], + controllers: [ConfidentialAssetsController], + providers: [ConfidentialAssetsService], + exports: [ConfidentialAssetsService], +}) +export class ConfidentialAssetsModule {} diff --git a/src/confidential-assets/confidential-assets.service.spec.ts b/src/confidential-assets/confidential-assets.service.spec.ts new file mode 100644 index 00000000..a09d297a --- /dev/null +++ b/src/confidential-assets/confidential-assets.service.spec.ts @@ -0,0 +1,427 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialVenueFilteringDetails, + EventIdEnum, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialAsset, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, + mockTransactionsProvider, + MockTransactionsService, +} from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; + +describe('ConfidentialAssetsService', () => { + let service: ConfidentialAssetsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + let mockConfidentialAccountsService: DeepMocked; + let mockConfidentialProofsService: DeepMocked; + const id = 'SOME-CONFIDENTIAL-ASSET-ID'; + + beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [PolymeshModule], + providers: [ + ConfidentialAssetsService, + mockTransactionsProvider, + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, + ], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockTransactionsService = module.get(TransactionsService); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + + service = module.get(ConfidentialAssetsService); + }); + + afterEach(async () => { + await polymeshService.close(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return a Confidential Asset for a valid ID', async () => { + const asset = createMockConfidentialAsset(); + mockPolymeshApi.confidentialAssets.getConfidentialAsset.mockResolvedValue(asset); + + const result = await service.findOne(id); + + expect(result).toEqual(asset); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialAssets.getConfidentialAsset.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('createConfidentialAsset', () => { + it('should create the Confidential Asset', async () => { + const input = { + signer, + data: 'SOME_DATA', + auditors: ['AUDITOR_KEY'], + mediators: ['MEDIATOR_DID'], + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateConfidentialAsset, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialAsset(input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); + + describe('issue', () => { + it('should mint Confidential Assets', async () => { + const input = { + signer, + amount: new BigNumber(100), + confidentialAccount: 'SOME_ACCOUNT', + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.MintConfidentialAsset, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockAsset); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.issue(id, input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); + + describe('fetchOwner', () => { + it('should return the owner of Confidential Account', async () => { + const asset = createMockConfidentialAsset(); + const expectedResult: ConfidentialVenueFilteringDetails = { + enabled: false, + }; + asset.getVenueFilteringDetails.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValueOnce(asset); + + const result = await service.getVenueFilteringDetails(id); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('setVenueFiltering', () => { + it('should call the setVenueFiltering procedure and return the results', async () => { + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetVenueFiltering, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + mockTransactionsService.submit.mockResolvedValue({ + transactions: [mockTransaction], + }); + + let result = await service.setVenueFilteringDetails(id, { signer, enabled: true }); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + result = await service.setVenueFilteringDetails(id, { + signer, + allowedVenues: [new BigNumber(1)], + }); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + result = await service.setVenueFilteringDetails(id, { + signer, + disallowedVenues: [new BigNumber(2)], + }); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); + + describe('toggleFreezeConfidentialAsset', () => { + it('should freeze/unfreeze a Confidential Asset', async () => { + const input = { + signer, + processMode: ProcessMode.Submit, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetAssetFrozen, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.freeze, {}, input) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + let result = await service.toggleFreezeConfidentialAsset(id, input, true); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.unfreeze, {}, input) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + result = await service.toggleFreezeConfidentialAsset(id, input, false); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); + + describe('toggleFreezeConfidentialAccountAsset', () => { + it('should freeze/unfreeze a Confidential Account from trading a Confidential Asset', async () => { + const params = { + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + const input = { + signer, + ...params, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.SetAccountAssetFrozen, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.freezeAccount, params, { signer, processMode: ProcessMode.Submit }) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + let result = await service.toggleFreezeConfidentialAccountAsset(id, input, true); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + + when(mockTransactionsService.submit) + .calledWith(mockAsset.unfreezeAccount, params, { signer, processMode: ProcessMode.Submit }) + .mockResolvedValue({ + transactions: [mockTransaction], + }); + + result = await service.toggleFreezeConfidentialAccountAsset(id, input, false); + + expect(result).toEqual({ + transactions: [mockTransaction], + }); + }); + }); + + describe('isConfidentialAccountFrozen', () => { + it('should return whether a given Confidential Account is frozen', async () => { + const asset = createMockConfidentialAsset(); + asset.isAccountFrozen.mockResolvedValue(false); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.isConfidentialAccountFrozen(id, 'SOME_PUBLIC_KEY'); + + expect(result).toEqual(false); + }); + }); + + describe('burnConfidentialAccount', () => { + it('should burn the specified amount of Confidential Assets from given Confidential Account`', async () => { + const params = { + confidentialAccount: 'SOME_PUBLIC_KEY', + amount: new BigNumber(100), + }; + const input = { + signer, + ...params, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.Burn, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockAsset = createMockConfidentialAsset(); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockAsset); + + const encryptedBalance = '0xencryptedbalance'; + when(mockConfidentialAccountsService.getAssetBalance) + .calledWith(params.confidentialAccount, id) + .mockResolvedValue({ balance: encryptedBalance, confidentialAsset: 'SOME_ASSET_ID' }); + + const mockProof = 'some_proof'; + when(mockConfidentialProofsService.generateBurnProof) + .calledWith(params.confidentialAccount, { + amount: params.amount, + encryptedBalance, + }) + .mockResolvedValue(mockProof); + + when(mockTransactionsService.submit) + .calledWith( + mockAsset.burn, + { ...params, proof: mockProof }, + { signer, processMode: ProcessMode.Submit } + ) + .mockResolvedValue({ + result: mockAsset, + transactions: [mockTransaction], + }); + + const result = await service.burnConfidentialAsset(id, input); + + expect(result).toEqual({ + result: mockAsset, + transactions: [mockTransaction], + }); + }); + }); + + describe('createdAt', () => { + it('should return creation event details for a Confidential Asset', async () => { + const mockResult = { + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2023-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }; + const asset = createMockConfidentialAsset(); + + asset.createdAt.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.createdAt('SOME_ASSET_ID'); + + expect(result).toEqual(mockResult); + }); + }); + + describe('transactionHistory', () => { + it('should return transaction history of a Confidential Asset', async () => { + const mockResult = { + data: [ + { + id: '', + assetId: 'someId', + amount: '10', + eventId: EventIdEnum.TransactionExecuted, + datetime: new Date(), + createdBlockId: new BigNumber(3), + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2023-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }, + ], + next: '', + }; + const asset = createMockConfidentialAsset(); + + asset.getTransactionHistory.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(asset); + + const result = await service.transactionHistory('SOME_ASSET_ID', new BigNumber(10)); + + expect(result).toEqual(mockResult); + }); + }); +}); diff --git a/src/confidential-assets/confidential-assets.service.ts b/src/confidential-assets/confidential-assets.service.ts new file mode 100644 index 00000000..d31ef2da --- /dev/null +++ b/src/confidential-assets/confidential-assets.service.ts @@ -0,0 +1,165 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAsset, + ConfidentialAssetTransactionHistory, + ConfidentialVenueFilteringDetails, + EventIdentifier, + ResultSet, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; +import { CreateConfidentialAssetDto } from '~/confidential-assets/dto/create-confidential-asset.dto'; +import { IssueConfidentialAssetDto } from '~/confidential-assets/dto/issue-confidential-asset.dto'; +import { ToggleFreezeConfidentialAccountAssetDto } from '~/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialAssetsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService, + private readonly confidentialProofsService: ConfidentialProofsService, + private readonly confidentialAccountsService: ConfidentialAccountsService + ) {} + + public async findOne(id: string): Promise { + return await this.polymeshService.polymeshApi.confidentialAssets + .getConfidentialAsset({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async createConfidentialAsset( + params: CreateConfidentialAssetDto + ): ServiceReturn { + const { options, args } = extractTxOptions(params); + + const createConfidentialAsset = + this.polymeshService.polymeshApi.confidentialAssets.createConfidentialAsset; + return this.transactionsService.submit(createConfidentialAsset, args, options); + } + + public async issue( + assetId: string, + params: IssueConfidentialAssetDto + ): ServiceReturn { + const { options, args } = extractTxOptions(params); + const asset = await this.findOne(assetId); + + return this.transactionsService.submit(asset.issue, args, options); + } + + public async getVenueFilteringDetails( + assetId: string + ): Promise { + const asset = await this.findOne(assetId); + + return asset.getVenueFilteringDetails(); + } + + public async setVenueFilteringDetails( + assetId: string, + params: TransactionBaseDto & + ( + | { enabled: boolean } + | { + allowedVenues: BigNumber[]; + } + | { disallowedVenues: BigNumber[] } + ) + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { options, args } = extractTxOptions(params); + + return this.transactionsService.submit(asset.setVenueFiltering, args, options); + } + + public async toggleFreezeConfidentialAsset( + assetId: string, + base: TransactionBaseDto, + freeze: boolean + ): ServiceReturn { + const { options } = extractTxOptions(base); + const asset = await this.findOne(assetId); + + const method = freeze ? asset.freeze : asset.unfreeze; + + return this.transactionsService.submit(method, {}, options); + } + + public async toggleFreezeConfidentialAccountAsset( + assetId: string, + params: ToggleFreezeConfidentialAccountAssetDto, + freeze: boolean + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { options, args } = extractTxOptions(params); + + const method = freeze ? asset.freezeAccount : asset.unfreezeAccount; + + return this.transactionsService.submit(method, args, options); + } + + public async isConfidentialAccountFrozen( + assetId: string, + confidentialAccount: string + ): Promise { + const asset = await this.findOne(assetId); + + return asset.isAccountFrozen(confidentialAccount); + } + + public async burnConfidentialAsset( + assetId: string, + params: BurnConfidentialAssetsDto + ): ServiceReturn { + const asset = await this.findOne(assetId); + + const { options, args } = extractTxOptions(params); + + const { balance: encryptedBalance } = await this.confidentialAccountsService.getAssetBalance( + args.confidentialAccount, + assetId + ); + + const proof = await this.confidentialProofsService.generateBurnProof(args.confidentialAccount, { + amount: args.amount, + encryptedBalance, + }); + + return this.transactionsService.submit( + asset.burn, + { + ...args, + proof, + }, + options + ); + } + + public async createdAt(assetId: string): Promise { + const asset = await this.findOne(assetId); + + return asset.createdAt(); + } + + public async transactionHistory( + assetId: string, + size: BigNumber, + start?: BigNumber + ): Promise> { + const asset = await this.findOne(assetId); + + return asset.getTransactionHistory({ size, start }); + } +} diff --git a/src/confidential-assets/confidential-assets.util.ts b/src/confidential-assets/confidential-assets.util.ts new file mode 100644 index 00000000..2b4a9731 --- /dev/null +++ b/src/confidential-assets/confidential-assets.util.ts @@ -0,0 +1,34 @@ +/* istanbul ignore file */ + +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAssetDetailsModel } from '~/confidential-assets/models/confidential-asset-details.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; + +/** + * Fetch and assemble data for an Confidential Asset + */ +export async function createConfidentialAssetDetailsModel( + asset: ConfidentialAsset +): Promise { + const [details, { auditors, mediators }, isFrozen] = await Promise.all([ + asset.details(), + asset.getAuditors(), + asset.isFrozen(), + ]); + + return new ConfidentialAssetDetailsModel({ + ...details, + isFrozen, + auditors: auditors.map(({ publicKey }) => new ConfidentialAccountModel({ publicKey })), + mediators: mediators.map(({ did }) => new IdentityModel({ did })), + }); +} + +export function createConfidentialAssetModel(asset: ConfidentialAsset): ConfidentialAssetModel { + const { id } = asset; + + return new ConfidentialAssetModel({ id }); +} diff --git a/src/confidential-assets/decorators/validation.ts b/src/confidential-assets/decorators/validation.ts new file mode 100644 index 00000000..f541023b --- /dev/null +++ b/src/confidential-assets/decorators/validation.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { applyDecorators } from '@nestjs/common'; +import { Length, Matches, ValidationOptions } from 'class-validator'; + +import { ASSET_ID_LENGTH } from '~/confidential-assets/confidential-assets.consts'; + +export function IsConfidentialAssetId(validationOptions?: ValidationOptions) { + return applyDecorators( + Length(ASSET_ID_LENGTH, undefined, { + ...validationOptions, + message: `ID must be ${ASSET_ID_LENGTH} characters long`, + }), + Matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { + ...validationOptions, + message: 'ID is not a valid confidential Asset ID', + }) + ); +} diff --git a/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts new file mode 100644 index 00000000..84c3699b --- /dev/null +++ b/src/confidential-assets/dto/add-allowed-confidential-venues.dto.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class AddAllowedConfidentialVenuesDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'List of confidential Venues to be allowed to create confidential Transactions for a specific Confidential Asset', + isArray: true, + type: 'string', + example: ['1', '2'], + }) + @ToBigNumber() + @IsBigNumber() + readonly confidentialVenues: BigNumber[]; +} diff --git a/src/confidential-assets/dto/burn-confidential-assets.dto.ts b/src/confidential-assets/dto/burn-confidential-assets.dto.ts new file mode 100644 index 00000000..3be0d7da --- /dev/null +++ b/src/confidential-assets/dto/burn-confidential-assets.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { IsString } from 'class-validator'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class BurnConfidentialAssetsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The amount of Confidential Assets to be burned', + example: '100', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: "The asset issuer's Confidential Account to burn the Confidential Assets from", + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-assets/dto/confidential-asset-id-params.dto.ts b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts new file mode 100644 index 00000000..c38b1c42 --- /dev/null +++ b/src/confidential-assets/dto/confidential-asset-id-params.dto.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; + +export class ConfidentialAssetIdParamsDto { + @IsConfidentialAssetId() + readonly confidentialAssetId: string; +} diff --git a/src/confidential-assets/dto/create-confidential-asset.dto.ts b/src/confidential-assets/dto/create-confidential-asset.dto.ts new file mode 100644 index 00000000..701cfd6a --- /dev/null +++ b/src/confidential-assets/dto/create-confidential-asset.dto.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; + +import { IsDid } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class CreateConfidentialAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Custom data to be associated with the Confidential Asset', + example: 'Some Random Data', + type: 'string', + }) + @IsString() + readonly data: string; + + @ApiProperty({ + description: + 'List of ElGamal public keys required to be included for all proofs related to the asset. The related private keys will be able to decrypt all transactions involving the Confidential Asset', + isArray: true, + type: 'string', + example: ['0x504aa5aa9f1e446e8f933eefb03c52f4bd6d47770892d5e18a1085ee2010a247'], + }) + @IsArray() + @IsString({ each: true }) + readonly auditors: string[]; + + @ApiPropertyOptional({ + description: 'List of mediator DIDs for the Confidential Asset', + isArray: true, + type: 'string', + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], + }) + @IsOptional() + @IsArray() + @IsDid({ each: true }) + readonly mediators?: string[]; +} diff --git a/src/confidential-assets/dto/issue-confidential-asset.dto.ts b/src/confidential-assets/dto/issue-confidential-asset.dto.ts new file mode 100644 index 00000000..17c68673 --- /dev/null +++ b/src/confidential-assets/dto/issue-confidential-asset.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { IsString } from 'class-validator'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class IssueConfidentialAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: 'The amount of the Confidential Asset to issue', + example: '1000', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: "The asset issuer's Confidential Account to receive the minted Assets", + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts new file mode 100644 index 00000000..a754d0cf --- /dev/null +++ b/src/confidential-assets/dto/remove-allowed-confidential-venues.dto.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class RemoveAllowedConfidentialVenuesDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'List of Confidential Venues to be removed from the allowed list of Confidential Venues for handling Confidential Asset transactions', + isArray: true, + type: 'string', + example: ['3'], + }) + @ToBigNumber() + @IsBigNumber() + readonly confidentialVenues: BigNumber[]; +} diff --git a/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts new file mode 100644 index 00000000..96756ac8 --- /dev/null +++ b/src/confidential-assets/dto/set-confidential-venue-filtering-params.dto.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean } from 'class-validator'; + +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class SetConfidentialVenueFilteringParamsDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Indicator to enable/disable when filtering', + type: 'boolean', + example: false, + }) + @IsBoolean() + readonly enabled: boolean; +} diff --git a/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts new file mode 100644 index 00000000..b6492b67 --- /dev/null +++ b/src/confidential-assets/dto/toggle-freeze-confidential-account-asset.dto.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class ToggleFreezeConfidentialAccountAssetDto extends TransactionBaseDto { + @ApiProperty({ + description: + 'The Confidential Account for which trading for a specific confidential asset is being modified', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + type: 'string', + }) + @IsString() + readonly confidentialAccount: string; +} diff --git a/src/confidential-assets/models/confidential-asset-details.model.ts b/src/confidential-assets/models/confidential-asset-details.model.ts new file mode 100644 index 00000000..acbc8e23 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset-details.model.ts @@ -0,0 +1,63 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Identity } from '@polymeshassociation/polymesh-private-sdk/types'; +import { Type } from 'class-transformer'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { + FromBigNumber, + FromEntity, +} from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialAssetDetailsModel { + @ApiProperty({ + description: 'The DID of the Confidential Asset owner', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @FromEntity() + readonly owner: Identity; + + @ApiProperty({ + description: 'Custom data associated with the Confidential Asset', + type: 'string', + example: 'Random Data', + }) + readonly data: string; + + @ApiProperty({ + description: 'Total supply count of the Asset', + type: 'string', + example: '1000', + }) + @FromBigNumber() + readonly totalSupply: BigNumber; + + @ApiProperty({ + description: 'Whether trading is frozen for the Confidential Asset', + type: 'boolean', + example: true, + }) + readonly isFrozen: boolean; + + @ApiProperty({ + description: 'Auditor Confidential Accounts configured for the Confidential Asset', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly auditors: ConfidentialAccountModel[]; + + @ApiPropertyOptional({ + description: 'Mediator Identities configured for the Confidential Asset', + type: IdentityModel, + }) + @Type(() => IdentityModel) + readonly mediators?: IdentityModel[]; + + constructor(model: ConfidentialAssetDetailsModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/confidential-asset-transaction.model.ts b/src/confidential-assets/models/confidential-asset-transaction.model.ts new file mode 100644 index 00000000..ef8ca4f3 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset-transaction.model.ts @@ -0,0 +1,69 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialAssetTransactionModel { + @ApiProperty({ + description: 'The confidential asset ID', + type: 'string', + example: '0x0a732f0ea43bb082ff1cff9a9ff59291', + }) + readonly assetId: string; + + @ApiPropertyOptional({ + description: 'The DID from which the transaction originated', + type: 'string', + }) + readonly fromId?: string; + + @ApiPropertyOptional({ + description: 'The DID for which the asset was sent to', + type: 'string', + example: '0x786a5b0ffef119dd43565768a3557e7880be8958c7eda070e4162b27f308b23e', + nullable: true, + }) + readonly toId?: string; + + @ApiProperty({ + description: 'The encrypted amount of the transaction', + type: 'string', + example: + '0x000000000000000000000000000000000000000000000000000000000000000064aff78e09b0fa5dccd82b594cd49d431d0fbf8ddd6830e65a0cdcd428d67428', + }) + readonly amount: string; + + @ApiProperty({ + description: 'The time the transaction took place', + type: 'string', + example: '2024-02-20T13:15:54', + }) + readonly datetime: Date; + + @ApiProperty({ + description: 'The created block id', + type: 'string', + example: '277', + }) + @FromBigNumber() + readonly createdBlockId: BigNumber; + + @ApiProperty({ + description: 'The event id associated with the transaction record', + type: 'string', + example: 'AccountDeposit', + }) + readonly eventId: string; + + @ApiPropertyOptional({ + description: 'The memo', + type: 'string', + }) + readonly memo?: string; + + constructor(model: ConfidentialAssetTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/confidential-asset.model.ts b/src/confidential-assets/models/confidential-asset.model.ts new file mode 100644 index 00000000..454b3264 --- /dev/null +++ b/src/confidential-assets/models/confidential-asset.model.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfidentialAssetModel { + @ApiProperty({ + description: 'The ID of the confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly id: string; + + constructor(model: ConfidentialAssetModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/confidential-venue-filtering-details.model.ts b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts new file mode 100644 index 00000000..3fed2467 --- /dev/null +++ b/src/confidential-assets/models/confidential-venue-filtering-details.model.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { FromEntityObject } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialVenueFilteringDetailsModel { + @ApiProperty({ + description: 'Indicates whether venue filtering is enabled or not', + type: 'boolean', + example: 'true', + }) + readonly enabled: boolean; + + @ApiProperty({ + description: + 'List of allowed confidential Venues. This value is present only if `enabled` is true', + type: 'string', + example: ['1', '2'], + isArray: true, + }) + @FromEntityObject() + readonly allowedConfidentialVenues?: ConfidentialVenue[]; + + constructor(model: ConfidentialVenueFilteringDetailsModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-assets/models/created-confidential-asset.model.ts b/src/confidential-assets/models/created-confidential-asset.model.ts new file mode 100644 index 00000000..eff4f253 --- /dev/null +++ b/src/confidential-assets/models/created-confidential-asset.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialAsset } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; + +export class CreatedConfidentialAssetModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created confidential Asset', + example: '123', + }) + @FromEntity() + readonly confidentialAsset: ConfidentialAsset; + + constructor(model: CreatedConfidentialAssetModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts new file mode 100644 index 00000000..15f947d5 --- /dev/null +++ b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.spec.ts @@ -0,0 +1,91 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountsMiddlewareController } from '~/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { createMockConfidentialAsset, createMockConfidentialTransaction } from '~/test-utils/mocks'; +import { mockConfidentialAccountsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialAccountsMiddlewareController', () => { + let controller: ConfidentialAccountsMiddlewareController; + let mockConfidentialAccountsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAccountsMiddlewareController], + providers: [mockConfidentialAccountsServiceProvider], + }).compile(); + + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + + controller = module.get( + ConfidentialAccountsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getHeldAssets', () => { + it('should return a paginated list of held Confidential Assets', async () => { + const mockAssets = { + data: [ + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_1' }), + createMockConfidentialAsset({ id: 'SOME_ASSET_ID_2' }), + ], + next: new BigNumber(2), + count: new BigNumber(2), + }; + + mockConfidentialAccountsService.findHeldAssets.mockResolvedValue(mockAssets); + + const result = await controller.getHeldAssets( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { start: new BigNumber(0), size: new BigNumber(2) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining([{ id: 'SOME_ASSET_ID_2' }, { id: 'SOME_ASSET_ID_2' }]), + total: new BigNumber(mockAssets.count), + next: mockAssets.next, + }) + ); + }); + }); + + describe('getAssociatedTransactions', () => { + it('should return the transactions associated with a given Confidential Account', async () => { + const mockResult = { + data: [createMockConfidentialTransaction()], + next: new BigNumber(1), + count: new BigNumber(1), + }; + + mockConfidentialAccountsService.getAssociatedTransactions.mockResolvedValue(mockResult); + + const result = await controller.getAssociatedTransactions( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + size: new BigNumber(1), + start: new BigNumber(0), + direction: ConfidentialTransactionDirectionEnum.All, + } + ); + + expect(result).toEqual( + expect.objectContaining({ + results: mockResult.data, + next: mockResult.next, + total: mockResult.count, + }) + ); + }); + }); +}); diff --git a/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts new file mode 100644 index 00000000..35273d36 --- /dev/null +++ b/src/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller.ts @@ -0,0 +1,107 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/internal'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialAccountTransactionsDto } from '~/confidential-middleware/dto/confidential-account-transaction-params.dto'; +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { ApiArrayResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; + +@ApiTags('confidential-accounts') +@Controller() +export class ConfidentialAccountsMiddlewareController { + constructor(private readonly confidentialAccountsService: ConfidentialAccountsService) {} + + @ApiTags('confidential-assets') + @ApiOperation({ + summary: 'Fetch all Confidential Assets held by a Confidential Account', + description: + 'This endpoint returns a list of all Confidential Assets which were held at one point by the given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse(ConfidentialAssetModel, { + description: 'List of all the held Confidential Assets', + paginated: true, + }) + @Get('confidential-accounts/:confidentialAccount/held-confidential-assets') + public async getHeldAssets( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.findHeldAssets( + confidentialAccount, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data.map(({ id }) => new ConfidentialAssetModel({ id })), + total: count, + next, + }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Get the transactions associated to a Confidential Account', + description: + 'This endpoint provides a list of transactions associated to a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiQuery({ + name: 'direction', + description: 'The direction of the transactions with respect to the given Confidential Account', + type: 'string', + enum: ConfidentialTransactionDirectionEnum, + example: ConfidentialTransactionDirectionEnum.All, + }) + @ApiQuery({ + name: 'size', + description: 'The number of transactions to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start key from which transactions are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'The confidential account was not found', + }) + @Get('confidential-accounts/:confidentialAccount/associated-transactions') + async getAssociatedTransactions( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Query() { size, start, direction }: ConfidentialAccountTransactionsDto + ): Promise> { + const { data, count, next } = await this.confidentialAccountsService.getAssociatedTransactions( + confidentialAccount, + direction, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data, + total: count, + next, + }); + } +} diff --git a/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts new file mode 100644 index 00000000..e2a543e8 --- /dev/null +++ b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.spec.ts @@ -0,0 +1,97 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { EventIdEnum } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialAssetsMiddlewareController } from '~/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; +import { mockConfidentialAssetsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialAssetsMiddlewareController', () => { + let controller: ConfidentialAssetsMiddlewareController; + let mockConfidentialAssetsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialAssetsMiddlewareController], + providers: [mockConfidentialAssetsServiceProvider], + }).compile(); + + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); + + controller = module.get( + ConfidentialAssetsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createdAt', () => { + it('should throw AppNotFoundError if the event details are not yet ready', () => { + mockConfidentialAssetsService.createdAt.mockResolvedValue(null); + + return expect(() => + controller.createdAt({ confidentialAssetId: 'SOME_ASSET_ID' }) + ).rejects.toBeInstanceOf(NotFoundException); + }); + + describe('otherwise', () => { + it('should return the Portfolio creation event details', async () => { + const eventIdentifier = { + blockNumber: new BigNumber('2719172'), + blockHash: 'someHash', + blockDate: new Date('2021-06-26T01:47:45.000Z'), + eventIndex: new BigNumber(1), + }; + mockConfidentialAssetsService.createdAt.mockResolvedValue(eventIdentifier); + + const result = await controller.createdAt({ confidentialAssetId: 'SOME_ASSET_ID' }); + + expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); + }); + }); + }); + + describe('getTransactionHistory', () => { + it('should return the transaction history', async () => { + const mockResult = { + data: [ + { + id: 'someId', + assetId: '0x0a732f0ea43bb082ff1cff9a9ff59291', + fromId: 'mockFrom', + toId: '0x786a5b0ffef119dd43565768a3557e7880be8958c7eda070e4162b27f308b23e', + amount: + '0x000000000000000000000000000000000000000000000000000000000000000064aff78e09b0fa5dccd82b594cd49d431d0fbf8ddd6830e65a0cdcd428d67428', + datetime: new Date('2024-02-20T13:15:54'), + createdBlockId: new BigNumber(277), + eventId: EventIdEnum.AccountDeposit, + memo: 'someMemo', + }, + ], + next: 'abc', + count: new BigNumber(1), + }; + + mockConfidentialAssetsService.transactionHistory.mockResolvedValue(mockResult); + + const result = await controller.getTransactionHistory( + { confidentialAssetId: 'SOME_ASSET_ID' }, + { size: new BigNumber(10) } + ); + + expect(result).toEqual( + expect.objectContaining({ + results: mockResult.data, + next: mockResult.next, + total: mockResult.count, + }) + ); + }); + }); +}); diff --git a/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts new file mode 100644 index 00000000..ecc89e81 --- /dev/null +++ b/src/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller.ts @@ -0,0 +1,101 @@ +import { Controller, Get, NotFoundException, Param, Query } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { ConfidentialAssetTransactionModel } from '~/confidential-assets/models/confidential-asset-transaction.model'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; + +@ApiTags('confidential-assets') +@Controller() +export class ConfidentialAssetsMiddlewareController { + constructor(private readonly confidentialAssetsService: ConfidentialAssetsService) {} + + @ApiOperation({ + summary: 'Get creation event data for a Confidential Asset', + description: + 'This endpoint will provide the basic details of an Confidential Asset along with the auditors information', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Details of event where the Confidential Asset was created', + type: EventIdentifierModel, + }) + @ApiNotFoundResponse({ + description: 'Data is not yet processed by the middleware', + }) + @Get('confidential-assets/:confidentialAssetId/created-at') + public async createdAt( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto + ): Promise { + const result = await this.confidentialAssetsService.createdAt(confidentialAssetId); + + if (!result) { + throw new NotFoundException( + "Confidential Asset's data hasn't yet been processed by the middleware" + ); + } + + return new EventIdentifierModel(result); + } + + @ApiTags('confidential-assets') + @ApiOperation({ + summary: 'Get transaction history of a Confidential Asset', + description: 'This endpoint provides a list of transactions involving a Confidential Asset', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiQuery({ + name: 'size', + description: 'The number of transactions to be fetched', + type: 'string', + required: false, + example: '10', + }) + @ApiQuery({ + name: 'start', + description: 'Start key from which transactions are to be fetched', + type: 'string', + required: false, + }) + @ApiNotFoundResponse({ + description: 'The confidential asset was not found', + }) + @Get('confidential-assets/:confidentialAssetId/transactions') + async getTransactionHistory( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { data, count, next } = await this.confidentialAssetsService.transactionHistory( + confidentialAssetId, + size, + new BigNumber(start || 0) + ); + + return new PaginatedResultsModel({ + results: data.map(txHistory => new ConfidentialAssetTransactionModel(txHistory)), + total: count, + next, + }); + } +} diff --git a/src/confidential-middleware/confidential-middleware.module.ts b/src/confidential-middleware/confidential-middleware.module.ts new file mode 100644 index 00000000..1e639bfd --- /dev/null +++ b/src/confidential-middleware/confidential-middleware.module.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ + +import { DynamicModule, forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialAccountsMiddlewareController } from '~/confidential-middleware/confidential-accounts-middleware/confidential-accounts-middleware.controller'; +import { ConfidentialAssetsMiddlewareController } from '~/confidential-middleware/confidential-assets-middleware/confidential-assets-middleware.controller'; +import { ConfidentialTransactionsMiddlewareController } from '~/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; + +@Module({}) +export class ConfidentialMiddlewareModule { + static register(): DynamicModule { + const controllers = []; + + const middlewareUrl = process.env.POLYMESH_MIDDLEWARE_V2_URL || ''; + + if (middlewareUrl.length) { + controllers.push(ConfidentialAssetsMiddlewareController); + controllers.push(ConfidentialAccountsMiddlewareController); + controllers.push(ConfidentialTransactionsMiddlewareController); + } + + return { + module: ConfidentialMiddlewareModule, + imports: [ + forwardRef(() => ConfidentialAssetsModule), + forwardRef(() => ConfidentialAccountsModule), + forwardRef(() => ConfidentialTransactionsModule), + ], + controllers, + providers: [], + exports: [], + }; + } +} diff --git a/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts new file mode 100644 index 00000000..621be593 --- /dev/null +++ b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.spec.ts @@ -0,0 +1,59 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialTransactionsMiddlewareController } from '~/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; + +describe('ConfidentialTransactionsMiddlewareController', () => { + let controller: ConfidentialTransactionsMiddlewareController; + let mockConfidentialTransactionsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialTransactionsMiddlewareController], + providers: [mockConfidentialTransactionsServiceProvider], + }).compile(); + + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + + controller = module.get( + ConfidentialTransactionsMiddlewareController + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('createdAt', () => { + it('should throw AppNotFoundError if the event details are not yet ready', () => { + mockConfidentialTransactionsService.createdAt.mockResolvedValue(null); + + return expect(() => controller.createdAt({ id: new BigNumber(99) })).rejects.toBeInstanceOf( + NotFoundException + ); + }); + + describe('otherwise', () => { + it('should return the Portfolio creation event details', async () => { + const eventIdentifier = { + blockDate: new Date('2021-06-26T01:47:45.000Z'), + blockNumber: new BigNumber('2719172'), + eventIndex: new BigNumber(1), + blockHash: 'someHash', + }; + mockConfidentialTransactionsService.createdAt.mockResolvedValue(eventIdentifier); + + const result = await controller.createdAt({ id: new BigNumber(10) }); + + expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); + }); + }); + }); +}); diff --git a/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts new file mode 100644 index 00000000..3b763d80 --- /dev/null +++ b/src/confidential-middleware/confidential-transactions-middleware/confidential-transactions-middleware.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, NotFoundException, Param } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { EventIdentifierModel } from '~/polymesh-rest-api/src/common/models/event-identifier.model'; + +@ApiTags('confidential-transactions') +@Controller() +export class ConfidentialTransactionsMiddlewareController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiOperation({ + summary: 'Get creation event data for a Confidential Transaction', + description: + 'The endpoint retrieves the identifier data (block number, date and event index) of the event that was emitted when the given Confidential Transaction was created', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '10', + }) + @ApiOkResponse({ + description: 'Details of event where the Confidential Transaction was created', + type: EventIdentifierModel, + }) + @ApiNotFoundResponse({ + description: 'Data is not yet processed by the middleware', + }) + @Get('confidential-transactions/:id/created-at') + public async createdAt(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.createdAt(id); + + if (!result) { + throw new NotFoundException( + "Confidential Transaction's data hasn't yet been processed by the middleware" + ); + } + + return new EventIdentifierModel(result); + } +} diff --git a/src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts b/src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts new file mode 100644 index 00000000..c35c81b0 --- /dev/null +++ b/src/confidential-middleware/dto/confidential-account-transaction-params.dto.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { IsEnum } from 'class-validator'; + +import { ConfidentialTransactionDirectionEnum } from '~/confidential-transactions/types'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; + +export class ConfidentialAccountTransactionsDto extends PaginatedParamsDto { + @IsEnum(ConfidentialTransactionDirectionEnum) + readonly direction: ConfidentialTransactionDirectionEnum; +} diff --git a/src/confidential-proofs/confidential-proofs.controller.spec.ts b/src/confidential-proofs/confidential-proofs.controller.spec.ts new file mode 100644 index 00000000..e4b81b2f --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.controller.spec.ts @@ -0,0 +1,210 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAsset, + ConfidentialTransaction, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { testValues, txResult } from '~/test-utils/consts'; +import { + mockConfidentialAssetsServiceProvider, + mockConfidentialProofsServiceProvider, + mockConfidentialTransactionsServiceProvider, +} from '~/test-utils/service-mocks'; + +const { signer } = testValues; + +describe('ConfidentialProofsController', () => { + let controller: ConfidentialProofsController; + let mockConfidentialProofsService: DeepMocked; + let mockConfidentialTransactionsService: DeepMocked; + let mockConfidentialAssetsService: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialProofsController], + providers: [ + mockConfidentialProofsServiceProvider, + mockConfidentialTransactionsServiceProvider, + mockConfidentialAssetsServiceProvider, + ], + }).compile(); + + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + mockConfidentialAssetsService = + module.get(ConfidentialAssetsService); + controller = module.get(ConfidentialProofsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getAccounts', () => { + it('should get the owner of a Confidential Account', async () => { + when(mockConfidentialProofsService.getConfidentialAccounts) + .calledWith() + .mockResolvedValue([ + { + confidentialAccount: 'SOME_PUBLIC_KEY', + } as ConfidentialAccountEntity, + ]); + + const result = await controller.getAccounts(); + + expect(result).toEqual([new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })]); + }); + }); + + describe('createAccount', () => { + it('should call the service and return the results', async () => { + const mockAccount = { + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + + mockConfidentialProofsService.createConfidentialAccount.mockResolvedValue( + mockAccount as unknown as ConfidentialAccountEntity + ); + + const result = await controller.createAccount(); + + expect(result).toEqual(new ConfidentialAccountModel({ publicKey: 'SOME_PUBLIC_KEY' })); + }); + }); + + describe('senderAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legId: new BigNumber(0), + legAmounts: [ + { + confidentialAsset: 'SOME_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.senderAffirmLeg) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.senderAffirmLeg({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('verifySenderProofAsAuditor', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }; + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue(mockResponse); + + const result = await controller.verifySenderProofAsAuditor( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + amount: new BigNumber(10), + auditorId: new BigNumber(1), + senderProof: '0xproof', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('verifySenderProofAsReceiver', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }; + + mockConfidentialProofsService.verifySenderProofAsReceiver.mockResolvedValue(mockResponse); + + const result = await controller.verifySenderProofAsReceiver( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + amount: new BigNumber(10), + senderProof: '0xproof', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('decryptBalance', () => { + it('should call the service and return the results', async () => { + const mockResponse = { + value: new BigNumber(10), + }; + + mockConfidentialProofsService.decryptBalance.mockResolvedValue(mockResponse); + + const result = await controller.decryptBalance( + { confidentialAccount: 'SOME_PUBLIC_KEY' }, + { + encryptedValue: '0xsomebalance', + } + ); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('burnConfidentialAsset', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + amount: new BigNumber(1), + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + + const confidentialAssetId = 'SOME_ASSET_ID'; + + when(mockConfidentialAssetsService.burnConfidentialAsset) + .calledWith(confidentialAssetId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.burnConfidentialAsset({ confidentialAssetId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('auditorVerifyTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + publicKey: 'SOME_PUBLIC_KEY', + }; + const id = new BigNumber(1); + + when(mockConfidentialTransactionsService.verifyTransactionAmounts) + .calledWith(id, input) + .mockResolvedValue([]); + + const result = await controller.verifyAmounts({ id }, input); + expect(result).toEqual({ verifications: [] }); + }); + }); +}); diff --git a/src/confidential-proofs/confidential-proofs.controller.ts b/src/confidential-proofs/confidential-proofs.controller.ts new file mode 100644 index 00000000..948704bd --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.controller.ts @@ -0,0 +1,255 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ApiInternalServerErrorResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { ConfidentialAccountParamsDto } from '~/confidential-accounts/dto/confidential-account-params.dto'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { BurnConfidentialAssetsDto } from '~/confidential-assets/dto/burn-confidential-assets.dto'; +import { ConfidentialAssetIdParamsDto } from '~/confidential-assets/dto/confidential-asset-id-params.dto'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; +import { AuditorVerifyTransactionModel } from '~/confidential-proofs/models/auditor-verify-transaction.model'; +import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; +import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; + +@Controller() +export class ConfidentialProofsController { + constructor( + private readonly confidentialProofsService: ConfidentialProofsService, + private readonly confidentialTransactionsService: ConfidentialTransactionsService, + private readonly confidentialAssetsService: ConfidentialAssetsService + ) {} + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Get all Confidential Accounts', + description: + 'This endpoint retrieves the list of all Confidential Accounts created on the Proof Server. Note, this needs the `PROOF_SERVER_URL` to be set in the environment', + }) + @ApiOkResponse({ + description: 'List of Confidential Accounts', + type: ConfidentialAccountModel, + isArray: true, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server API is not set', + }) + @Get('confidential-accounts') + public async getAccounts(): Promise { + const result = await this.confidentialProofsService.getConfidentialAccounts(); + + return result.map( + ({ confidentialAccount: publicKey }) => new ConfidentialAccountModel({ publicKey }) + ); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Create a Confidential Account', + description: + 'This endpoint creates a new Confidential Account (ElGamal key pair) on the proof server. Note, this needs the `PROOF_SERVER_URL` to be set in the environment', + }) + @ApiOkResponse({ + description: 'Public key of the newly created Confidential Account (ElGamal key pair)', + type: ConfidentialAccountModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/create') + public async createAccount(): Promise { + const { confidentialAccount: publicKey } = + await this.confidentialProofsService.createConfidentialAccount(); + + return new ConfidentialAccountModel({ publicKey }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction as a Sender', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction for the Sender. Note, this needs the `PROOF_SERVER_URL` to be set in the environment in order to generate the sender proof', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-transactions/:id/affirm-leg/sender') + public async senderAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: SenderAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.senderAffirmLeg(id, body); + return handleServiceResult(result); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Verify all sender proofs of a transaction as an auditor', + description: + 'This endpoint will verify all asset amounts for legs which have been proven by their sender, and for which the auditor was included', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be verified', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The proof verification responses for each leg and asset', + type: AuditorVerifyProofModel, + isArray: true, + }) + @ApiNotFoundResponse({ + description: 'Transaction was not found', + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-transactions/:id/verify-amounts') + public async verifyAmounts( + @Param() { id }: IdParamsDto, + @Body() body: VerifyTransactionAmountsDto + ): Promise { + const verifications = await this.confidentialTransactionsService.verifyTransactionAmounts( + id, + body + ); + + return new AuditorVerifyTransactionModel({ verifications }); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Verify a sender proof as an auditor', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Auditor Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Details about the verification', + type: SenderProofVerificationResponseModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/auditor-verify') + public async verifySenderProofAsAuditor( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: AuditorVerifySenderProofDto + ): Promise { + return this.confidentialProofsService.verifySenderProofAsAuditor(confidentialAccount, params); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Verify a sender proof as a receiver', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the receiver Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Details about the verification', + type: SenderProofVerificationResponseModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/receiver-verify') + public async verifySenderProofAsReceiver( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: ReceiverVerifySenderProofDto + ): Promise { + return this.confidentialProofsService.verifySenderProofAsReceiver(confidentialAccount, params); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Decrypts an encrypted balance for a Confidential Account', + }) + @ApiParam({ + name: 'confidentialAccount', + description: 'The public key of the Confidential Account', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @ApiOkResponse({ + description: 'Decrypted balance value', + type: DecryptedBalanceModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-accounts/:confidentialAccount/decrypt-balance') + public async decryptBalance( + @Param() { confidentialAccount }: ConfidentialAccountParamsDto, + @Body() params: DecryptBalanceDto + ): Promise { + return this.confidentialProofsService.decryptBalance(confidentialAccount, params); + } + + @ApiTags('confidential-accounts') + @ApiOperation({ + summary: 'Burn Confidential Assets', + description: + 'This endpoints allows to burn a specific amount of Confidential Assets from a given Confidential Account', + }) + @ApiParam({ + name: 'confidentialAssetId', + description: 'The ID of the Confidential Asset to be burned', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @ApiOkResponse({ + description: 'Decrypted balance value', + type: DecryptedBalanceModel, + }) + @ApiInternalServerErrorResponse({ + description: 'Proof server returned a non-OK status', + }) + @Post('confidential-assets/:confidentialAssetId/burn') + public async burnConfidentialAsset( + @Param() { confidentialAssetId }: ConfidentialAssetIdParamsDto, + @Body() params: BurnConfidentialAssetsDto + ): Promise { + const result = await this.confidentialAssetsService.burnConfidentialAsset( + confidentialAssetId, + params + ); + return handleServiceResult(result); + } +} diff --git a/src/confidential-proofs/confidential-proofs.module.ts b/src/confidential-proofs/confidential-proofs.module.ts new file mode 100644 index 00000000..facffa53 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.module.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ + +import { HttpModule } from '@nestjs/axios'; +import { DynamicModule, forwardRef, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { ConfidentialAssetsModule } from '~/confidential-assets/confidential-assets.module'; +import { ConfidentialProofsController } from '~/confidential-proofs/confidential-proofs.controller'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { LoggerModule } from '~/polymesh-rest-api/src/logger/logger.module'; + +@Module({}) +export class ConfidentialProofsModule { + static register(): DynamicModule { + const controllers = []; + + const proofServerUrl = process.env.PROOF_SERVER_URL || ''; + + if (proofServerUrl.length) { + controllers.push(ConfidentialProofsController); + } + + return { + module: ConfidentialProofsModule, + imports: [ + ConfigModule.forFeature(confidentialProofsConfig), + HttpModule, + LoggerModule, + forwardRef(() => ConfidentialTransactionsModule), + forwardRef(() => ConfidentialAssetsModule), + ], + controllers, + providers: [ConfidentialProofsService], + exports: [ConfidentialProofsService], + }; + } +} diff --git a/src/confidential-proofs/confidential-proofs.service.spec.ts b/src/confidential-proofs/confidential-proofs.service.spec.ts new file mode 100644 index 00000000..d07f3ad4 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.service.spec.ts @@ -0,0 +1,269 @@ +/* eslint-disable import/first */ +const mockLastValueFrom = jest.fn(); + +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { mockPolymeshLoggerProvider } from '~/polymesh-rest-api/src/logger/mock-polymesh-logger'; +import { MockHttpService } from '~/test-utils/service-mocks'; + +jest.mock('rxjs', () => ({ + ...jest.requireActual('rxjs'), + lastValueFrom: mockLastValueFrom, +})); + +describe('ConfidentialProofsService', () => { + let service: ConfidentialProofsService; + let mockHttpService: MockHttpService; + const proofServerUrl = 'https://some-api.com/api/v1'; + + beforeEach(async () => { + mockHttpService = new MockHttpService(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ConfidentialProofsService, + HttpService, + mockPolymeshLoggerProvider, + { + provide: confidentialProofsConfig.KEY, + useValue: { proofServerUrl }, + }, + ], + }) + .overrideProvider(HttpService) + .useValue(mockHttpService) + .compile(); + + service = module.get(ConfidentialProofsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getConfidentialAccounts', () => { + it('should throw an error if status is not OK', async () => { + mockLastValueFrom.mockReturnValue({ + status: 400, + }); + + await expect(service.getConfidentialAccounts()).rejects.toThrow( + 'Proof server responded with non-OK status: 400' + ); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts`, + method: 'GET', + timeout: 10000, + }); + }); + + it('should return all the Confidential Accounts from proof server', async () => { + const mockResult = [ + { + confidentialAccount: 'SOME_PUBLIC_KEY', + }, + ]; + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.getConfidentialAccounts(); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts`, + method: 'GET', + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); + + describe('createConfidentialAccount', () => { + it('should return create a new confidential account in proof server', async () => { + const mockResult = { + confidentialAccount: 'SOME_PUBLIC_KEY', + }; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.createConfidentialAccount(); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts`, + method: 'POST', + data: {}, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); + + describe('generateSenderProof', () => { + it('should return generated sender proof', async () => { + const mockResult = 'some_proof'; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.generateSenderProof('confidentialAccount', { + amount: new BigNumber(100), + auditors: ['auditor'], + receiver: 'receiver', + encryptedBalance: '0xencryptedBalance', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/send`, + method: 'POST', + data: { + amount: 100, + auditors: ['auditor'], + receiver: 'receiver', + encrypted_balance: '0xencryptedBalance', + }, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); + + describe('verifySenderProofAsAuditor', () => { + it('should return verify sender proof as an auditor', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + is_valid: true, + amount: 10, + errMsg: null, + }, + }); + + const result = await service.verifySenderProofAsAuditor('confidentialAccount', { + amount: new BigNumber(10), + auditorId: new BigNumber(1), + senderProof: '0xsomeproof', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/auditor_verify`, + method: 'POST', + data: { + amount: 10, + auditor_id: 1, + sender_proof: '0xsomeproof', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + isValid: true, + amount: new BigNumber(10), + errMsg: null, + }); + }); + }); + + describe('verifySenderProofAsReceiver', () => { + it('should return verify sender proof as a receiver', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + is_valid: true, + amount: 100, + errMsg: null, + }, + }); + + const result = await service.verifySenderProofAsReceiver('confidentialAccount', { + amount: new BigNumber(10), + senderProof: '0xsomeproof', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/receiver_verify`, + method: 'POST', + data: { + amount: 10, + sender_proof: '0xsomeproof', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + isValid: true, + amount: new BigNumber(100), + errMsg: null, + }); + }); + }); + + describe('decrypt', () => { + it('should return decrypted balance', async () => { + mockLastValueFrom.mockReturnValue({ + status: 200, + data: { + value: 10, + }, + }); + + const result = await service.decryptBalance('confidentialAccount', { + encryptedValue: '0xsomebalance', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/decrypt`, + method: 'POST', + data: { + encrypted_value: '0xsomebalance', + }, + timeout: 10000, + }); + + expect(result).toEqual({ + value: new BigNumber(10), + }); + }); + }); + + describe('generateBurnProof', () => { + it('should return generated burn proof', async () => { + const mockResult = 'some_proof'; + + mockLastValueFrom.mockReturnValue({ + status: 200, + data: mockResult, + }); + + const result = await service.generateBurnProof('confidentialAccount', { + amount: new BigNumber(100), + encryptedBalance: '0xencryptedBalance', + }); + + expect(mockHttpService.request).toHaveBeenCalledWith({ + url: `${proofServerUrl}/accounts/confidentialAccount/burn`, + method: 'POST', + data: { + amount: 100, + encrypted_balance: '0xencryptedBalance', + }, + timeout: 10000, + }); + + expect(result).toEqual(mockResult); + }); + }); +}); diff --git a/src/confidential-proofs/confidential-proofs.service.ts b/src/confidential-proofs/confidential-proofs.service.ts new file mode 100644 index 00000000..30f445b5 --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.service.ts @@ -0,0 +1,187 @@ +import { HttpService } from '@nestjs/axios'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { ConfigType } from '@nestjs/config'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Method } from 'axios'; +import { lastValueFrom } from 'rxjs'; + +import { + deserializeObject, + serializeObject, +} from '~/confidential-proofs/confidential-proofs.utils'; +import confidentialProofsConfig from '~/confidential-proofs/config/confidential-proofs.config'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { DecryptBalanceDto } from '~/confidential-proofs/dto/decrypt-balance.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { ConfidentialAccountEntity } from '~/confidential-proofs/entities/confidential-account.entity'; +import { DecryptedBalanceModel } from '~/confidential-proofs/models/decrypted-balance.model'; +import { SenderProofVerificationResponseModel } from '~/confidential-proofs/models/sender-proof-verification-response.model'; +import { AppInternalError } from '~/polymesh-rest-api/src/common/errors'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; + +@Injectable() +export class ConfidentialProofsService { + private apiPath: string; + + constructor( + @Inject(confidentialProofsConfig.KEY) config: ConfigType, + private readonly httpService: HttpService, + private readonly logger: PolymeshLogger + ) { + this.apiPath = config.proofServerUrl; + + logger.setContext(ConfidentialProofsService.name); + } + + /** + * Make API requests to Proof Server + */ + private async requestProofServer( + apiEndpoint: string, + method: Method, + data?: unknown + ): Promise { + const { status, data: responseBody } = await lastValueFrom( + this.httpService.request({ + url: `${this.apiPath}/${apiEndpoint}`, + method, + data: serializeObject(data), + timeout: 10000, + }) + ); + + if (status !== HttpStatus.OK) { + this.logger.error( + `requestProofServer - Proof server responded with non-OK status : ${status} with message for the endpoint: ${apiEndpoint}` + ); + + throw new AppInternalError(`Proof server responded with non-OK status: ${status}`); + } + + this.logger.log(`requestProofServer - Received OK status for endpoint : "${apiEndpoint}"`); + + return deserializeObject(responseBody) as T; + } + + /** + * Gets all confidential accounts present in the Proof Server + */ + public async getConfidentialAccounts(): Promise { + this.logger.debug('getConfidentialAccounts - Fetching Confidential Accounts from proof server'); + + return this.requestProofServer('accounts', 'GET'); + } + + /** + * Creates a new ElGamal key pair in the Proof Server and returns its public key + */ + public async createConfidentialAccount(): Promise { + this.logger.debug( + 'createConfidentialAccount - Creating a new Confidential account in proof server' + ); + + return this.requestProofServer('accounts', 'POST', {}); + } + + /** + * Generates sender proof for a transaction leg. This will be used by the sender to affirm the transaction + * @param confidentialAccount + * @param senderInfo + * @returns sender proof + */ + public async generateSenderProof( + confidentialAccount: string, + senderInfo: { + amount: BigNumber; + auditors: string[]; + receiver: string; + encryptedBalance: string; + } + ): Promise { + this.logger.debug( + `generateSenderProof - Generating sender proof for account ${confidentialAccount}` + ); + + return this.requestProofServer(`accounts/${confidentialAccount}/send`, 'POST', senderInfo); + } + + /** + * Verifies sender proof as an auditor + */ + public async verifySenderProofAsAuditor( + confidentialAccount: string, + params: AuditorVerifySenderProofDto + ): Promise { + this.logger.debug( + `verifySenderProofAsAuditor - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` + ); + + const response = await this.requestProofServer( + `accounts/${confidentialAccount}/auditor_verify`, + 'POST', + params + ); + + return new SenderProofVerificationResponseModel(response); + } + + /** + * Verifies sender proof as a receiver + */ + public async verifySenderProofAsReceiver( + confidentialAccount: string, + params: ReceiverVerifySenderProofDto + ): Promise { + this.logger.debug( + `verifySenderProofAsReceiver - Verifying sender proof ${params.senderProof} for account ${confidentialAccount}` + ); + + const response = await this.requestProofServer( + `accounts/${confidentialAccount}/receiver_verify`, + 'POST', + params + ); + + return new SenderProofVerificationResponseModel(response); + } + + /** + * Decrypts balance for a confidential account + */ + public async decryptBalance( + confidentialAccount: string, + params: DecryptBalanceDto + ): Promise { + this.logger.debug( + `decryptBalance - Decrypting balance ${params.encryptedValue} for account ${confidentialAccount}` + ); + + const response = await this.requestProofServer( + `accounts/${confidentialAccount}/decrypt`, + 'POST', + params + ); + + return new DecryptedBalanceModel(response); + } + + /** + * Generates sender proof for a transaction leg. This will be used by the sender to affirm the transaction + * @param confidentialAccount + * @param burnInfo consisting of the amount and current encrypted balance + * @returns sender proof + */ + public async generateBurnProof( + confidentialAccount: string, + burnInfo: { + amount: BigNumber; + encryptedBalance: string; + } + ): Promise { + this.logger.debug( + `generateBurnProof - Generating burn proof for account ${confidentialAccount}` + ); + + return this.requestProofServer(`accounts/${confidentialAccount}/burn`, 'POST', burnInfo); + } +} diff --git a/src/confidential-proofs/confidential-proofs.utils.ts b/src/confidential-proofs/confidential-proofs.utils.ts new file mode 100644 index 00000000..c7c1dfaa --- /dev/null +++ b/src/confidential-proofs/confidential-proofs.utils.ts @@ -0,0 +1,38 @@ +/* istanbul ignore file */ + +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { camelCase, mapKeys, mapValues, snakeCase } from 'lodash'; + +export function serializeObject(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj.map(serializeObject); + } + + if (obj instanceof BigNumber && !obj.isNaN()) { + return obj.toNumber(); + } + + if (obj && typeof obj === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const snakeCasedObject = mapKeys(obj as any, (_, key) => snakeCase(key)); + return mapValues(snakeCasedObject, val => serializeObject(val)); + } + return obj; +} + +export function deserializeObject(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj.map(deserializeObject); + } + + if (typeof obj === 'number') { + return new BigNumber(obj); + } + + if (obj && typeof obj === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const snakeCasedObject = mapKeys(obj as any, (_, key) => camelCase(key)); + return mapValues(snakeCasedObject, val => deserializeObject(val)); + } + return obj; +} diff --git a/src/confidential-proofs/config/confidential-proofs.config.ts b/src/confidential-proofs/config/confidential-proofs.config.ts new file mode 100644 index 00000000..ec670f6d --- /dev/null +++ b/src/confidential-proofs/config/confidential-proofs.config.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +import { registerAs } from '@nestjs/config'; + +export default registerAs('confidential-proofs', () => { + const { PROOF_SERVER_URL } = process.env; + + return { + proofServerUrl: PROOF_SERVER_URL || '', + }; +}); diff --git a/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts new file mode 100644 index 00000000..5129d332 --- /dev/null +++ b/src/confidential-proofs/dto/auditor-verify-sender-proof.dto.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { IsOptional, IsString } from 'class-validator'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; + +export class AuditorVerifySenderProofDto { + @ApiProperty({ + description: 'Amount to be matched in the sender proof. Null value will not match anything', + example: '1000', + nullable: true, + type: 'string', + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber | null; + + @ApiProperty({ + description: 'The id of the auditor', + example: '1', + type: 'string', + }) + @ToBigNumber() + @IsBigNumber() + readonly auditorId: BigNumber; + + @ApiProperty({ + description: 'The sender proof to be verified', + example: + '0x10f620c5c018989944b064c8fa2424fe6770c1c7fe7ee2de6a913dc7571d0ee55b46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b30b7e76740925642846097ac38093efb57faea9dfa724313b294059d843e59641c9c319fc06b41279a57d759127226eb26faed5ecc21fe213f01efa17624b75754b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e8d1110e6c1351cfc1c1eeba177dab45487f6bc349ae94c9214ca3ad235d9916dbfa55f8a9a8b9c09922522d79b824f60894aca2e1d5c5a747cc6a98d0698cf267e124206e82f047920eda60dbd4c1cf9b6e2df89c7c88d30b0bb10157ae560c5982c022c59eff9c588f62a365a252220a154ccc5062be0f134b48a007ff87791f65f15d8e251a89f2a76a9ed2b9ec5d7a4f008e448a5d0b2ad7896ede15dff1108de574a36817324abf3e23492809c65a695b366063d2caabbe887a474251bfef8e10a8529f3866e8646e05f5b12728fb89c3b044ce833e64c58103ac337ce3c5c980b78efd031093f17fc4c5ccfa555c9879cf046cbe408ed5e930a8b987b33e3d6399eb93bdcde5d10ba7ef0995b51839ba6f39260d7e82969c37dea9b22389c12092e8e6210ec580b485731143099369d0eb053e7be2c48f4f48d480505e79aec260e25e01c88ade5f002932c6d669b988d64a14efd97d7bf8bc528fd87a7f8230a0a4c4f94226431226b59c1fa21b42c13a3e556bb4e626d3d649a4ccca2fc470f810b0294296a9db47977e771ed9d4eaa9035594958060c46462217d8c80dbe45372168da6a3ee3d4a6025acc878d1b75d435d8f7efbc7430421e1a4407a1f6474631507d89feb6fbd3eaa61ae4b7f167ae4301130e8ba5f2f25d12bdb0206206db36d27ad9ec0124599b0b602e40eae0dc66d8e9a1f95559e0d58b9afc88f1c0c949d884e60c9c7afb0c2ed3ef89dcc9e2a24d51ca42b2d856055bae25a695d91e0fa4a5f02e842bacfdd5b5e1080afed6e9baada9389370345f5f18a37835ab3209b31ba22cf666d0a64863a37c15cee2fc258d72df31c35b08c4889e6d222e4b081295716ea0de0a32f33f3346ff75912abe126214940a93009abf8624cda12f13a4ab0a2477503b830fdcc9119b226060eb1eaf1e5c183608d7d2f5f05ebd9852545e3c7283ffc8e46cc38529dd4340587d03e9b4464717164a5f9ab807afa45110841dda10762896d8b0851d2653a7f28d5419b3053b1ac785c0b803b8f9234eb00e593c6b9a915a923e2e10c23a8c09fef37b947025136b847d16838b47c851146b8094dfa7231d4ea8dba4b5dc88e2d4a553606539e89b58b06344470c58694253be83c430f2d842762e393bf083330cc7903a6a9d8dced3456eed9e329c18c896be037637c79e64bf0647024865c78b72f436df77e755bc9f78f0144d757c86530f5ff7c1d65bb0dcd424e5880b37e28084d1506d4259c434a709c4ac1361fae5c19ad48dd95ec631624753bb31d7dff4f7901ad9d2da14c0a78480ab40225e585ec2e3f595c52775c602759f16f06a3ac700db302576233c605aed07a01b7a05cf9cfd5b449a8b278289d11fee535739472c2091e401499cd7f3526abd6af65873e854f8e279c836ba575c7554c89990ccb2e6d22f94c0af0923f3984727ec07c3503d3773fc63770dbc013a6754d5b6c5558c8840829e59a3a0384a5d421f54844ba51f77ccfb2935a022813efc629ba5e64157b0c25de0f118764bea098c33d248e28c86ae177e279a886f0a1455448bad6dab4bbd8a1cef5684c0250d', + type: 'string', + }) + @IsString() + readonly senderProof: string; +} diff --git a/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts new file mode 100644 index 00000000..9307c187 --- /dev/null +++ b/src/confidential-proofs/dto/auditor-verify-transaction.dto.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class VerifyTransactionAmountsDto { + @ApiProperty({ + description: + 'The public key to decrypt transaction amounts for. Any leg with a provided sender proof involving this key as auditor or a receiver will be verified. The corresponding private key must be present in the proof server', + type: 'string', + example: '0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d', + }) + @IsString() + readonly publicKey: string; +} diff --git a/src/confidential-proofs/dto/decrypt-balance.dto.ts b/src/confidential-proofs/dto/decrypt-balance.dto.ts new file mode 100644 index 00000000..1180dcc5 --- /dev/null +++ b/src/confidential-proofs/dto/decrypt-balance.dto.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class DecryptBalanceDto { + @ApiProperty({ + description: 'Encrypted balance', + example: + '0x46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b54b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e', + type: 'string', + }) + @IsString() + readonly encryptedValue: string; +} diff --git a/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts new file mode 100644 index 00000000..c6a7244e --- /dev/null +++ b/src/confidential-proofs/dto/receiver-verify-sender-proof.dto.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { IsOptional, IsString } from 'class-validator'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; + +export class ReceiverVerifySenderProofDto { + @ApiProperty({ + description: 'Amount to be matched in the sender proof. Null value will not match anything', + example: '1000', + nullable: true, + type: 'string', + }) + @IsOptional() + @ToBigNumber() + @IsBigNumber() + readonly amount: BigNumber | null; + + @ApiProperty({ + description: 'The sender proof to be verified', + example: + '0x10f620c5c018989944b064c8fa2424fe6770c1c7fe7ee2de6a913dc7571d0ee55b46247c432a2632d23644aab44da0457506cbf7e712cea7158eeb4324f932161b30b7e76740925642846097ac38093efb57faea9dfa724313b294059d843e59641c9c319fc06b41279a57d759127226eb26faed5ecc21fe213f01efa17624b75754b44b6e87ca5028099745482c1ef3fc9901ae760a08f925c8e68c1511f6f77e8d1110e6c1351cfc1c1eeba177dab45487f6bc349ae94c9214ca3ad235d9916dbfa55f8a9a8b9c09922522d79b824f60894aca2e1d5c5a747cc6a98d0698cf267e124206e82f047920eda60dbd4c1cf9b6e2df89c7c88d30b0bb10157ae560c5982c022c59eff9c588f62a365a252220a154ccc5062be0f134b48a007ff87791f65f15d8e251a89f2a76a9ed2b9ec5d7a4f008e448a5d0b2ad7896ede15dff1108de574a36817324abf3e23492809c65a695b366063d2caabbe887a474251bfef8e10a8529f3866e8646e05f5b12728fb89c3b044ce833e64c58103ac337ce3c5c980b78efd031093f17fc4c5ccfa555c9879cf046cbe408ed5e930a8b987b33e3d6399eb93bdcde5d10ba7ef0995b51839ba6f39260d7e82969c37dea9b22389c12092e8e6210ec580b485731143099369d0eb053e7be2c48f4f48d480505e79aec260e25e01c88ade5f002932c6d669b988d64a14efd97d7bf8bc528fd87a7f8230a0a4c4f94226431226b59c1fa21b42c13a3e556bb4e626d3d649a4ccca2fc470f810b0294296a9db47977e771ed9d4eaa9035594958060c46462217d8c80dbe45372168da6a3ee3d4a6025acc878d1b75d435d8f7efbc7430421e1a4407a1f6474631507d89feb6fbd3eaa61ae4b7f167ae4301130e8ba5f2f25d12bdb0206206db36d27ad9ec0124599b0b602e40eae0dc66d8e9a1f95559e0d58b9afc88f1c0c949d884e60c9c7afb0c2ed3ef89dcc9e2a24d51ca42b2d856055bae25a695d91e0fa4a5f02e842bacfdd5b5e1080afed6e9baada9389370345f5f18a37835ab3209b31ba22cf666d0a64863a37c15cee2fc258d72df31c35b08c4889e6d222e4b081295716ea0de0a32f33f3346ff75912abe126214940a93009abf8624cda12f13a4ab0a2477503b830fdcc9119b226060eb1eaf1e5c183608d7d2f5f05ebd9852545e3c7283ffc8e46cc38529dd4340587d03e9b4464717164a5f9ab807afa45110841dda10762896d8b0851d2653a7f28d5419b3053b1ac785c0b803b8f9234eb00e593c6b9a915a923e2e10c23a8c09fef37b947025136b847d16838b47c851146b8094dfa7231d4ea8dba4b5dc88e2d4a553606539e89b58b06344470c58694253be83c430f2d842762e393bf083330cc7903a6a9d8dced3456eed9e329c18c896be037637c79e64bf0647024865c78b72f436df77e755bc9f78f0144d757c86530f5ff7c1d65bb0dcd424e5880b37e28084d1506d4259c434a709c4ac1361fae5c19ad48dd95ec631624753bb31d7dff4f7901ad9d2da14c0a78480ab40225e585ec2e3f595c52775c602759f16f06a3ac700db302576233c605aed07a01b7a05cf9cfd5b449a8b278289d11fee535739472c2091e401499cd7f3526abd6af65873e854f8e279c836ba575c7554c89990ccb2e6d22f94c0af0923f3984727ec07c3503d3773fc63770dbc013a6754d5b6c5558c8840829e59a3a0384a5d421f54844ba51f77ccfb2935a022813efc629ba5e64157b0c25de0f118764bea098c33d248e28c86ae177e279a886f0a1455448bad6dab4bbd8a1cef5684c0250d', + type: 'string', + }) + @IsString() + readonly senderProof: string; +} diff --git a/src/confidential-proofs/entities/confidential-account.entity.ts b/src/confidential-proofs/entities/confidential-account.entity.ts new file mode 100644 index 00000000..277a52de --- /dev/null +++ b/src/confidential-proofs/entities/confidential-account.entity.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +export class ConfidentialAccountEntity { + /** + * Public key of ElGamal Key Pair + */ + public confidentialAccount: string; + + public createdAt: Date; + + public updatedAt: Date; + + constructor(entity: ConfidentialAccountEntity) { + Object.assign(this, entity); + } +} diff --git a/src/confidential-proofs/models/auditor-verify-proof.model.ts b/src/confidential-proofs/models/auditor-verify-proof.model.ts new file mode 100644 index 00000000..82068a8a --- /dev/null +++ b/src/confidential-proofs/models/auditor-verify-proof.model.ts @@ -0,0 +1,83 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class AuditorVerifyProofModel { + @ApiProperty({ + description: 'The leg ID for which this response relates to', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'The asset ID for which this response relates to', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + readonly assetId: string; + + @ApiProperty({ + type: 'boolean', + example: true, + description: + 'Whether the sender has provided proof for the leg. If a proof has yet to be provided, then the amount being transferred has yet to be determined', + }) + readonly isProved: boolean; + + @ApiProperty({ + type: 'boolean', + example: true, + description: + 'Whether is specified public key is an auditor for the related portion of the transaction. If not, then the given auditor is unable to decrypt the amount', + }) + readonly isAuditor: boolean; + + @ApiProperty({ + description: 'Whether the specified public key is is the receiver for the transaction', + type: 'boolean', + example: false, + }) + readonly isReceiver: boolean; + + @ApiProperty({ + description: + 'Whether the amount has been decrypted or not. If true the `amount` field will be present. Will be true if the leg has been proved and the specified key is either an auditor or the receiver', + type: 'boolean', + example: true, + }) + readonly amountDecrypted: boolean; + + @ApiPropertyOptional({ + description: + 'The amount of the asset being transferred in this leg. Will only be present if sender has already submitted the proof and the provided auditor was specified', + type: 'string', + nullable: true, + example: '100', + }) + @FromBigNumber() + readonly amount: BigNumber | null; + + @ApiPropertyOptional({ + description: + 'Whether the proof server determined to sender proof to be valid or not. Will only be present if the sender has already submitted the proof and the provided auditor was specified', + type: 'string', + nullable: true, + }) + readonly isValid: boolean | null; + + @ApiPropertyOptional({ + description: 'The error message provided by the proof server, if one was returned', + type: 'string', + nullable: true, + }) + readonly errMsg: string | null; + + constructor(model: AuditorVerifyProofModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-proofs/models/auditor-verify-transaction.model.ts b/src/confidential-proofs/models/auditor-verify-transaction.model.ts new file mode 100644 index 00000000..4b0c9cf2 --- /dev/null +++ b/src/confidential-proofs/models/auditor-verify-transaction.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; + +export class AuditorVerifyTransactionModel { + @ApiProperty({ + description: 'The verification status of each leg and asset', + isArray: true, + type: AuditorVerifyProofModel, + }) + @Type(() => AuditorVerifyProofModel) + readonly verifications: AuditorVerifyProofModel[]; + + constructor(model: AuditorVerifyTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-proofs/models/decrypted-balance.model.ts b/src/confidential-proofs/models/decrypted-balance.model.ts new file mode 100644 index 00000000..a42fc8e0 --- /dev/null +++ b/src/confidential-proofs/models/decrypted-balance.model.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class DecryptedBalanceModel { + @ApiProperty({ + description: 'Decrypted balance value', + type: 'string', + example: '100', + }) + @FromBigNumber() + readonly value: BigNumber; + + constructor(model: DecryptedBalanceModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-proofs/models/sender-proof-verification-response.model.ts b/src/confidential-proofs/models/sender-proof-verification-response.model.ts new file mode 100644 index 00000000..5702f6d4 --- /dev/null +++ b/src/confidential-proofs/models/sender-proof-verification-response.model.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class SenderProofVerificationResponseModel { + @ApiProperty({ + description: 'Indicates if the sender proof was valid', + type: 'boolean', + example: true, + }) + readonly isValid: boolean; + + @ApiProperty({ + description: 'Amount specified in the in the transaction', + type: 'string', + example: '100', + }) + @FromBigNumber() + readonly amount: BigNumber; + + @ApiProperty({ + description: 'Indicates if the sender proof was valid', + type: 'boolean', + example: 'Invalid proof: TransactionAmountMismatch { expected_amount: 1000 }', + nullable: true, + }) + readonly errMsg: string | null; + + constructor(model: SenderProofVerificationResponseModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/confidential-transactions.controller.spec.ts b/src/confidential-transactions/confidential-transactions.controller.spec.ts new file mode 100644 index 00000000..38247caf --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.controller.spec.ts @@ -0,0 +1,181 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAffirmParty, + ConfidentialTransaction, + ConfidentialTransactionStatus, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAccount, + createMockConfidentialAsset, + createMockConfidentialTransaction, + createMockIdentity, +} from '~/test-utils/mocks'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; + +describe('ConfidentialTransactionsController', () => { + let controller: ConfidentialTransactionsController; + let mockConfidentialTransactionsService: DeepMocked; + const id = new BigNumber(1); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialTransactionsController], + providers: [mockConfidentialTransactionsServiceProvider], + }).compile(); + + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ConfidentialTransactionsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getDetails', () => { + it('should return the details of Confidential Transaction', async () => { + const details = { + status: ConfidentialTransactionStatus.Pending, + createdAt: new BigNumber(100000), + memo: 'SOME_MEMO', + venueId: new BigNumber(1), + }; + const mockLeg = { + id: new BigNumber(0), + sender: createMockConfidentialAccount({ publicKey: 'SENDER' }), + receiver: createMockConfidentialAccount({ publicKey: 'RECEIVER' }), + mediators: [createMockIdentity({ did: 'MEDIATOR' })], + assetAuditors: [ + { + asset: createMockConfidentialAsset({ id: 'SOME_ASSET_ID' }), + auditors: [createMockConfidentialAccount({ publicKey: 'AUDITOR' })], + }, + ], + }; + const mockConfidentialTransaction = createMockConfidentialTransaction(); + + mockConfidentialTransaction.details.mockResolvedValue(details); + mockConfidentialTransaction.getLegs.mockResolvedValue([mockLeg]); + + mockConfidentialTransactionsService.findOne.mockResolvedValue(mockConfidentialTransaction); + + const result = await controller.getDetails({ id }); + + const expectedLegs = [ + { + id: mockLeg.id, + sender: expect.objectContaining({ publicKey: 'SENDER' }), + receiver: expect.objectContaining({ publicKey: 'RECEIVER' }), + mediators: expect.arrayContaining([{ did: 'MEDIATOR' }]), + assetAuditors: expect.arrayContaining([ + { + asset: expect.objectContaining({ id: 'SOME_ASSET_ID' }), + auditors: expect.arrayContaining([{ publicKey: 'AUDITOR' }]), + }, + ]), + }, + ]; + + expect(result).toEqual({ + id, + ...details, + legs: expectedLegs, + }); + }); + }); + + describe('observerAffirmLeg', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Receiver, + } as ObserverAffirmConfidentialTransactionDto; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.observerAffirmLeg) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.observerAffirmLeg({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('rejectTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.rejectTransaction) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.rejectConfidentialTransaction({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('executeTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + + const transactionId = new BigNumber(1); + + when(mockConfidentialTransactionsService.executeTransaction) + .calledWith(transactionId, input) + .mockResolvedValue(txResult as unknown as ServiceReturn); + + const result = await controller.executeConfidentialTransaction({ id: transactionId }, input); + expect(result).toEqual(txResult); + }); + }); + + describe('getInvolvedParties', () => { + it('should call the service and return the result', async () => { + const transactionId = new BigNumber(1); + when(mockConfidentialTransactionsService.getInvolvedParties) + .calledWith(transactionId) + .mockResolvedValue([createMockIdentity({ did: 'INVOLVED_PARTY_DID' })]); + + const result = await controller.getInvolvedParties({ id: transactionId }); + + expect(result).toEqual([ + expect.objectContaining({ + did: 'INVOLVED_PARTY_DID', + }), + ]); + }); + }); + + describe('getPendingAffirmsCount', () => { + it('should call the service and return the result', async () => { + const transactionId = new BigNumber(1); + when(mockConfidentialTransactionsService.getPendingAffirmsCount) + .calledWith(transactionId) + .mockResolvedValue(new BigNumber(3)); + + const result = await controller.getPendingAffirmsCount({ id: transactionId }); + + expect(result).toEqual(3); + }); + }); +}); diff --git a/src/confidential-transactions/confidential-transactions.controller.ts b/src/confidential-transactions/confidential-transactions.controller.ts new file mode 100644 index 00000000..5ca76d54 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.controller.ts @@ -0,0 +1,172 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from '@nestjs/swagger'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; + +@ApiTags('confidential-transactions') +@Controller('confidential-transactions') +export class ConfidentialTransactionsController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiOperation({ + summary: 'Fetch Confidential transaction details', + description: 'This endpoint will provide the details of a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the Confidential Transaction', + type: ConfidentialTransactionModel, + }) + @ApiNotFoundResponse({ + description: 'The Confidential Transaction with the given ID was not found', + }) + @Get(':id') + public async getDetails(@Param() { id }: IdParamsDto): Promise { + const transaction = await this.confidentialTransactionsService.findOne(id); + return createConfidentialTransactionModel(transaction); + } + + @ApiOperation({ + summary: 'Affirm a leg of an existing Confidential Transaction as a Receiver/Mediator', + description: + 'This endpoint will affirm a specific leg of a pending Confidential Transaction as a Receiver/Mediator. Note, a mediator/receiver can only affirm a confidential transaction when the sender has already affirmed it. All owners of involved portfolios must affirm for the Confidential Transaction to be executed', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be affirmed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/affirm-leg/observer') + public async observerAffirmLeg( + @Param() { id }: IdParamsDto, + @Body() body: ObserverAffirmConfidentialTransactionDto + ): Promise { + const result = await this.confidentialTransactionsService.observerAffirmLeg(id, body); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Reject a Confidential Transaction', + description: 'This endpoint will reject a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be rejected', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/reject') + public async rejectConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.confidentialTransactionsService.rejectTransaction(id, signerDto); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Execute a Confidential Transaction', + description: + 'This endpoint will execute a Confidential Transaction already affirmed by all the involved parties', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction to be executed', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'Details of the transaction', + type: TransactionQueueModel, + }) + @Post(':id/execute') + public async executeConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() signerDto: TransactionBaseDto + ): Promise { + const result = await this.confidentialTransactionsService.executeTransaction(id, signerDto); + return handleServiceResult(result); + } + + @ApiOperation({ + summary: 'Get list of all involved parties', + description: + 'This endpoint will return a list of all identities involved in a given Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'List of DIDs of the involved parties', + type: IdentityModel, + isArray: true, + }) + @ApiNotFoundResponse({ + description: 'No involved parties were found for the given Confidential Transaction', + }) + @Get(':id/involved-parties') + public async getInvolvedParties(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.getInvolvedParties(id); + + return result.map(({ did }) => new IdentityModel({ did })); + } + + @ApiOperation({ + summary: 'Get pending affirmation count', + description: + 'This endpoint retrieves the number of pending affirmations for a Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'Number of pending affirmation', + type: 'number', + }) + @ApiNotFoundResponse({ + description: 'Affirm count not available', + }) + @Get(':id/pending-affirmation-count') + public async getPendingAffirmsCount(@Param() { id }: IdParamsDto): Promise { + const result = await this.confidentialTransactionsService.getPendingAffirmsCount(id); + + return result.toNumber(); + } +} diff --git a/src/confidential-transactions/confidential-transactions.module.ts b/src/confidential-transactions/confidential-transactions.module.ts new file mode 100644 index 00000000..524b4217 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.module.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +import { forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialAccountsModule } from '~/confidential-accounts/confidential-accounts.module'; +import { ConfidentialProofsModule } from '~/confidential-proofs/confidential-proofs.module'; +import { ConfidentialTransactionsController } from '~/confidential-transactions/confidential-transactions.controller'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; +import { ExtendedIdentitiesModule } from '~/extended-identities/identities.module'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { TransactionsModule } from '~/transactions/transactions.module'; + +@Module({ + imports: [ + PolymeshModule, + TransactionsModule, + ConfidentialAccountsModule, + forwardRef(() => ConfidentialProofsModule.register()), + forwardRef(() => ExtendedIdentitiesModule), + ], + providers: [ConfidentialTransactionsService], + controllers: [ConfidentialTransactionsController, ConfidentialVenuesController], + exports: [ConfidentialTransactionsService], +}) +export class ConfidentialTransactionsModule {} diff --git a/src/confidential-transactions/confidential-transactions.service.spec.ts b/src/confidential-transactions/confidential-transactions.service.spec.ts new file mode 100644 index 00000000..0904c624 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.service.spec.ts @@ -0,0 +1,866 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAccount, + ConfidentialAffirmParty, + ConfidentialTransaction, + ConfidentialTransactionStatus, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import * as confidentialTransactionsUtilModule from '~/confidential-transactions/confidential-transactions.util'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; +import { testValues } from '~/test-utils/consts'; +import { + createMockConfidentialAccount, + createMockConfidentialTransaction, + createMockConfidentialVenue, + createMockIdentity, + MockIdentity, + MockPolymesh, + MockTransaction, +} from '~/test-utils/mocks'; +import { + mockConfidentialAccountsServiceProvider, + mockConfidentialProofsServiceProvider, + MockIdentitiesService, + mockTransactionsProvider, + MockTransactionsService, +} from '~/test-utils/service-mocks'; +import { TransactionsService } from '~/transactions/transactions.service'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +const { signer } = testValues; + +describe('ConfidentialTransactionsService', () => { + let service: ConfidentialTransactionsService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + let mockTransactionsService: MockTransactionsService; + let mockConfidentialProofsService: DeepMocked; + let mockConfidentialAccountsService: ConfidentialAccountsService; + let mockIdentitiesService: MockIdentitiesService; + const id = new BigNumber(1); + + beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + + mockIdentitiesService = new MockIdentitiesService(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [PolymeshModule], + providers: [ + ConfidentialTransactionsService, + mockTransactionsProvider, + mockConfidentialProofsServiceProvider, + mockConfidentialAccountsServiceProvider, + ExtendedIdentitiesService, + ], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .overrideProvider(ExtendedIdentitiesService) + .useValue(mockIdentitiesService) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + mockConfidentialAccountsService = module.get( + ConfidentialAccountsService + ); + mockConfidentialProofsService = + module.get(ConfidentialProofsService); + mockTransactionsService = module.get(TransactionsService); + + service = module.get(ConfidentialTransactionsService); + }); + + afterEach(async () => { + await polymeshService.close(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return a Confidential Transaction for a valid ID', async () => { + const transaction = createMockConfidentialTransaction(); + mockPolymeshApi.confidentialSettlements.getTransaction.mockResolvedValue(transaction); + + const result = await service.findOne(id); + + expect(result).toEqual(transaction); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialSettlements.getTransaction.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('findVenue', () => { + it('should return a Confidential Venue for a valid ID', async () => { + const venue = createMockConfidentialVenue(); + mockPolymeshApi.confidentialSettlements.getVenue.mockResolvedValue(venue); + + const result = await service.findVenue(id); + + expect(result).toEqual(venue); + }); + + it('should call handleSdkError and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.confidentialSettlements.getVenue.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findVenue(id)).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + + describe('getVenueCreator', () => { + it('should return the creator of the Venue', async () => { + const venue = createMockConfidentialVenue(); + + jest.spyOn(service, 'findVenue').mockResolvedValue(venue); + + const result = await service.getVenueCreator(id); + + expect(result).toEqual(expect.objectContaining({ did: 'SOME_OWNER' })); + }); + }); + + describe('createConfidentialVenue', () => { + it('should create the Confidential Venue', async () => { + const input = { + signer, + }; + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.CreateVenue, + }; + const mockTransaction = new MockTransaction(mockTransactions); + const mockVenue = createMockConfidentialVenue(); + + mockTransactionsService.submit.mockResolvedValue({ + result: mockVenue, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialVenue(input); + + expect(result).toEqual({ + result: mockVenue, + transactions: [mockTransaction], + }); + }); + }); + + describe('createConfidentialTransaction', () => { + it('should call the addTransaction procedure in the venue where the transaction is to be created', async () => { + const args = { + legs: [ + { + assets: ['SOME_CONFIDENTIAL_ASSET'], + sender: 'SENDER_CONFIDENTIAL_ACCOUNT', + receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', + auditors: [], + mediators: [], + }, + ], + memo: 'SOME_MEMO', + }; + + const mockVenue = createMockConfidentialVenue(); + jest.spyOn(service, 'findVenue').mockResolvedValue(mockVenue); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AddTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + const mockConfidentialTransaction = createMockConfidentialTransaction(); + + when(mockTransactionsService.submit) + .calledWith(mockVenue.addTransaction, args, { signer, processMode: ProcessMode.Submit }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.createConfidentialTransaction(new BigNumber(1), { + signer, + ...args, + }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('observerAffirmLeg', () => { + it('should call the affirmLeg procedure for the transaction being approved by Receiver/Mediator', async () => { + const args = { + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Receiver, + } as ObserverAffirmConfidentialTransactionDto; + + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AffirmTransactions, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith(mockConfidentialTransaction.affirmLeg, args, { + signer, + processMode: ProcessMode.Submit, + }) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.observerAffirmLeg(new BigNumber(1), { ...args, signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('senderAffirmLeg', () => { + let mockConfidentialTransaction: ConfidentialTransaction; + let mockConfidentialTransactionModel: ConfidentialTransactionModel; + let body: Omit; + let sender: ConfidentialAccount; + + beforeEach(() => { + mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + mockConfidentialTransactionModel = new ConfidentialTransactionModel({ + id: new BigNumber(1), + venueId: new BigNumber(1), + createdAt: new BigNumber(100000), + status: ConfidentialTransactionStatus.Pending, + memo: 'Some transfer memo', + legs: [ + { + id: new BigNumber(0), + sender: new ConfidentialAccountModel({ publicKey: 'SENDER_CONFIDENTIAL_ACCOUNT' }), + receiver: new ConfidentialAccountModel({ publicKey: 'RECEIVER_CONFIDENTIAL_ACCOUNT' }), + mediators: [], + assetAuditors: [ + new ConfidentialAssetAuditorModel({ + asset: new ConfidentialAssetModel({ id: 'SOME_ASSET_ID' }), + auditors: [ + new ConfidentialAccountModel({ publicKey: 'AUDITOR_CONFIDENTIAL_ACCOUNT' }), + ], + }), + ], + }, + ], + }); + + jest + .spyOn(confidentialTransactionsUtilModule, 'createConfidentialTransactionModel') + .mockResolvedValue(mockConfidentialTransactionModel); + + body = { + legId: new BigNumber(0), + legAmounts: [ + { + confidentialAsset: 'SOME_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }; + + sender = createMockConfidentialAccount(); + when(mockConfidentialAccountsService.findOne) + .calledWith('SENDER_CONFIDENTIAL_ACCOUNT') + .mockResolvedValue(sender); + + when(mockConfidentialProofsService.generateSenderProof) + .calledWith('SENDER_CONFIDENTIAL_ACCOUNT', { + amount: new BigNumber(100), + auditors: ['AUDITOR_CONFIDENTIAL_ACCOUNT'], + receiver: 'RECEIVER_CONFIDENTIAL_ACCOUNT', + encryptedBalance: '0x0ceabalance', + }) + .mockResolvedValue('some_proof'); + }); + + it('should throw an error for an invalid legId', () => { + return expect( + service.senderAffirmLeg(new BigNumber(1), { signer, ...body, legId: new BigNumber(10) }) + ).rejects.toThrow('Invalid leg ID received'); + }); + + it('should throw an error if leg amounts has an invalid Asset ID', () => { + return expect( + service.senderAffirmLeg(new BigNumber(1), { + signer, + ...body, + legAmounts: [ + { + confidentialAsset: 'RANDOM_ASSET_ID', + amount: new BigNumber(100), + }, + ], + }) + ).rejects.toThrow('Asset not found in the leg'); + }); + + it('should call the affirmLeg procedure for the transaction being approved by Sender', async () => { + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.AffirmTransactions, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith( + mockConfidentialTransaction.affirmLeg, + { + legId: new BigNumber(0), + party: ConfidentialAffirmParty.Sender, + proofs: [ + { + asset: 'SOME_ASSET_ID', + proof: 'some_proof', + }, + ], + }, + { signer, processMode: ProcessMode.Submit } + ) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.senderAffirmLeg(new BigNumber(1), { ...body, signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('rejectTransaction', () => { + it('should call the reject procedure for the transaction being rejected', async () => { + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.RejectTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith( + mockConfidentialTransaction.reject, + {}, + { signer, processMode: ProcessMode.Submit } + ) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.rejectTransaction(new BigNumber(1), { signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('rejectTransaction', () => { + it('should call the reject procedure for the transaction being rejected', async () => { + const mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const mockTransactions = { + blockHash: '0x1', + txHash: '0x2', + blockNumber: new BigNumber(1), + tag: TxTags.confidentialAsset.ExecuteTransaction, + }; + + const mockTransaction = new MockTransaction(mockTransactions); + + when(mockTransactionsService.submit) + .calledWith( + mockConfidentialTransaction.execute, + {}, + { signer, processMode: ProcessMode.Submit } + ) + .mockResolvedValue({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + + const result = await service.executeTransaction(new BigNumber(1), { signer }); + + expect(result).toEqual({ + result: mockConfidentialTransaction, + transactions: [mockTransaction], + }); + }); + }); + + describe('getInvolvedParties', () => { + it('should return the involved parties in a transaction', async () => { + const expectedResult = [createMockIdentity()]; + const mockConfidentialTransaction = createMockConfidentialTransaction(); + mockConfidentialTransaction.getInvolvedParties.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const result = await service.getInvolvedParties(new BigNumber(1)); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('findVenuesByOwner', () => { + it('should return the confidential venues for an identity', async () => { + const mockIdentity = new MockIdentity(); + const mockConfidentialVenues = [createMockConfidentialVenue()]; + mockIdentity.getConfidentialVenues.mockResolvedValue(mockConfidentialVenues); + mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); + + const result = await service.findVenuesByOwner('SOME_DID'); + + expect(result).toEqual(mockConfidentialVenues); + }); + }); + + describe('getPendingAffirmsCount', () => { + it('should return the pending affirms count for a transaction', async () => { + const expectedResult = new BigNumber(3); + + const mockConfidentialTransaction = createMockConfidentialTransaction(); + mockConfidentialTransaction.getPendingAffirmsCount.mockResolvedValue(expectedResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + + const result = await service.getPendingAffirmsCount(new BigNumber(1)); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('verifyTransactionAmounts', () => { + const publicKey = '0x123'; + const assetId = 'someAssetId'; + const legId = new BigNumber(1); + + let mockConfidentialTransaction: DeepMocked; + + beforeEach(() => { + mockConfidentialTransaction = createMockConfidentialTransaction(); + jest.spyOn(service, 'findOne').mockResolvedValue(mockConfidentialTransaction); + }); + + it('should return results when the public key is an auditor for unproven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: true, + isProved: false, + isReceiver: false, + amountDecrypted: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is the receiver for unproven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock({ publicKey }), + proofs: [ + { + assetId, + auditors: [], + }, + ], + }, + ], + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: false, + isProved: false, + isReceiver: true, + amountDecrypted: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is not an auditor for unproven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [], + pending: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + auditors: [createMockConfidentialAccount({ publicKey: 'someOtherKey' })], + }, + ], + }, + ], + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: false, + isProved: false, + isReceiver: false, + amountDecrypted: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is an auditor for a proven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + pending: [], + }); + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: new BigNumber(100), + legId: new BigNumber(1), + isAuditor: true, + isProved: true, + isReceiver: false, + amountDecrypted: true, + isValid: true, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is the receiver for a proven legs', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock({ publicKey }), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [], + }); + + mockConfidentialProofsService.verifySenderProofAsReceiver.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: new BigNumber(100), + legId: new BigNumber(1), + isAuditor: false, + isProved: true, + isReceiver: true, + amountDecrypted: true, + isValid: true, + errMsg: null, + }, + ]); + }); + + it('should return results when the public key is not an auditor for a proven leg', async () => { + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [], + }); + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual([ + { + assetId, + amount: null, + legId, + isAuditor: false, + isProved: true, + isReceiver: false, + amountDecrypted: false, + isValid: null, + errMsg: null, + }, + ]); + }); + + it('should return results where auditor is only specified for some assets', async () => { + const otherAssetId = 'otherAssetId'; + + mockConfidentialTransaction.getProofDetails.mockResolvedValue({ + proved: [ + { + legId, + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId, + proof: 'someProof', + auditors: [createMock({ publicKey })], + }, + { + assetId: otherAssetId, + proof: 'otherProof', + auditors: [createMock()], + }, + ], + }, + ], + pending: [ + { + legId: new BigNumber(2), + sender: createMock(), + receiver: createMock(), + proofs: [ + { + assetId: otherAssetId, + auditors: [createMock({ publicKey })], + }, + ], + }, + ], + }); + + mockConfidentialProofsService.verifySenderProofAsAuditor.mockResolvedValue({ + amount: new BigNumber(100), + isValid: true, + errMsg: null, + }); + + const result = await service.verifyTransactionAmounts(mockConfidentialTransaction.id, { + publicKey, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + assetId, + amount: new BigNumber(100), + legId, + isAuditor: true, + amountDecrypted: true, + isReceiver: false, + isProved: true, + isValid: true, + errMsg: null, + }, + { + assetId: otherAssetId, + amount: null, + legId: new BigNumber(1), + isAuditor: false, + isProved: true, + amountDecrypted: false, + isReceiver: false, + isValid: null, + errMsg: null, + }, + { + assetId: otherAssetId, + amount: null, + legId: new BigNumber(2), + isAuditor: true, + isProved: false, + isReceiver: false, + amountDecrypted: false, + isValid: null, + errMsg: null, + }, + ]) + ); + }); + }); + + describe('createdAt', () => { + it('should return creation event details for a Confidential Transaction', async () => { + const mockResult = { + blockHash: 'someHash', + eventIndex: new BigNumber(1), + blockNumber: new BigNumber('2719172'), + blockDate: new Date('2023-06-26T01:47:45.000Z'), + }; + const transaction = createMockConfidentialTransaction(); + + transaction.createdAt.mockResolvedValue(mockResult); + + jest.spyOn(service, 'findOne').mockResolvedValue(transaction); + + const result = await service.createdAt(new BigNumber(10)); + + expect(result).toEqual(mockResult); + }); + }); +}); diff --git a/src/confidential-transactions/confidential-transactions.service.ts b/src/confidential-transactions/confidential-transactions.service.ts new file mode 100644 index 00000000..3a5dfce6 --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.service.ts @@ -0,0 +1,355 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + AddConfidentialTransactionParams, + AffirmConfidentialTransactionParams, + ConfidentialAffirmParty, + ConfidentialTransaction, + ConfidentialVenue, + EventIdentifier, + Identity, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { AuditorVerifySenderProofDto } from '~/confidential-proofs/dto/auditor-verify-sender-proof.dto'; +import { VerifyTransactionAmountsDto } from '~/confidential-proofs/dto/auditor-verify-transaction.dto'; +import { ReceiverVerifySenderProofDto } from '~/confidential-proofs/dto/receiver-verify-sender-proof.dto'; +import { AuditorVerifyProofModel } from '~/confidential-proofs/models/auditor-verify-proof.model'; +import { createConfidentialTransactionModel } from '~/confidential-transactions/confidential-transactions.util'; +import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; +import { ObserverAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/observer-affirm-confidential-transaction.dto'; +import { SenderAffirmConfidentialTransactionDto } from '~/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { AppValidationError } from '~/polymesh-rest-api/src/common/errors'; +import { extractTxOptions, ServiceReturn } from '~/polymesh-rest-api/src/common/utils/functions'; +import { TransactionsService } from '~/transactions/transactions.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ConfidentialTransactionsService { + constructor( + private readonly polymeshService: PolymeshService, + private readonly transactionsService: TransactionsService, + private readonly confidentialAccountsService: ConfidentialAccountsService, + private readonly confidentialProofsService: ConfidentialProofsService, + private readonly extendedIdentitiesService: ExtendedIdentitiesService + ) {} + + public async findOne(id: BigNumber): Promise { + return await this.polymeshService.polymeshApi.confidentialSettlements + .getTransaction({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async findVenue(id: BigNumber): Promise { + return await this.polymeshService.polymeshApi.confidentialSettlements + .getVenue({ id }) + .catch(error => { + throw handleSdkError(error); + }); + } + + public async getVenueCreator(id: BigNumber): Promise { + const venue = await this.findVenue(id); + return venue.creator(); + } + + public async createConfidentialVenue( + baseParams: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(baseParams); + const createVenue = this.polymeshService.polymeshApi.confidentialSettlements.createVenue; + return this.transactionsService.submit(createVenue, {}, options); + } + + public async createConfidentialTransaction( + venueId: BigNumber, + createConfidentialTransactionDto: CreateConfidentialTransactionDto + ): ServiceReturn { + const venue = await this.findVenue(venueId); + + const { options, args } = extractTxOptions(createConfidentialTransactionDto); + + return this.transactionsService.submit( + venue.addTransaction, + args as AddConfidentialTransactionParams, + options + ); + } + + public async observerAffirmLeg( + transactionId: BigNumber, + body: ObserverAffirmConfidentialTransactionDto + ): ServiceReturn { + const transaction = await this.findOne(transactionId); + + const { options, args } = extractTxOptions(body); + + return this.transactionsService.submit( + transaction.affirmLeg, + args as AffirmConfidentialTransactionParams, + options + ); + } + + public async senderAffirmLeg( + transactionId: BigNumber, + body: SenderAffirmConfidentialTransactionDto + ): ServiceReturn { + const tx = await this.findOne(transactionId); + + const transaction = await createConfidentialTransactionModel(tx); + + const { options, args } = extractTxOptions(body); + + const { legId, legAmounts } = args as SenderAffirmConfidentialTransactionDto; + + if (legId.gte(transaction.legs.length)) { + throw new AppValidationError('Invalid leg ID received'); + } + + const { receiver, sender, assetAuditors } = transaction.legs[legId.toNumber()]; + + const senderConfidentialAccount = await this.confidentialAccountsService.findOne( + sender.publicKey + ); + + const proofs = []; + + for (const legAmount of legAmounts) { + const { amount, confidentialAsset } = legAmount; + const assetAuditor = assetAuditors.find(({ asset }) => asset.id === confidentialAsset); + + if (!assetAuditor) { + throw new AppValidationError('Asset not found in the leg'); + } + + const encryptedBalance = await senderConfidentialAccount.getBalance({ + asset: confidentialAsset, + }); + + const proof = await this.confidentialProofsService.generateSenderProof(sender.publicKey, { + amount, + auditors: assetAuditor.auditors.map(({ publicKey }) => publicKey), + receiver: receiver.publicKey, + encryptedBalance, + }); + + proofs.push({ asset: confidentialAsset, proof }); + } + + return this.transactionsService.submit( + tx.affirmLeg, + { + legId, + party: ConfidentialAffirmParty.Sender, + proofs, + }, + options + ); + } + + public async rejectTransaction( + transactionId: BigNumber, + base: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(base); + const transaction = await this.findOne(transactionId); + + return this.transactionsService.submit(transaction.reject, {}, options); + } + + public async executeTransaction( + transactionId: BigNumber, + base: TransactionBaseDto + ): ServiceReturn { + const { options } = extractTxOptions(base); + const transaction = await this.findOne(transactionId); + + return this.transactionsService.submit(transaction.execute, {}, options); + } + + public async getInvolvedParties(transactionId: BigNumber): Promise { + const transaction = await this.findOne(transactionId); + + return transaction.getInvolvedParties(); + } + + public async findVenuesByOwner(did: string): Promise { + const identity = await this.extendedIdentitiesService.findOne(did); + + return identity.getConfidentialVenues(); + } + + public async getPendingAffirmsCount(transactionId: BigNumber): Promise { + const transaction = await this.findOne(transactionId); + + return transaction.getPendingAffirmsCount(); + } + + /** + * Given an ElGamal public key this method decrypts all asset amounts with the corresponding private key + */ + public async verifyTransactionAmounts( + transactionId: BigNumber, + params: VerifyTransactionAmountsDto + ): Promise { + const transaction = await this.findOne(transactionId); + const { proved, pending } = await transaction.getProofDetails(); + const publicKey = params.publicKey; + + const response: AuditorVerifyProofModel[] = []; + + pending.forEach(value => { + let isReceiver = false; + if (value.receiver.publicKey === publicKey) { + isReceiver = true; + } + + value.proofs.forEach(assetProof => { + const isAuditor = assetProof.auditors.map(auditor => auditor.publicKey).includes(publicKey); + + response.push({ + isProved: false, + isAuditor, + isReceiver, + amountDecrypted: false, + legId: value.legId, + assetId: assetProof.assetId, + amount: null, + isValid: null, + errMsg: null, + }); + }); + }); + + const auditorRequests: { + confidentialAccount: string; + params: AuditorVerifySenderProofDto; + trackers: { legId: BigNumber; assetId: string }; + }[] = []; + + const receiverRequests: { + confidentialAccount: string; + params: ReceiverVerifySenderProofDto; + trackers: { legId: BigNumber; assetId: string; isAuditor: boolean }; + }[] = []; + + proved.forEach(value => { + let isReceiver = false; + if (value.receiver.publicKey === publicKey) { + isReceiver = true; + } + + value.proofs.forEach(assetProof => { + const auditorIndex = assetProof.auditors.findIndex( + auditorKey => auditorKey.publicKey === publicKey + ); + + const isAuditor = auditorIndex >= 0; + + if (isReceiver) { + receiverRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + amount: null, + }, + trackers: { assetId: assetProof.assetId, legId: value.legId, isAuditor }, + }); + } else if (isAuditor) { + auditorRequests.push({ + confidentialAccount: publicKey, + params: { + senderProof: assetProof.proof, + auditorId: new BigNumber(auditorIndex), + amount: null, + }, + trackers: { assetId: assetProof.assetId, legId: value.legId }, + }); + } else { + response.push({ + isProved: true, + isAuditor: false, + isReceiver: false, + amountDecrypted: false, + legId: value.legId, + assetId: assetProof.assetId, + amount: null, + isValid: null, + errMsg: null, + }); + } + }); + }); + + const auditorResponses = await Promise.all( + auditorRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { + const proofResponse = await this.confidentialProofsService.verifySenderProofAsAuditor( + confidentialAccount, + proofParams + ); + + return { + proofResponse, + trackers, + }; + }) + ); + + const receiverResponses = await Promise.all( + receiverRequests.map(async ({ confidentialAccount, params: proofParams, trackers }) => { + const proofResponse = await this.confidentialProofsService.verifySenderProofAsReceiver( + confidentialAccount, + proofParams + ); + + return { + proofResponse, + trackers, + }; + }) + ); + + auditorResponses.forEach(({ proofResponse, trackers: { assetId, legId } }) => { + response.push({ + isProved: true, + isAuditor: true, + isReceiver: false, + amountDecrypted: true, + amount: proofResponse.amount, + assetId, + legId, + errMsg: proofResponse.errMsg, + isValid: proofResponse.isValid, + }); + }); + + receiverResponses.forEach(({ proofResponse, trackers: { assetId, legId, isAuditor } }) => { + response.push({ + isProved: true, + isAuditor, + isReceiver: true, + amountDecrypted: true, + amount: proofResponse.amount, + assetId, + legId, + errMsg: proofResponse.errMsg, + isValid: proofResponse.isValid, + }); + }); + + return response.sort((a, b) => a.legId.minus(b.legId).toNumber()); + } + + public async createdAt(id: BigNumber): Promise { + const transaction = await this.findOne(id); + + return transaction.createdAt(); + } +} diff --git a/src/confidential-transactions/confidential-transactions.util.ts b/src/confidential-transactions/confidential-transactions.util.ts new file mode 100644 index 00000000..8806cdfc --- /dev/null +++ b/src/confidential-transactions/confidential-transactions.util.ts @@ -0,0 +1,44 @@ +/* istanbul ignore file */ + +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { createConfidentialAccountModel } from '~/confidential-accounts/confidential-accounts.util'; +import { createConfidentialAssetModel } from '~/confidential-assets/confidential-assets.util'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; +import { ConfidentialTransactionModel } from '~/confidential-transactions/models/confidential-transaction.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; + +export async function createConfidentialTransactionModel( + transaction: ConfidentialTransaction +): Promise { + const [details, legsData] = await Promise.all([transaction.details(), transaction.getLegs()]); + + const { status, createdAt, venueId, memo } = details; + + const legs = legsData.map( + ({ id, sender, receiver, assetAuditors, mediators }) => + new ConfidentialLegModel({ + id, + sender: createConfidentialAccountModel(sender), + receiver: createConfidentialAccountModel(receiver), + mediators: mediators?.map(({ did }) => new IdentityModel({ did })), + assetAuditors: assetAuditors.map( + ({ asset, auditors }) => + new ConfidentialAssetAuditorModel({ + asset: createConfidentialAssetModel(asset), + auditors: auditors.map(auditor => createConfidentialAccountModel(auditor)), + }) + ), + }) + ); + + return new ConfidentialTransactionModel({ + id: transaction.id, + venueId, + memo, + status, + createdAt, + legs, + }); +} diff --git a/src/confidential-transactions/confidential-venues.controller.spec.ts b/src/confidential-transactions/confidential-venues.controller.spec.ts new file mode 100644 index 00000000..efbc0795 --- /dev/null +++ b/src/confidential-transactions/confidential-venues.controller.spec.ts @@ -0,0 +1,121 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialTransaction, + ConfidentialVenue, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialVenuesController } from '~/confidential-transactions/confidential-venues.controller'; +import { ConfidentialTransactionLegDto } from '~/confidential-transactions/dto/confidential-transaction-leg.dto'; +import { CreatedConfidentialTransactionModel } from '~/confidential-transactions/models/created-confidential-transaction.model'; +import { CreatedConfidentialVenueModel } from '~/confidential-transactions/models/created-confidential-venue.model'; +import { getMockTransaction, testValues } from '~/test-utils/consts'; +import { + createMockConfidentialTransaction, + createMockConfidentialVenue, + createMockIdentity, + createMockTransactionResult, +} from '~/test-utils/mocks'; +import { mockConfidentialTransactionsServiceProvider } from '~/test-utils/service-mocks'; + +const { signer, txResult } = testValues; + +describe('ConfidentialVenuesController', () => { + let controller: ConfidentialVenuesController; + let mockConfidentialTransactionsService: DeepMocked; + const id = new BigNumber(1); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfidentialVenuesController], + providers: [mockConfidentialTransactionsServiceProvider], + }).compile(); + + mockConfidentialTransactionsService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ConfidentialVenuesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getCreator', () => { + it('should get the creator of a Confidential Venue', async () => { + mockConfidentialTransactionsService.getVenueCreator.mockResolvedValue( + createMockIdentity({ did: 'CREATOR_DID' }) + ); + + const result = await controller.getCreator({ id }); + + expect(result).toEqual(expect.objectContaining({ did: 'CREATOR_DID' })); + }); + }); + + describe('createVenue', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + }; + + const mockVenue = createMockConfidentialVenue(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateVenue); + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockVenue, + }); + + when(mockConfidentialTransactionsService.createConfidentialVenue) + .calledWith(input) + .mockResolvedValue(testTxResult); + + const result = await controller.createVenue(input); + expect(result).toEqual( + new CreatedConfidentialVenueModel({ + ...txResult, + transactions: [transaction], + confidentialVenue: mockVenue, + }) + ); + }); + }); + + describe('createConfidentialTransaction', () => { + it('should call the service and return the results', async () => { + const input = { + signer, + legs: 'some_legs' as unknown as ConfidentialTransactionLegDto[], + memo: 'some_memo', + }; + + const venueId = new BigNumber(1); + + const mockResult = createMockConfidentialTransaction(); + const transaction = getMockTransaction(TxTags.confidentialAsset.CreateVenue); + const testTxResult = createMockTransactionResult({ + ...txResult, + transactions: [transaction], + result: mockResult, + }); + + when(mockConfidentialTransactionsService.createConfidentialTransaction) + .calledWith(venueId, input) + .mockResolvedValue(testTxResult); + + const result = await controller.createConfidentialTransaction({ id: venueId }, input); + expect(result).toEqual( + new CreatedConfidentialTransactionModel({ + ...txResult, + transactions: [transaction], + confidentialTransaction: mockResult, + }) + ); + }); + }); +}); diff --git a/src/confidential-transactions/confidential-venues.controller.ts b/src/confidential-transactions/confidential-venues.controller.ts new file mode 100644 index 00000000..a94381fa --- /dev/null +++ b/src/confidential-transactions/confidential-venues.controller.ts @@ -0,0 +1,113 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { + ConfidentialTransaction, + ConfidentialVenue, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { CreateConfidentialTransactionDto } from '~/confidential-transactions/dto/create-confidential-transaction.dto'; +import { CreatedConfidentialTransactionModel } from '~/confidential-transactions/models/created-confidential-transaction.model'; +import { CreatedConfidentialVenueModel } from '~/confidential-transactions/models/created-confidential-venue.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { ApiTransactionResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { IdParamsDto } from '~/polymesh-rest-api/src/common/dto/id-params.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; +import { + handleServiceResult, + TransactionResolver, + TransactionResponseModel, +} from '~/polymesh-rest-api/src/common/utils/functions'; +import { CreatedInstructionModel } from '~/polymesh-rest-api/src/settlements/models/created-instruction.model'; + +@ApiTags('confidential-venues') +@Controller('confidential-venues') +export class ConfidentialVenuesController { + constructor(private readonly confidentialTransactionsService: ConfidentialTransactionsService) {} + + @ApiOperation({ + summary: 'Get creator', + description: 'This endpoint retrieves the creator of a Confidential Venue', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Venue', + type: 'string', + example: '1', + }) + @ApiOkResponse({ + description: 'DID of the creator of the Confidential Venue', + type: IdentityModel, + }) + @Get(':id/creator') + public async getCreator(@Param() { id }: IdParamsDto): Promise { + const { did } = await this.confidentialTransactionsService.getVenueCreator(id); + + return new IdentityModel({ did }); + } + + @ApiOperation({ + summary: 'Create a Confidential Venue', + description: 'This endpoint allows for the creation of a new Confidential Venue', + }) + @ApiTransactionResponse({ + description: 'Details about the transaction', + type: TransactionQueueModel, + }) + @Post('create') + public async createVenue(@Body() params: TransactionBaseDto): Promise { + const result = await this.confidentialTransactionsService.createConfidentialVenue(params); + + const resolver: TransactionResolver = ({ + result: confidentialVenue, + transactions, + details, + }) => + new CreatedConfidentialVenueModel({ + confidentialVenue, + transactions, + details, + }); + + return handleServiceResult(result, resolver); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Create a new Confidential Transaction', + }) + @ApiParam({ + name: 'id', + description: 'The ID of the Confidential Venue', + type: 'string', + example: '123', + }) + @ApiOkResponse({ + description: 'The ID of the newly created Confidential Transaction', + type: CreatedInstructionModel, + }) + @Post(':id/transactions/create') + public async createConfidentialTransaction( + @Param() { id }: IdParamsDto, + @Body() createConfidentialTransactionDto: CreateConfidentialTransactionDto + ): Promise { + const serviceResult = await this.confidentialTransactionsService.createConfidentialTransaction( + id, + createConfidentialTransactionDto + ); + + const resolver: TransactionResolver = ({ + result: confidentialTransaction, + transactions, + details, + }) => + new CreatedConfidentialTransactionModel({ + confidentialTransaction, + details, + transactions, + }); + + return handleServiceResult(serviceResult, resolver); + } +} diff --git a/src/confidential-transactions/dto/confidential-leg-amount.dto.ts b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts new file mode 100644 index 00000000..8f99866b --- /dev/null +++ b/src/confidential-transactions/dto/confidential-leg-amount.dto.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; + +export class ConfidentialLegAmountDto { + @ApiProperty({ + description: 'Then Confidential Asset ID whose amount is being specified', + type: 'string', + example: '76702175-d8cb-e3a5-5a19-734433351e25', + }) + @IsConfidentialAssetId() + readonly confidentialAsset: string; + + @ApiProperty({ + description: 'Amount to be transferred', + type: 'string', + example: '1000', + }) + @ToBigNumber() + @IsBigNumber({ min: 0 }) + readonly amount: BigNumber; +} diff --git a/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts new file mode 100644 index 00000000..3ee5c3a6 --- /dev/null +++ b/src/confidential-transactions/dto/confidential-transaction-leg.dto.ts @@ -0,0 +1,58 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; + +import { IsConfidentialAssetId } from '~/confidential-assets/decorators/validation'; +import { IsDid } from '~/polymesh-rest-api/src/common/decorators/validation'; + +export class ConfidentialTransactionLegDto { + @ApiProperty({ + description: + 'The confidential Asset IDs for this leg of the transaction. Amounts are specified in the later proof generation steps', + type: 'string', + isArray: true, + example: ['76702175-d8cb-e3a5-5a19-734433351e25'], + }) + @IsArray() + @IsConfidentialAssetId({ each: true }) + readonly assets: string[]; + + @ApiProperty({ + description: + 'The Confidential Account from which the Confidential Assets will be withdrawn from', + type: 'string', + example: '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + }) + @IsString() + readonly sender: string; + + @ApiProperty({ + description: 'The Confidential Account from which the Confidential Assets will be deposited', + type: 'string', + example: '0xdeadbeef11111111111111111111111111111111111111111111111111111111', + }) + @IsString() + readonly receiver: string; + + @ApiPropertyOptional({ + description: 'The Confidential Accounts of the auditors of the transaction leg', + type: 'string', + isArray: true, + example: ['0x7e9cf42766e08324c015f183274a9e977706a59a28d64f707e410a03563be77d'], + }) + @IsArray() + @IsString({ each: true }) + readonly auditors: string[] = []; + + @ApiPropertyOptional({ + description: 'The DID of mediators of the transaction leg', + type: 'string', + isArray: true, + example: ['0x0600000000000000000000000000000000000000000000000000000000000000'], + }) + @IsOptional() + @IsArray() + @IsDid({ each: true }) + readonly mediators: string[] = []; +} diff --git a/src/confidential-transactions/dto/create-confidential-transaction.dto.ts b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts new file mode 100644 index 00000000..37c75303 --- /dev/null +++ b/src/confidential-transactions/dto/create-confidential-transaction.dto.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsByteLength, IsOptional, IsString, ValidateNested } from 'class-validator'; + +import { ConfidentialTransactionLegDto } from '~/confidential-transactions/dto/confidential-transaction-leg.dto'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class CreateConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'List of Confidential Asset movements', + type: ConfidentialTransactionLegDto, + isArray: true, + }) + @ValidateNested({ each: true }) + @Type(() => ConfidentialTransactionLegDto) + readonly legs: ConfidentialTransactionLegDto[]; + + @ApiPropertyOptional({ + description: 'Identifier string to help differentiate transactions. Maximum 32 bytes', + type: 'string', + example: 'Transfer of GROWTH Asset', + }) + @IsOptional() + @IsString() + @IsByteLength(0, 32) + readonly memo?: string; +} diff --git a/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts new file mode 100644 index 00000000..a79bb17c --- /dev/null +++ b/src/confidential-transactions/dto/observer-affirm-confidential-transaction.dto.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialAffirmParty } from '@polymeshassociation/polymesh-private-sdk/types'; +import { IsEnum } from 'class-validator'; + +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class ObserverAffirmConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Index of the leg to be affirmed in the Confidential Transaction', + type: 'string', + example: '1', + }) + @ToBigNumber() + @IsBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'Affirming party', + example: ConfidentialAffirmParty.Receiver, + }) + @IsEnum(ConfidentialAffirmParty) + readonly party: ConfidentialAffirmParty.Receiver | ConfidentialAffirmParty.Mediator; +} diff --git a/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts new file mode 100644 index 00000000..1c648e92 --- /dev/null +++ b/src/confidential-transactions/dto/sender-affirm-confidential-transaction.dto copy.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Type } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; + +import { ConfidentialLegAmountDto } from '~/confidential-transactions/dto/confidential-leg-amount.dto'; +import { ToBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { IsBigNumber } from '~/polymesh-rest-api/src/common/decorators/validation'; +import { TransactionBaseDto } from '~/polymesh-rest-api/src/common/dto/transaction-base-dto'; + +export class SenderAffirmConfidentialTransactionDto extends TransactionBaseDto { + @ApiProperty({ + description: 'Index of the leg to be affirmed in the Confidential Transaction', + type: 'string', + example: '1', + }) + @ToBigNumber() + @IsBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'List of confidential Asset IDs along with their transfer amount', + type: ConfidentialLegAmountDto, + isArray: true, + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConfidentialLegAmountDto) + readonly legAmounts: ConfidentialLegAmountDto[]; +} diff --git a/src/confidential-transactions/models/confidential-affirmation.model.ts b/src/confidential-transactions/models/confidential-affirmation.model.ts new file mode 100644 index 00000000..86b0852a --- /dev/null +++ b/src/confidential-transactions/models/confidential-affirmation.model.ts @@ -0,0 +1,49 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialLegParty, + ConfidentialTransaction, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { + FromBigNumber, + FromEntity, +} from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialAffirmationModel { + @ApiProperty({ + description: 'Confidential Asset ID being transferred in the leg', + type: 'string', + example: '10', + }) + @FromEntity() + readonly transaction: ConfidentialTransaction; + + @ApiProperty({ + description: 'Index of the leg for which the affirmation was given', + type: 'string', + example: '0', + }) + @FromBigNumber() + readonly legId: BigNumber; + + @ApiProperty({ + description: 'Affirming party', + type: ConfidentialLegParty, + example: ConfidentialLegParty.Auditor, + }) + readonly role: ConfidentialLegParty; + + @ApiProperty({ + description: 'Indicates whether the leg was affirmed or not', + type: 'boolean', + example: true, + }) + readonly affirmed: boolean; + + constructor(model: ConfidentialAffirmationModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/confidential-asset-auditor.model.ts b/src/confidential-transactions/models/confidential-asset-auditor.model.ts new file mode 100644 index 00000000..d8a6aeb9 --- /dev/null +++ b/src/confidential-transactions/models/confidential-asset-auditor.model.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetModel } from '~/confidential-assets/models/confidential-asset.model'; + +export class ConfidentialAssetAuditorModel { + @ApiProperty({ + description: 'Confidential Asset ID being transferred in the leg', + type: ConfidentialAssetModel, + }) + @Type(() => ConfidentialAssetModel) + readonly asset: ConfidentialAssetModel; + + @ApiProperty({ + description: 'List of auditor Confidential Accounts for the `asset`', + type: ConfidentialAccountModel, + isArray: true, + }) + @Type(() => ConfidentialAccountModel) + readonly auditors: ConfidentialAccountModel[]; + + constructor(model: ConfidentialAssetAuditorModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/confidential-leg.model.ts b/src/confidential-transactions/models/confidential-leg.model.ts new file mode 100644 index 00000000..59630292 --- /dev/null +++ b/src/confidential-transactions/models/confidential-leg.model.ts @@ -0,0 +1,55 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { Type } from 'class-transformer'; + +import { ConfidentialAccountModel } from '~/confidential-accounts/models/confidential-account.model'; +import { ConfidentialAssetAuditorModel } from '~/confidential-transactions/models/confidential-asset-auditor.model'; +import { IdentityModel } from '~/extended-identities/models/identity.model'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialLegModel { + @ApiProperty({ + description: 'The index of this leg in the Confidential Transaction', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + @ApiProperty({ + description: 'Confidential Account from which the transfer is to be made', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly sender: ConfidentialAccountModel; + + @ApiProperty({ + description: 'Confidential Account to which the transfer is to be made', + type: ConfidentialAccountModel, + }) + @Type(() => ConfidentialAccountModel) + readonly receiver: ConfidentialAccountModel; + + @ApiProperty({ + description: 'List of mediator identities configured for this leg', + type: IdentityModel, + isArray: true, + }) + @Type(() => IdentityModel) + readonly mediators: IdentityModel[]; + + @ApiProperty({ + description: + 'Auditor Confidential Accounts for the leg, grouped by asset they are auditors for', + type: ConfidentialAssetAuditorModel, + isArray: true, + }) + @Type(() => ConfidentialAssetAuditorModel) + readonly assetAuditors: ConfidentialAssetAuditorModel[]; + + constructor(model: ConfidentialLegModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/confidential-transaction.model.ts b/src/confidential-transactions/models/confidential-transaction.model.ts new file mode 100644 index 00000000..e118c423 --- /dev/null +++ b/src/confidential-transactions/models/confidential-transaction.model.ts @@ -0,0 +1,61 @@ +/* istanbul ignore file */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialTransactionStatus } from '@polymeshassociation/polymesh-private-sdk/types'; +import { Type } from 'class-transformer'; + +import { ConfidentialLegModel } from '~/confidential-transactions/models/confidential-leg.model'; +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class ConfidentialTransactionModel { + @ApiProperty({ + description: 'The ID of the Confidential Transaction', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly id: BigNumber; + + @ApiProperty({ + description: 'ID of the Confidential Venue through which the settlement is handled', + type: 'string', + example: '123', + }) + @FromBigNumber() + readonly venueId: BigNumber; + + @ApiProperty({ + description: 'Block number at which the Confidential Transaction was created', + type: 'string', + example: '100000', + }) + @FromBigNumber() + readonly createdAt: BigNumber; + + @ApiProperty({ + description: 'The current status of the Confidential Transaction', + type: 'string', + enum: ConfidentialTransactionStatus, + example: ConfidentialTransactionStatus.Pending, + }) + readonly status: ConfidentialTransactionStatus; + + @ApiPropertyOptional({ + description: 'Identifier string provided while creating the Confidential Transaction', + example: 'Transfer of GROWTH Asset', + }) + readonly memo?: string; + + @ApiProperty({ + description: 'List of legs in the Confidential Transaction', + type: ConfidentialLegModel, + isArray: true, + }) + @Type(() => ConfidentialLegModel) + readonly legs: ConfidentialLegModel[]; + + constructor(model: ConfidentialTransactionModel) { + Object.assign(this, model); + } +} diff --git a/src/confidential-transactions/models/created-confidential-transaction.model.ts b/src/confidential-transactions/models/created-confidential-transaction.model.ts new file mode 100644 index 00000000..81d5dced --- /dev/null +++ b/src/confidential-transactions/models/created-confidential-transaction.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialTransaction } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; + +export class CreatedConfidentialTransactionModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created Confidential Transaction', + example: '123', + }) + @FromEntity() + readonly confidentialTransaction: ConfidentialTransaction; + + constructor(model: CreatedConfidentialTransactionModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-transactions/models/created-confidential-venue.model.ts b/src/confidential-transactions/models/created-confidential-venue.model.ts new file mode 100644 index 00000000..5505a481 --- /dev/null +++ b/src/confidential-transactions/models/created-confidential-venue.model.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { FromEntity } from '~/polymesh-rest-api/src/common/decorators/transformation'; +import { TransactionQueueModel } from '~/polymesh-rest-api/src/common/models/transaction-queue.model'; + +export class CreatedConfidentialVenueModel extends TransactionQueueModel { + @ApiProperty({ + type: 'string', + description: 'ID of the newly created Confidential Venue', + example: '123', + }) + @FromEntity() + readonly confidentialVenue: ConfidentialVenue; + + constructor(model: CreatedConfidentialVenueModel) { + const { transactions, details, ...rest } = model; + super({ transactions, details }); + + Object.assign(this, rest); + } +} diff --git a/src/confidential-transactions/types.ts b/src/confidential-transactions/types.ts new file mode 100644 index 00000000..c56e7e4f --- /dev/null +++ b/src/confidential-transactions/types.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +export enum ConfidentialTransactionDirectionEnum { + All = 'All', + Incoming = 'Incoming', + Outgoing = 'Outgoing', +} diff --git a/src/corporate-actions/corporate-actions.controller.spec.ts b/src/corporate-actions/corporate-actions.controller.spec.ts deleted file mode 100644 index a15365b7..00000000 --- a/src/corporate-actions/corporate-actions.controller.spec.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { ResultsModel } from '~/common/models/results.model'; -import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { - createDividendDistributionDetailsModel, - createDividendDistributionModel, -} from '~/corporate-actions/corporate-actions.util'; -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; -import { testValues } from '~/test-utils/consts'; - -const { did, signer, txResult } = testValues; - -describe('CorporateActionsController', () => { - let controller: CorporateActionsController; - - const mockCorporateActionsService = { - findDefaultConfigByTicker: jest.fn(), - updateDefaultConfigByTicker: jest.fn(), - findDistributionsByTicker: jest.fn(), - findDistribution: jest.fn(), - createDividendDistribution: jest.fn(), - remove: jest.fn(), - payDividends: jest.fn(), - claimDividends: jest.fn(), - linkDocuments: jest.fn(), - reclaimRemainingFunds: jest.fn(), - modifyCheckpoint: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CorporateActionsController], - providers: [CorporateActionsService], - }) - .overrideProvider(CorporateActionsService) - .useValue(mockCorporateActionsService) - .compile(); - - controller = module.get(CorporateActionsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getDefaultConfig', () => { - it('should return the Corporate Action Default Config for an Asset', async () => { - const mockCorporateActionDefaultConfig = new MockCorporateActionDefaultConfig(); - - mockCorporateActionsService.findDefaultConfigByTicker.mockResolvedValue( - mockCorporateActionDefaultConfig - ); - - const result = await controller.getDefaultConfig({ ticker: 'TICKER' }); - - expect(result).toEqual(mockCorporateActionDefaultConfig); - }); - }); - - describe('updateDefaultConfig', () => { - it('should update the Corporate Action Default Config and return the details of transaction', async () => { - mockCorporateActionsService.updateDefaultConfigByTicker.mockResolvedValue(txResult); - const body = { - signer, - defaultTaxWithholding: new BigNumber(25), - }; - - const result = await controller.updateDefaultConfig({ ticker: 'TICKER' }, body); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.updateDefaultConfigByTicker).toHaveBeenCalledWith( - 'TICKER', - body - ); - }); - }); - - describe('getDividendDistributions', () => { - it('should return the Dividend Distributions associated with an Asset', async () => { - const mockDistributions = [new MockDistributionWithDetails()]; - - mockCorporateActionsService.findDistributionsByTicker.mockResolvedValue(mockDistributions); - - const result = await controller.getDividendDistributions({ ticker: 'TICKER' }); - - expect(result).toEqual( - new ResultsModel({ - results: mockDistributions.map(distributionWithDetails => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createDividendDistributionDetailsModel(distributionWithDetails as any) - ), - }) - ); - }); - }); - - describe('findDistribution', () => { - it('should return a specific Dividend Distribution associated with an Asset', async () => { - const mockDistribution = new MockDistributionWithDetails(); - - mockCorporateActionsService.findDistribution.mockResolvedValue(mockDistribution); - - const result = await controller.getDividendDistribution({ - ticker: 'TICKER', - id: new BigNumber(1), - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(result).toEqual(createDividendDistributionDetailsModel(mockDistribution as any)); - }); - }); - - describe('createDividendDistribution', () => { - it('should call the service and return the results', async () => { - const mockDistribution = new MockDistribution(); - const response = { - result: mockDistribution, - ...txResult, - }; - mockCorporateActionsService.createDividendDistribution.mockResolvedValue(response); - const mockDate = new Date(); - const body = { - signer, - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: new BigNumber(0), - currency: 'TICKER', - perShare: new BigNumber(2), - maxAmount: new BigNumber(1000), - paymentDate: mockDate, - }; - - const result = await controller.createDividendDistribution({ ticker: 'TICKER' }, body); - - expect(result).toEqual({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dividendDistribution: createDividendDistributionModel(mockDistribution as any), - transactions: txResult.transactions, - details: txResult.details, - }); - expect(mockCorporateActionsService.createDividendDistribution).toHaveBeenCalledWith( - 'TICKER', - body - ); - }); - }); - - describe('deleteCorporateAction', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.remove.mockResolvedValue(txResult); - - const result = await controller.deleteCorporateAction( - { id: new BigNumber(1), ticker: 'TICKER' }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.remove).toHaveBeenCalledWith('TICKER', new BigNumber(1), { - signer, - }); - }); - }); - - describe('payDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.payDividends.mockResolvedValue(txResult); - - const body = { - signer, - targets: [did], - }; - const result = await controller.payDividends( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - body - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.payDividends).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - body - ); - }); - }); - - describe('linkDocuments', () => { - it('should call the service and return the results', async () => { - const body = { - documents: [ - new AssetDocumentDto({ - name: 'DOC_NAME', - uri: 'DOC_URI', - type: 'DOC_TYPE', - }), - ], - signer, - }; - - mockCorporateActionsService.linkDocuments.mockResolvedValue(txResult); - - const result = await controller.linkDocuments( - { ticker: 'TICKER', id: new BigNumber(1) }, - body - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('claimDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.claimDividends.mockResolvedValue(txResult); - - const result = await controller.claimDividends( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.claimDividends).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - { signer } - ); - }); - }); - - describe('reclaimDividends', () => { - it('should call the service and return the transaction details', async () => { - mockCorporateActionsService.reclaimRemainingFunds.mockResolvedValue(txResult); - - const result = await controller.reclaimRemainingFunds( - { - id: new BigNumber(1), - ticker: 'TICKER', - }, - { signer } - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.reclaimRemainingFunds).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - { signer } - ); - }); - }); - - describe('modifyCheckpoint', () => { - it('should call the service and return the results', async () => { - const body = { - checkpoint: new Date(), - signer, - }; - - mockCorporateActionsService.modifyCheckpoint.mockResolvedValue(txResult); - - const result = await controller.modifyDistributionCheckpoint( - { ticker: 'TICKER', id: new BigNumber(1) }, - body - ); - - expect(result).toEqual(txResult); - expect(mockCorporateActionsService.modifyCheckpoint).toHaveBeenCalledWith( - 'TICKER', - new BigNumber(1), - body - ); - }); - }); -}); diff --git a/src/corporate-actions/corporate-actions.controller.ts b/src/corporate-actions/corporate-actions.controller.ts deleted file mode 100644 index 2987d3e9..00000000 --- a/src/corporate-actions/corporate-actions.controller.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { DividendDistribution } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { - createDividendDistributionDetailsModel, - createDividendDistributionModel, -} from '~/corporate-actions/corporate-actions.util'; -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { LinkDocumentsDto } from '~/corporate-actions/dto/link-documents.dto'; -import { ModifyDistributionCheckpointDto } from '~/corporate-actions/dto/modify-distribution-checkpoint.dto'; -import { PayDividendsDto } from '~/corporate-actions/dto/pay-dividends.dto'; -import { CorporateActionDefaultConfigModel } from '~/corporate-actions/models/corporate-action-default-config.model'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { CreatedDividendDistributionModel } from '~/corporate-actions/models/created-dividend-distribution.model'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; -import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -class DividendDistributionParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class DeleteCorporateActionParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -class DistributeFundsParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('corporate-actions', 'assets') -@Controller('assets/:ticker/corporate-actions') -export class CorporateActionsController { - constructor(private readonly corporateActionsService: CorporateActionsService) {} - - @ApiOperation({ - summary: 'Fetch Corporate Action Default Config', - description: - "This endpoint will provide the default target Identities, global tax withholding percentage, and per-Identity tax withholding percentages for the Asset's Corporate Actions. Any Corporate Action that is created will use these values unless they are explicitly overridden", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action Default Config is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Corporate Action Default Config for the specified Asset', - type: CorporateActionDefaultConfigModel, - }) - @Get('default-config') - public async getDefaultConfig( - @Param() { ticker }: TickerParamsDto - ): Promise { - const { targets, defaultTaxWithholding, taxWithholdings } = - await this.corporateActionsService.findDefaultConfigByTicker(ticker); - return new CorporateActionDefaultConfigModel({ - targets: new CorporateActionTargetsModel(targets), - defaultTaxWithholding, - taxWithholdings: taxWithholdings.map( - taxWithholding => new TaxWithholdingModel(taxWithholding) - ), - }); - } - - @ApiOperation({ - summary: 'Update Corporate Action Default Config', - description: - "This endpoint updates the default target Identities, global tax withholding percentage, and per-Identity tax withholding percentages for the Asset's Corporate Actions. Any Corporate Action that is created will use these values unless they are explicitly overridden", - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action Default Config is to be updated', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post('default-config/modify') - public async updateDefaultConfig( - @Param() { ticker }: TickerParamsDto, - @Body() corporateActionDefaultConfigDto: CorporateActionDefaultConfigDto - ): Promise { - const result = await this.corporateActionsService.updateDefaultConfigByTicker( - ticker, - corporateActionDefaultConfigDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Fetch Dividend Distributions', - description: - 'This endpoint will provide the list of Dividend Distributions associated with an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distributions are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(DividendDistributionDetailsModel, { - description: 'List of Dividend Distributions associated with the specified Asset', - paginated: false, - }) - @Get('dividend-distributions') - public async getDividendDistributions( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const results = await this.corporateActionsService.findDistributionsByTicker(ticker); - return new ResultsModel({ - results: results.map(distributionWithDetails => - createDividendDistributionDetailsModel(distributionWithDetails) - ), - }); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Fetch a Dividend Distribution', - description: - 'This endpoint will provide a specific Dividend Distribution associated with an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distribution is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Dividend Distribution', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'The details of the Dividend Distribution', - type: DividendDistributionModel, - }) - @Get('dividend-distributions/:id') - public async getDividendDistribution( - @Param() { ticker, id }: DividendDistributionParamsDto - ): Promise { - const result = await this.corporateActionsService.findDistribution(ticker, id); - return createDividendDistributionDetailsModel(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Create a Dividend Distribution', - description: - 'This endpoint will create a Dividend Distribution for a subset of the Asset holders at a certain (existing or future) Checkpoint', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which a Dividend Distribution is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Dividend Distribution', - type: CreatedDividendDistributionModel, - }) - @ApiBadRequestResponse({ - description: - '
    ' + - '
  • Payment date must be in the future
  • ' + - '
  • Expiry date must be after payment date
  • ' + - '
  • Declaration date must be in the past
  • ' + - '
  • Payment date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Expiry date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Checkpoint date must be in the future when passing a Date instead of an existing Checkpoint
  • ' + - '
', - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The origin Portfolio's free balance is not enough to cover the Distribution amount
  • " + - '
  • The Distribution has already expired
  • ' + - '
', - }) - @ApiNotFoundResponse({ - description: - '
    ' + - "
  • Checkpoint doesn't exist
  • " + - "
  • Checkpoint Schedule doesn't exist
  • " + - '
  • Cannot distribute Dividends using the Asset as currency
  • ' + - '
', - }) - @Post('dividend-distributions/create') - public async createDividendDistribution( - @Param() { ticker }: TickerParamsDto, - @Body() dividendDistributionDto: DividendDistributionDto - ): Promise { - const serviceResult = await this.corporateActionsService.createDividendDistribution( - ticker, - dividendDistributionDto - ); - - const resolver: TransactionResolver = ({ - transactions, - result, - details, - }) => - new CreatedDividendDistributionModel({ - dividendDistribution: createDividendDistributionModel(result), - transactions, - details, - }); - - return handleServiceResult(serviceResult, resolver); - } - - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete a Corporate Action', - description: 'This endpoint deletes a Corporate Action of a specific Asset', - }) - @ApiParam({ - name: 'id', - description: 'Corporate Action number to be deleted', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Corporate Action is to be deleted', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: "The Corporate Action doesn't exist", - }) - @Post(':id/delete') - public async deleteCorporateAction( - @Param() { id, ticker }: DeleteCorporateActionParamsDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.remove(ticker, id, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Pay dividends for a Dividend Distribution', - description: 'This endpoint transfers unclaimed dividends to a list of target Identities', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: - '
    ' + - "
  • The Distribution's payment date hasn't been reached
  • " + - '
  • The Distribution has already expired
  • ' + - '
  • Some of the supplied Identities have already either been paid or claimed their share of the Distribution
  • ' + - '
  • Some of the supplied Identities are not included in this Distribution
  • ' + - '
', - }) - @Post('dividend-distributions/:id/payments/pay') - public async payDividends( - @Param() { id, ticker }: DistributeFundsParamsDto, - @Body() payDividendsDto: PayDividendsDto - ): Promise { - const result = await this.corporateActionsService.payDividends(ticker, id, payDividendsDto); - return handleServiceResult(result); - } - - // TODO @prashantasdeveloper: Update error responses post handling error codes - @ApiOperation({ - summary: 'Link documents to a Corporate Action', - description: - 'This endpoint links a list of documents to the Corporate Action. Any previous links are removed in favor of the new list. All the documents to be linked should already be linked to the Asset of the Corporate Action.', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset to which the documents are attached', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Corporate Action', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'Some of the provided documents are not associated with the Asset', - }) - @Post(':id/documents/link') - public async linkDocuments( - @Param() { ticker, id }: DividendDistributionParamsDto, - @Body() linkDocumentsDto: LinkDocumentsDto - ): Promise { - const result = await this.corporateActionsService.linkDocuments(ticker, id, linkDocumentsDto); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Claim dividend payment for a Dividend Distribution', - description: - 'This endpoint allows a target Identity of a Dividend distribution to claim their unclaimed Dividends', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be claimed', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - "
  • The Distribution's payment date hasn't been reached
  • " + - '
  • The Distribution has already expired
  • ' + - '
  • The current Identity is not included in this Distribution
  • ' + - '
  • The current Identity has already claimed dividends
  • ' + - '
', - }) - @Post(':id/payments/claim') - public async claimDividends( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.claimDividends( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions') - @ApiOperation({ - summary: 'Reclaim remaining funds of a Dividend Distribution', - description: - 'This endpoint reclaims any remaining funds back to the origin Portfolio from which the initial dividend funds came from. This can only be done after the Distribution has expired', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which dividends are to be reclaimed', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the expired Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • The Distribution must be expired
  • ' + - '
  • Distribution funds have already been reclaimed
  • ' + - '
', - }) - @Post(':id/reclaim-funds') - public async reclaimRemainingFunds( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.corporateActionsService.reclaimRemainingFunds( - ticker, - id, - transactionBaseDto - ); - return handleServiceResult(result); - } - - @ApiTags('dividend-distributions', 'checkpoints') - @ApiOperation({ - summary: 'Modify the Checkpoint of a Dividend Distribution', - description: - 'This endpoint modifies the Checkpoint of a Dividend Distribution. The Checkpoint can be modified only if the payment period for the Distribution has not yet started', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Dividend Distribution Checkpoint is to be modified', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: - 'The Corporate Action number for the the Dividend Distribution (Dividend Distribution ID)', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: - 'The Checkpoint date must be in the future when passing a Date instead of an existing Checkpoint', - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • Distribution is already in its payment period
  • ' + - '
  • Payment date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
  • Expiry date must be after the Checkpoint date when passing a Date instead of an existing Checkpoint
  • ' + - '
', - }) - @ApiNotFoundResponse({ - description: - '
    ' + - "
  • Checkpoint doesn't exist
  • " + - "
  • Checkpoint Schedule doesn't exist
  • " + - '
', - }) - @Post('dividend-distributions/:id/modify-checkpoint') - public async modifyDistributionCheckpoint( - @Param() { id, ticker }: DividendDistributionParamsDto, - @Body() modifyDistributionCheckpointDto: ModifyDistributionCheckpointDto - ): Promise { - const result = await this.corporateActionsService.modifyCheckpoint( - ticker, - id, - modifyDistributionCheckpointDto - ); - return handleServiceResult(result); - } -} diff --git a/src/corporate-actions/corporate-actions.module.ts b/src/corporate-actions/corporate-actions.module.ts deleted file mode 100644 index 711e0134..00000000 --- a/src/corporate-actions/corporate-actions.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { CorporateActionsController } from '~/corporate-actions/corporate-actions.controller'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [AssetsModule, TransactionsModule], - controllers: [CorporateActionsController], - providers: [CorporateActionsService], -}) -export class CorporateActionsModule {} diff --git a/src/corporate-actions/corporate-actions.service.spec.ts b/src/corporate-actions/corporate-actions.service.spec.ts deleted file mode 100644 index 3f8d84d4..00000000 --- a/src/corporate-actions/corporate-actions.service.spec.ts +++ /dev/null @@ -1,406 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CaCheckpointType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { CorporateActionsService } from '~/corporate-actions/corporate-actions.service'; -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockDistributionWithDetails } from '~/corporate-actions/mocks/distribution-with-details.mock'; -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockAsset, MockTransaction } from '~/test-utils/mocks'; -import { MockAssetService, mockTransactionsProvider } from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('CorporateActionsService', () => { - let service: CorporateActionsService; - - const mockAssetsService = new MockAssetService(); - - const mockTransactionsService = mockTransactionsProvider.useValue; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CorporateActionsService, AssetsService, mockTransactionsProvider], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(CorporateActionsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - describe('findDefaultConfigByTicker', () => { - it('should return the Corporate Action Default Config for an Asset', async () => { - const mockCorporateActionDefaultConfig = new MockCorporateActionDefaultConfig(); - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.getDefaultConfig.mockResolvedValue( - mockCorporateActionDefaultConfig - ); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDefaultConfigByTicker('TICKER'); - - expect(result).toEqual(mockCorporateActionDefaultConfig); - }); - }); - - describe('updateDefaultConfigByTicker', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a setDefaultConfig procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.SetDefaultWithholdingTax, - }; - const mockTransaction = new MockTransaction(transaction); - - mockAsset.corporateActions.setDefaultConfig.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - defaultTaxWithholding: new BigNumber(25), - }; - const result = await service.updateDefaultConfigByTicker(ticker, body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.corporateActions.setDefaultConfig, - { defaultTaxWithholding: new BigNumber(25) }, - { signer } - ); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('findDistributionsByTicker', () => { - it('should return the Dividend Distributions associated with an Asset', async () => { - const mockDistributions = [new MockDistributionWithDetails()]; - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.distributions.get.mockResolvedValue(mockDistributions); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDistributionsByTicker('TICKER'); - - expect(result).toEqual(mockDistributions); - }); - }); - - describe('findDistribution', () => { - it('should return a specific Dividend Distribution associated with an given ticker', async () => { - const mockDistributions = new MockDistributionWithDetails(); - - const mockAsset = new MockAsset(); - mockAsset.corporateActions.distributions.getOne.mockResolvedValue(mockDistributions); - - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findDistribution('TICKER', new BigNumber(1)); - - expect(result).toEqual(mockDistributions); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockAsset = new MockAsset(); - const mockError = new Error('Some Error'); - mockAsset.corporateActions.distributions.getOne.mockRejectedValue(mockError); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => - service.findDistribution('TICKER', new BigNumber(1)) - ).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('createDividendDistribution', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - const mockDate = new Date(); - const body = { - signer, - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: new BigNumber(0), - currency: 'TICKER', - perShare: new BigNumber(2), - maxAmount: new BigNumber(1000), - paymentDate: mockDate, - }; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a configureDividendDistribution procedure and return the created Dividend Distribution', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.InitiateCorporateActionAndDistribute, - }; - const mockTransaction = new MockTransaction(transaction); - const mockDistribution = new MockDistribution(); - mockTransactionsService.submit.mockResolvedValue({ - result: mockDistribution, - transactions: [mockTransaction], - }); - - const result = await service.createDividendDistribution(ticker, body); - - expect(result).toEqual({ - result: mockDistribution, - transactions: [mockTransaction], - }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('remove', () => { - let mockAsset: MockAsset; - const ticker = 'TICKER'; - - beforeEach(() => { - mockAsset = new MockAsset(); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - }); - - it('should run a remove procedure and return the delete the Corporate Action', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.RemoveCa, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const result = await service.remove(ticker, new BigNumber(1), { signer }); - - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockAssetsService.findFungible).toHaveBeenCalledWith(ticker); - }); - }); - - describe('payDividends', () => { - it('should call the pay procedure and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.PushBenefit, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const body = { - signer, - targets: ['0x6'.padEnd(66, '1')], - }; - - const result = await service.payDividends('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.pay, - { - targets: body.targets, - }, - { - signer, - } - ); - }); - }); - - describe('linkDocuments', () => { - const body = { - documents: [ - new AssetDocumentDto({ - name: 'DOC_NAME', - uri: 'DOC_URI', - type: 'DOC_TYPE', - }), - ], - signer, - }; - - it('should run the linkDocuments procedure and return the queue results', async () => { - const mockDistributionWithDetails = new MockDistributionWithDetails(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.LinkCaDoc, - }; - const mockTransaction = new MockTransaction(transaction); - mockDistributionWithDetails.distribution.linkDocuments.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(mockDistributionWithDetails as any); - - const result = await service.linkDocuments('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('claimDividends', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.Claim, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - distributionWithDetails.distribution.claim.mockResolvedValue(mockTransaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const result = await service.claimDividends('TICKER', new BigNumber(1), { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.claim, - undefined, - { - signer, - } - ); - }); - }); - }); - - describe('reclaimRemainingFunds', () => { - const webhookUrl = 'http://example.com'; - const dryRun = false; - - it('should call the reclaimFunds procedure and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.capitalDistribution.Reclaim, - }; - const mockTransaction = new MockTransaction(transaction); - - const distributionWithDetails = new MockDistributionWithDetails(); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(distributionWithDetails as any); - - const result = await service.reclaimRemainingFunds('TICKER', new BigNumber(1), { - signer, - webhookUrl, - dryRun, - }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - distributionWithDetails.distribution.reclaimFunds, - undefined, - { signer, webhookUrl, dryRun } - ); - }); - }); - - describe('modifyCheckpoint', () => { - it('should run the modifyCheckpoint procedure and return the queue results', async () => { - const mockDistributionWithDetails = new MockDistributionWithDetails(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.corporateAction.ChangeRecordDate, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findDistributionSpy = jest.spyOn(service, 'findDistribution'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findDistributionSpy.mockResolvedValue(mockDistributionWithDetails as any); - - const body = { - checkpoint: { - id: new BigNumber(1), - type: CaCheckpointType.Existing, - }, - signer, - }; - const result = await service.modifyCheckpoint('TICKER', new BigNumber(1), body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/corporate-actions/corporate-actions.service.ts b/src/corporate-actions/corporate-actions.service.ts deleted file mode 100644 index c33d3485..00000000 --- a/src/corporate-actions/corporate-actions.service.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - CorporateActionDefaultConfig, - DistributionWithDetails, - DividendDistribution, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { LinkDocumentsDto } from '~/corporate-actions/dto/link-documents.dto'; -import { ModifyDistributionCheckpointDto } from '~/corporate-actions/dto/modify-distribution-checkpoint.dto'; -import { PayDividendsDto } from '~/corporate-actions/dto/pay-dividends.dto'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class CorporateActionsService { - constructor( - private readonly assetsService: AssetsService, - private readonly transactionService: TransactionsService - ) {} - - public async findDefaultConfigByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.corporateActions.getDefaultConfig(); - } - - public async updateDefaultConfigByTicker( - ticker: string, - corporateActionDefaultConfigDto: CorporateActionDefaultConfigDto - ): ServiceReturn { - const { base, args } = extractTxBase(corporateActionDefaultConfigDto); - const asset = await this.assetsService.findFungible(ticker); - - return this.transactionService.submit( - asset.corporateActions.setDefaultConfig, - args as Required, - base - ); - } - - public async findDistributionsByTicker(ticker: string): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.corporateActions.distributions.get(); - } - - public async findDistribution(ticker: string, id: BigNumber): Promise { - const asset = await this.assetsService.findFungible(ticker); - - return await asset.corporateActions.distributions.getOne({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async createDividendDistribution( - ticker: string, - dividendDistributionDto: DividendDistributionDto - ): ServiceReturn { - const { - base, - args: { originPortfolio, ...rest }, - } = extractTxBase(dividendDistributionDto); - - const asset = await this.assetsService.findFungible(ticker); - return this.transactionService.submit( - asset.corporateActions.distributions.configureDividendDistribution, - { - ...rest, - originPortfolio: toPortfolioId(originPortfolio), - }, - base - ); - } - - public async remove( - ticker: string, - corporateAction: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const asset = await this.assetsService.findFungible(ticker); - return this.transactionService.submit( - asset.corporateActions.remove, - { corporateAction }, - transactionBaseDto - ); - } - - public async payDividends( - ticker: string, - id: BigNumber, - payDividendsDto: PayDividendsDto - ): ServiceReturn { - const { base, args } = extractTxBase(payDividendsDto); - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.pay, args, base); - } - - public async linkDocuments( - ticker: string, - id: BigNumber, - linkDocumentsDto: LinkDocumentsDto - ): ServiceReturn { - const { - base, - args: { documents }, - } = extractTxBase(linkDocumentsDto); - - const { distribution } = await this.findDistribution(ticker, id); - - const params = { - documents: documents.map(document => document.toAssetDocument()), - }; - return this.transactionService.submit(distribution.linkDocuments, params, base); - } - - public async claimDividends( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { distribution } = await this.findDistribution(ticker, id); - return this.transactionService.submit(distribution.claim, undefined, transactionBaseDto); - } - - public async reclaimRemainingFunds( - ticker: string, - id: BigNumber, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.reclaimFunds, undefined, transactionBaseDto); - } - - public async modifyCheckpoint( - ticker: string, - id: BigNumber, - modifyDistributionCheckpointDto: ModifyDistributionCheckpointDto - ): ServiceReturn { - const { base, args } = extractTxBase(modifyDistributionCheckpointDto); - - const { distribution } = await this.findDistribution(ticker, id); - - return this.transactionService.submit(distribution.modifyCheckpoint, args, base); - } -} diff --git a/src/corporate-actions/corporate-actions.util.ts b/src/corporate-actions/corporate-actions.util.ts deleted file mode 100644 index a942f808..00000000 --- a/src/corporate-actions/corporate-actions.util.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* istanbul ignore file */ - -import { - DistributionWithDetails, - DividendDistribution, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; -import { DividendDistributionDetailsModel } from '~/corporate-actions/models/dividend-distribution-details.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createDividendDistributionModel( - distribution: DividendDistribution -): DividendDistributionModel { - const { - origin, - currency, - perShare, - maxAmount, - expiryDate, - paymentDate, - id, - asset: { ticker }, - declarationDate, - description, - targets, - defaultTaxWithholding, - taxWithholdings, - } = distribution; - return new DividendDistributionModel({ - origin: createPortfolioIdentifierModel(origin), - currency, - perShare, - maxAmount, - expiryDate, - paymentDate, - id, - ticker, - declarationDate, - description, - targets, - defaultTaxWithholding, - taxWithholdings, - }); -} - -export function createDividendDistributionDetailsModel( - distributionWithDetails: DistributionWithDetails -): DividendDistributionDetailsModel { - const { distribution, details } = distributionWithDetails; - - return new DividendDistributionDetailsModel({ - ...createDividendDistributionModel(distribution), - ...details, - }); -} diff --git a/src/corporate-actions/decorators/transformation.ts b/src/corporate-actions/decorators/transformation.ts deleted file mode 100644 index 902cde62..00000000 --- a/src/corporate-actions/decorators/transformation.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { plainToClass, Transform } from 'class-transformer'; - -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -type CaCheckpoint = string | { type: string; id: BigNumber }; - -/** - * String | { type: string; id: string; } -> CorporateActionCheckpointDto | Date - */ -export function ToCaCheckpoint() { - return applyDecorators( - Transform(({ value }: { value: CaCheckpoint }) => { - if (typeof value === 'string') { - return new Date(value); - } else { - return plainToClass(CorporateActionCheckpointDto, value); - } - }) - ); -} diff --git a/src/corporate-actions/decorators/validation.ts b/src/corporate-actions/decorators/validation.ts deleted file mode 100644 index c11b4f8a..00000000 --- a/src/corporate-actions/decorators/validation.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { registerDecorator, validate as validateClass, ValidationArguments } from 'class-validator'; - -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -export function IsCaCheckpoint() { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isCaCheckpoint', - target: object.constructor, - propertyName, - validator: { - async validate(value: unknown) { - if (value instanceof Date) { - return !isNaN(new Date(value).getTime()); - } - if (value instanceof CorporateActionCheckpointDto) { - return (await validateClass(value)).length === 0; - } - return false; - }, - defaultMessage(args: ValidationArguments) { - return `${args.property} must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'`; - }, - }, - }); - }; -} diff --git a/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts b/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts deleted file mode 100644 index 516ada64..00000000 --- a/src/corporate-actions/dto/corporate-action-checkpoint.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { CaCheckpointType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class CorporateActionCheckpointDto { - @ApiProperty({ - description: 'Whether the Checkpoint already exists or will be created by a Schedule', - enum: CaCheckpointType, - example: CaCheckpointType.Existing, - }) - @IsEnum(CaCheckpointType) - readonly type: CaCheckpointType; - - @ApiProperty({ - description: 'ID of the Checkpoint/Schedule (depending on `type`)', - type: 'string', - example: '1', - }) - @ToBigNumber() - @IsBigNumber() - readonly id: BigNumber; - - constructor(dto: CorporateActionCheckpointDto) { - Object.assign(this, dto); - } -} diff --git a/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts b/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts deleted file mode 100644 index 96784a72..00000000 --- a/src/corporate-actions/dto/corporate-action-default-config.dto.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { CorporateActionDefaultConfigDto } from '~/corporate-actions/dto/corporate-action-default-config.dto'; -import { testValues } from '~/test-utils/consts'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const { did, signer } = testValues; - -describe('corporateActionDefaultConfigDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: CorporateActionDefaultConfigDto, - data: '', - }; - describe('valid Corporate Action Default values', () => { - const cases: ValidCase[] = [ - [ - 'Update all parameters', - { - targets: { - treatment: TargetTreatment.Include, - identities: [did], - }, - defaultTaxWithholding: '25', - taxWithholdings: [ - { - identity: did, - percentage: '10', - }, - ], - signer, - }, - ], - [ - 'Update only `targets`', - { - targets: { - treatment: TargetTreatment.Include, - identities: [did], - }, - signer, - }, - ], - [ - 'Update only `defaultTaxWithholding`', - { - defaultTaxWithholding: '25', - signer, - }, - ], - [ - 'Update only `taxWithholdings`', - { - taxWithholdings: [ - { - identity: did, - percentage: '10', - }, - ], - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidCase[] = [ - [ - 'No values being updated', - { - signer, - }, - ['defaultTaxWithholding must be a number'], - ], - [ - 'All undefined params', - { - targets: undefined, - defaultTaxWithholding: undefined, - taxWithholdings: undefined, - signer, - }, - ['defaultTaxWithholding must be a number'], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/corporate-actions/dto/corporate-action-default-config.dto.ts b/src/corporate-actions/dto/corporate-action-default-config.dto.ts deleted file mode 100644 index fa5e3620..00000000 --- a/src/corporate-actions/dto/corporate-action-default-config.dto.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateIf, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { CorporateActionTargetsDto } from '~/corporate-actions/dto/corporate-action-targets.dto'; -import { TaxWithholdingDto } from '~/corporate-actions/dto/tax-withholding.dto'; - -export class CorporateActionDefaultConfigDto extends TransactionBaseDto { - @ApiPropertyOptional({ - description: 'Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsDto, - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!targets || (!taxWithholdings && !defaultTaxWithholding) - ) - @ValidateNested() - @Type(() => CorporateActionTargetsDto) - readonly targets?: CorporateActionTargetsDto; - - @ApiPropertyOptional({ - description: - "Tax withholding percentage (0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!defaultTaxWithholding || (!targets && !taxWithholdings) - ) - @ToBigNumber() - @IsBigNumber() - readonly defaultTaxWithholding?: BigNumber; - - @ApiPropertyOptional({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingDto, - isArray: true, - }) - @ValidateIf( - ({ targets, defaultTaxWithholding, taxWithholdings }: CorporateActionDefaultConfigDto) => - !!taxWithholdings || (!targets && !defaultTaxWithholding) - ) - @ValidateNested({ each: true }) - @Type(() => TaxWithholdingDto) - readonly taxWithholdings?: TaxWithholdingDto[]; -} diff --git a/src/corporate-actions/dto/corporate-action-targets.dto.ts b/src/corporate-actions/dto/corporate-action-targets.dto.ts deleted file mode 100644 index d065113b..00000000 --- a/src/corporate-actions/dto/corporate-action-targets.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; - -export class CorporateActionTargetsDto { - @ApiProperty({ - description: 'Indicates how the `identities` are to be treated', - type: 'string', - enum: TargetTreatment, - example: TargetTreatment.Include, - }) - @IsEnum(TargetTreatment) - readonly treatment: TargetTreatment; - - @ApiProperty({ - description: - 'Determines which Identities will be affected by the Corporate Action. If the value of `treatment` is `Include`, then all Identities in this array will be affected. Otherwise, every Asset holder Identity **EXCEPT** for the ones in this array will be affected', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsDid({ each: true }) - readonly identities: string[]; -} diff --git a/src/corporate-actions/dto/dividend-distribution.dto.spec.ts b/src/corporate-actions/dto/dividend-distribution.dto.spec.ts deleted file mode 100644 index 9d54421f..00000000 --- a/src/corporate-actions/dto/dividend-distribution.dto.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { CaCheckpointType } from '@polymeshassociation/polymesh-sdk/types'; - -import { DividendDistributionDto } from '~/corporate-actions/dto/dividend-distribution.dto'; -import { testValues } from '~/test-utils/consts'; -import { InvalidCase, ValidCase } from '~/test-utils/types'; - -const { signer } = testValues; - -describe('dividendDistributionDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const mockDate = new Date(); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: DividendDistributionDto, - data: '', - }; - describe('valid values', () => { - const cases: ValidCase[] = [ - [ - "checkpoint as 'Date'", - { - description: 'Corporate Action description', - checkpoint: mockDate, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with type 'Existing'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: CaCheckpointType.Existing, - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with type 'Scheduled'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: CaCheckpointType.Schedule, - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidCase[] = [ - [ - "'checkpoint' as random string", - { - description: 'Corporate Action description', - checkpoint: 'abc', - originPortfolio: '1', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - [ - "'checkpoint' as random JSON", - { - description: 'Corporate Action description', - checkpoint: { - xyz: 'abc', - }, - originPortfolio: '1', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - [ - "checkpoint as 'CorporateActionCheckpointDto' with invalid type 'Existing'", - { - description: 'Corporate Action description', - checkpoint: { - id: '1', - type: 'Unknown', - }, - originPortfolio: '0', - currency: 'TICKER', - perShare: '2', - maxAmount: '1000', - paymentDate: mockDate, - signer, - }, - ["checkpoint must be a valid 'Date' or object of type 'CorporateActionCheckpointDto'"], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/corporate-actions/dto/dividend-distribution.dto.ts b/src/corporate-actions/dto/dividend-distribution.dto.ts deleted file mode 100644 index 28b96831..00000000 --- a/src/corporate-actions/dto/dividend-distribution.dto.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ToCaCheckpoint } from '~/corporate-actions/decorators/transformation'; -import { IsCaCheckpoint } from '~/corporate-actions/decorators/validation'; -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; -import { CorporateActionTargetsDto } from '~/corporate-actions/dto/corporate-action-targets.dto'; -import { TaxWithholdingDto } from '~/corporate-actions/dto/tax-withholding.dto'; - -@ApiExtraModels(CorporateActionCheckpointDto) -export class DividendDistributionDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Brief description of the Corporate Action', - type: 'string', - example: 'Corporate Action description', - }) - @IsString() - readonly description: string; - - @ApiPropertyOptional({ - description: - 'Date at which the issuer publicly declared the Distribution. Optional, defaults to the current date', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly declarationDate?: Date; - - @ApiPropertyOptional({ - description: 'Asset holder Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => CorporateActionTargetsDto) - readonly targets?: CorporateActionTargetsDto; - - @ApiPropertyOptional({ - description: 'Tax withholding percentage(0-100) of the Benefits to be held for tax purposes', - type: 'string', - example: '25', - }) - @IsOptional() - @ToBigNumber() - @IsBigNumber() - readonly defaultTaxWithholding?: BigNumber; - - @ApiPropertyOptional({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingDto, - isArray: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => TaxWithholdingDto) - readonly taxWithholdings?: TaxWithholdingDto[]; - - @ApiPropertyOneOf({ - description: - 'Checkpoint to be used to calculate Dividends. If a Schedule is passed, the next Checkpoint it creates will be used. If a Date is passed, a Checkpoint will be created at that date and used', - union: [ - CorporateActionCheckpointDto, - { type: 'string', example: new Date('10/14/1987').toISOString() }, - ], - }) - @ToCaCheckpoint() - @IsCaCheckpoint() - readonly checkpoint: CorporateActionCheckpointDto | Date; - - @ApiPropertyOptional({ - description: - 'Portfolio number from which the Dividends will be distributed. Use 0 for default Portfolio', - type: 'string', - example: '123', - }) - @ToBigNumber() - @IsBigNumber() - readonly originPortfolio: BigNumber; - - @ApiProperty({ - description: 'Ticker of the currency in which Dividends will be distributed', - type: 'string', - example: 'TICKER', - }) - @IsTicker() - readonly currency: string; - - @ApiProperty({ - description: "Amount of `currency` to pay for each Asset holders' share", - type: 'string', - example: '100', - }) - @ToBigNumber() - @IsBigNumber() - readonly perShare: BigNumber; - - @ApiProperty({ - description: 'Maximum amount of `currency` to be distributed', - type: 'string', - example: '1000', - }) - @ToBigNumber() - @IsBigNumber() - readonly maxAmount: BigNumber; - - @ApiProperty({ - description: 'Date starting from which Asset holders can claim their dividends', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsDate() - readonly paymentDate: Date; - - @ApiPropertyOptional({ - description: - 'Date after which Dividends can no longer be claimed. Optional, defaults to never expiring', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - @Type(() => Date) - readonly expiryDate?: Date; -} diff --git a/src/corporate-actions/dto/link-documents.dto.ts b/src/corporate-actions/dto/link-documents.dto.ts deleted file mode 100644 index 7cc3953e..00000000 --- a/src/corporate-actions/dto/link-documents.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class LinkDocumentsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'List of documents to be linked to the Corporate Action', - type: AssetDocumentDto, - isArray: true, - }) - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents: AssetDocumentDto[]; -} diff --git a/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts b/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts deleted file mode 100644 index d2ebf510..00000000 --- a/src/corporate-actions/dto/modify-distribution-checkpoint.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiExtraModels } from '@nestjs/swagger'; - -import { ApiPropertyOneOf } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { ToCaCheckpoint } from '~/corporate-actions/decorators/transformation'; -import { IsCaCheckpoint } from '~/corporate-actions/decorators/validation'; -import { CorporateActionCheckpointDto } from '~/corporate-actions/dto/corporate-action-checkpoint.dto'; - -@ApiExtraModels(CorporateActionCheckpointDto) -export class ModifyDistributionCheckpointDto extends TransactionBaseDto { - @ApiPropertyOneOf({ - description: - 'Checkpoint to be updated. If a Schedule is passed, the next Checkpoint it creates will be used. If a Date is passed, a Checkpoint will be created at that date and used', - union: [ - CorporateActionCheckpointDto, - { type: 'string', example: new Date('10/14/1987').toISOString() }, - ], - }) - @IsCaCheckpoint() - @ToCaCheckpoint() - readonly checkpoint: Date | CorporateActionCheckpointDto; -} diff --git a/src/corporate-actions/dto/pay-dividends.dto.ts b/src/corporate-actions/dto/pay-dividends.dto.ts deleted file mode 100644 index d3e0919b..00000000 --- a/src/corporate-actions/dto/pay-dividends.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class PayDividendsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'DIDs of the target Identities', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @IsDid({ each: true }) - readonly targets: string[]; -} diff --git a/src/corporate-actions/dto/tax-withholding.dto.ts b/src/corporate-actions/dto/tax-withholding.dto.ts deleted file mode 100644 index 77bead66..00000000 --- a/src/corporate-actions/dto/tax-withholding.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid } from '~/common/decorators/validation'; - -export class TaxWithholdingDto { - @ApiProperty({ - description: 'DID for which the tax withholding percentage is to be overridden', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly identity: string; - - @ApiProperty({ - description: 'Tax withholding percentage (from 0 to 100)', - type: 'string', - example: '67.25', - }) - @ToBigNumber() - @IsBigNumber() - readonly percentage: BigNumber; -} diff --git a/src/corporate-actions/mocks/corporate-action-default-config.mock.ts b/src/corporate-actions/mocks/corporate-action-default-config.mock.ts deleted file mode 100644 index eff584fb..00000000 --- a/src/corporate-actions/mocks/corporate-action-default-config.mock.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { MockIdentity } from '~/test-utils/mocks'; - -export class MockCorporateActionDefaultConfig { - defaultTaxWithholding = new BigNumber(25); - taxWithholdings = [ - { - identity: new MockIdentity(), - percentage: new BigNumber(10), - }, - ]; - - targets = { - identities: [new MockIdentity()], - treatment: TargetTreatment.Exclude, - }; -} diff --git a/src/corporate-actions/mocks/distribution-with-details.mock.ts b/src/corporate-actions/mocks/distribution-with-details.mock.ts deleted file mode 100644 index cda8d749..00000000 --- a/src/corporate-actions/mocks/distribution-with-details.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockDistribution } from '~/corporate-actions/mocks/dividend-distribution.mock'; - -export class MockDistributionWithDetails { - distribution = new MockDistribution(); - details = { - remainingFunds: new BigNumber(2100.1), - fundsReclaimed: false, - }; -} diff --git a/src/corporate-actions/mocks/dividend-distribution.mock.ts b/src/corporate-actions/mocks/dividend-distribution.mock.ts deleted file mode 100644 index 47d75648..00000000 --- a/src/corporate-actions/mocks/dividend-distribution.mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockCorporateActionDefaultConfig } from '~/corporate-actions/mocks/corporate-action-default-config.mock'; -import { MockPortfolio } from '~/test-utils/mocks'; - -export class MockDistribution extends MockCorporateActionDefaultConfig { - origin = new MockPortfolio(); - currency = 'FAKE_CURRENCY'; - perShare = new BigNumber(0.1); - maxAmount = new BigNumber(2100.1); - expiryDate = null; - paymentDate = new Date('10/14/1987'); - asset = { ticker: 'FAKE_TICKER' }; - id = new BigNumber(1); - declarationDate = new Date('10/14/1987'); - description = 'Mock Description'; - - public pay = jest.fn(); - public claim = jest.fn(); - public linkDocuments = jest.fn(); - public reclaimFunds = jest.fn(); - public modifyCheckpoint = jest.fn(); -} diff --git a/src/corporate-actions/models/corporate-action-default-config.model.ts b/src/corporate-actions/models/corporate-action-default-config.model.ts deleted file mode 100644 index d01f716b..00000000 --- a/src/corporate-actions/models/corporate-action-default-config.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -export class CorporateActionDefaultConfigModel { - @ApiProperty({ - description: 'Identities that will be affected by the Corporate Actions', - type: CorporateActionTargetsModel, - }) - @Type(() => CorporateActionTargetsModel) - readonly targets: CorporateActionTargetsModel; - - @ApiProperty({ - description: - "Tax withholding percentage(0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @FromBigNumber() - readonly defaultTaxWithholding: BigNumber; - - @ApiProperty({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingModel, - isArray: true, - }) - @Type(() => TaxWithholdingModel) - readonly taxWithholdings: TaxWithholdingModel[]; - - constructor(model: CorporateActionDefaultConfigModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/corporate-action-targets.model.ts b/src/corporate-actions/models/corporate-action-targets.model.ts deleted file mode 100644 index 60cd0cdd..00000000 --- a/src/corporate-actions/models/corporate-action-targets.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, TargetTreatment } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class CorporateActionTargetsModel { - @ApiProperty({ - description: 'Indicates how the `identities` are to be treated', - type: 'string', - enum: TargetTreatment, - example: TargetTreatment.Include, - }) - readonly treatment: TargetTreatment; - - @ApiProperty({ - description: - 'Determines which Identities will be affected by the Corporate Action. If the value of `treatment` is `Include`, then all Identities in this array will be affected. Otherwise, every Asset holder Identity **EXCEPT** for the ones in this array will be affected', - type: 'string', - isArray: true, - example: [ - '0x0600000000000000000000000000000000000000000000000000000000000000', - '0x0611111111111111111111111111111111111111111111111111111111111111', - ], - }) - @FromEntityObject() - readonly identities: Identity[]; - - constructor(model: CorporateActionTargetsModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/corporate-action.model.ts b/src/corporate-actions/models/corporate-action.model.ts deleted file mode 100644 index 861b3667..00000000 --- a/src/corporate-actions/models/corporate-action.model.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionTargetsModel } from '~/corporate-actions/models/corporate-action-targets.model'; -import { TaxWithholdingModel } from '~/corporate-actions/models/tax-withholding.model'; - -export class CorporateActionModel { - @ApiProperty({ - description: 'ID of the Corporate Action', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - readonly ticker: string; - - @ApiProperty({ - description: 'Date at which the Corporate Action was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly declarationDate: Date; - - @ApiProperty({ - description: 'Brief description of the Corporate Action', - type: 'string', - example: 'Corporate Action description', - }) - readonly description: string; - - @ApiProperty({ - description: 'Identities that will be affected by this Corporate Action', - type: CorporateActionTargetsModel, - }) - @Type(() => CorporateActionTargetsModel) - readonly targets: CorporateActionTargetsModel; - - @ApiProperty({ - description: - "Tax withholding percentage(0-100) that applies to Identities that don't have a specific percentage assigned to them", - type: 'string', - example: '25', - }) - @FromBigNumber() - readonly defaultTaxWithholding: BigNumber; - - @ApiProperty({ - description: - 'List of Identities and the specific tax withholding percentage that should apply to them. This takes precedence over `defaultTaxWithholding`', - type: TaxWithholdingModel, - isArray: true, - }) - @Type(() => TaxWithholdingModel) - readonly taxWithholdings: TaxWithholdingModel[]; - - constructor(model: CorporateActionModel) { - Object.assign(this, model); - } -} diff --git a/src/corporate-actions/models/created-dividend-distribution.model.ts b/src/corporate-actions/models/created-dividend-distribution.model.ts deleted file mode 100644 index 0972dbc9..00000000 --- a/src/corporate-actions/models/created-dividend-distribution.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; - -export class CreatedDividendDistributionModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Dividend Distribution', - type: DividendDistributionModel, - }) - @Type(() => DividendDistributionModel) - readonly dividendDistribution: DividendDistributionModel; - - constructor(model: CreatedDividendDistributionModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/corporate-actions/models/dividend-distribution-details.model.ts b/src/corporate-actions/models/dividend-distribution-details.model.ts deleted file mode 100644 index bd109633..00000000 --- a/src/corporate-actions/models/dividend-distribution-details.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { DividendDistributionModel } from '~/corporate-actions/models/dividend-distribution.model'; - -export class DividendDistributionDetailsModel extends DividendDistributionModel { - @ApiProperty({ - description: 'Amount of remaining funds', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly remainingFunds: BigNumber; - - @ApiProperty({ - description: 'Indicates whether the unclaimed funds have been reclaimed by an Agent', - type: 'boolean', - example: false, - }) - readonly fundsReclaimed: boolean; - - constructor(model: DividendDistributionDetailsModel) { - const { remainingFunds, fundsReclaimed, ...dividendDistribution } = model; - super(dividendDistribution); - this.remainingFunds = remainingFunds; - this.fundsReclaimed = fundsReclaimed; - } -} diff --git a/src/corporate-actions/models/dividend-distribution.model.ts b/src/corporate-actions/models/dividend-distribution.model.ts deleted file mode 100644 index a441e3c7..00000000 --- a/src/corporate-actions/models/dividend-distribution.model.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { CorporateActionModel } from '~/corporate-actions/models/corporate-action.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class DividendDistributionModel extends CorporateActionModel { - @ApiProperty({ - description: 'Portfolio from which the Dividends are distributed', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly origin: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Ticker of the currency in which Dividends are distributed', - type: 'string', - example: 'TICKER', - }) - readonly currency: string; - - @ApiProperty({ - description: "Amount of `currency` to pay for each Asset holders' share", - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly perShare: BigNumber; - - @ApiProperty({ - description: 'Maximum amount of `currency` to be distributed', - type: 'string', - example: '1000', - }) - @FromBigNumber() - readonly maxAmount: BigNumber; - - @ApiProperty({ - description: - 'Date after which Dividends can no longer be paid/reclaimed. A null value means the Distribution never expires', - type: 'string', - example: new Date('10/14/1987').toISOString(), - nullable: true, - }) - readonly expiryDate: null | Date; - - @ApiProperty({ - description: 'Date starting from which dividends can be paid/reclaimed', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly paymentDate: Date; - - constructor(model: DividendDistributionModel) { - const { - id, - ticker, - declarationDate, - description, - targets, - taxWithholdings, - defaultTaxWithholding, - ...rest - } = model; - - super({ - id, - ticker, - declarationDate, - description, - targets, - taxWithholdings, - defaultTaxWithholding, - }); - - Object.assign(this, rest); - } -} diff --git a/src/corporate-actions/models/tax-withholding.model.ts b/src/corporate-actions/models/tax-withholding.model.ts deleted file mode 100644 index 6cd2452b..00000000 --- a/src/corporate-actions/models/tax-withholding.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; - -export class TaxWithholdingModel { - @ApiProperty({ - description: 'DID for which the tax withholding percentage is overridden', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly identity: Identity; - - @ApiProperty({ - description: 'Tax withholding percentage (from 0 to 100)', - type: 'string', - example: '67.25', - }) - @FromBigNumber() - readonly percentage: BigNumber; - - constructor(model: TaxWithholdingModel) { - Object.assign(this, model); - } -} diff --git a/src/datastore/config.module-definition.ts b/src/datastore/config.module-definition.ts deleted file mode 100644 index d6cb3a51..00000000 --- a/src/datastore/config.module-definition.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ConfigurableModuleBuilder } from '@nestjs/common'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataStoreModuleOptions {} - -export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = - new ConfigurableModuleBuilder().build(); diff --git a/src/datastore/datastore.module.ts b/src/datastore/datastore.module.ts deleted file mode 100644 index 9979e508..00000000 --- a/src/datastore/datastore.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* istanbul ignore file */ - -import { DynamicModule, Module } from '@nestjs/common'; -import { DataSource } from 'typeorm'; - -import { ConfigurableModuleClass } from '~/datastore/config.module-definition'; -import { LocalStoreModule } from '~/datastore/local-store/local-store.module'; -import { PostgresModule } from '~/datastore/postgres/postgres.module'; -import { createDataSource } from '~/datastore/postgres/source'; - -/** - * responsible for selecting a module to store state in - * - * @note defaults to LocalStoreModule - */ -@Module({}) -export class DatastoreModule extends ConfigurableModuleClass { - public static registerAsync(): DynamicModule { - const postgresSource = createDataSource(); - if (!postgresSource) { - return { - module: LocalStoreModule, - exports: [LocalStoreModule], - }; - } else { - return { - providers: [ - { - provide: DataSource, - useFactory: async (): Promise => { - await postgresSource.initialize(); - return postgresSource; - }, - }, - ], - module: PostgresModule, - exports: [PostgresModule], - }; - } - } -} diff --git a/src/datastore/interfaces/postgres-config.interface.ts b/src/datastore/interfaces/postgres-config.interface.ts deleted file mode 100644 index ab393789..00000000 --- a/src/datastore/interfaces/postgres-config.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ - -export interface PostgresConfig { - type: 'postgres'; - host: string; - username: string; - password: string; - database: string; - port: number; -} diff --git a/src/datastore/local-store/local-store.module.ts b/src/datastore/local-store/local-store.module.ts deleted file mode 100644 index 019b7f7b..00000000 --- a/src/datastore/local-store/local-store.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; -import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -/** - * provides Repos that use process memory to store state - */ -@Module({ - imports: [ConfigModule], - providers: [ - { provide: ApiKeyRepo, useClass: LocalApiKeysRepo }, - { - provide: UsersRepo, - useClass: LocalUserRepo, - }, - ], - exports: [ApiKeyRepo, UsersRepo], -}) -export class LocalStoreModule {} diff --git a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap deleted file mode 100644 index e71a9c32..00000000 --- a/src/datastore/local-store/repos/__snapshots__/users.repo.spec.ts.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocalUserRepo does not meet User test suite requirements method: createUser should create a user 1`] = ` -{ - "id": "1", - "name": "Alice", -} -`; - -exports[`LocalUserRepo does not meet User test suite requirements method: findByName should find the created user 1`] = ` -{ - "id": "1", - "name": "Alice", -} -`; diff --git a/src/datastore/local-store/repos/api-key.repo.spec.ts b/src/datastore/local-store/repos/api-key.repo.spec.ts deleted file mode 100644 index e617ea17..00000000 --- a/src/datastore/local-store/repos/api-key.repo.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ConfigService } from '@nestjs/config'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { LocalApiKeysRepo } from '~/datastore/local-store/repos/api-key.repo'; -import { defaultUser } from '~/users/user.consts'; - -describe(`LocalApiKeyRepo does not meet ${ApiKeyRepo.type} test suite requirements`, () => { - const mockConfig = createMock(); - const repo = new LocalApiKeysRepo(mockConfig); - - ApiKeyRepo.test(repo); -}); - -describe('LocalApiKeyRepo', () => { - const config = 'ConfiguredSecret'; - - it('should be configured with keys from the config service', () => { - const mockConfig = createMock(); - mockConfig.getOrThrow.mockReturnValue(config); - - const repo = new LocalApiKeysRepo(mockConfig); - - return expect(repo.getUserByApiKey(config)).resolves.toEqual(defaultUser); - }); -}); diff --git a/src/datastore/local-store/repos/api-key.repo.ts b/src/datastore/local-store/repos/api-key.repo.ts deleted file mode 100644 index 5f26cfcf..00000000 --- a/src/datastore/local-store/repos/api-key.repo.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -import { parseApiKeysConfig } from '~/auth/auth.utils'; -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { UserModel } from '~/users/model/user.model'; -import { defaultUser } from '~/users/user.consts'; - -@Injectable() -export class LocalApiKeysRepo implements ApiKeyRepo { - private apiKeys: Record = {}; - - constructor(readonly config: ConfigService) { - const givenApiKeys = config.getOrThrow('API_KEYS'); - const apiKeys = parseApiKeysConfig(givenApiKeys); - apiKeys.forEach(key => { - this.apiKeys[key] = defaultUser; - }); - } - - public async getUserByApiKey(apiKey: string): Promise { - const user = this.apiKeys[apiKey]; - if (!user) { - throw new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - } - - return user; - } - - public async deleteApiKey(apiKey: string): Promise { - delete this.apiKeys[apiKey]; - } - - public async createApiKey(user: UserModel): Promise { - const secret = await generateBase64Secret(32); - - this.apiKeys[secret] = user; - - return { userId: user.id, secret }; - } -} diff --git a/src/datastore/local-store/repos/users.repo.spec.ts b/src/datastore/local-store/repos/users.repo.spec.ts deleted file mode 100644 index 7c7b64a2..00000000 --- a/src/datastore/local-store/repos/users.repo.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LocalUserRepo } from '~/datastore/local-store/repos/users.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -describe(`LocalUserRepo does not meet ${UsersRepo.type} test suite requirements`, () => { - const repo = new LocalUserRepo(); - - UsersRepo.test(repo); -}); diff --git a/src/datastore/local-store/repos/users.repo.ts b/src/datastore/local-store/repos/users.repo.ts deleted file mode 100644 index 875c352a..00000000 --- a/src/datastore/local-store/repos/users.repo.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { AppConflictError, AppNotFoundError } from '~/common/errors'; -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; -import { defaultUser } from '~/users/user.consts'; - -@Injectable() -export class LocalUserRepo implements UsersRepo { - private users: Record = { [defaultUser.id]: defaultUser }; - private _nextId = 0; - - public async createUser(params: CreateUserDto): Promise { - const { name } = params; - const existingUser = this.getUserByName(name); - if (existingUser) { - throw new AppConflictError(name, UsersRepo.type); - } - - const id = this.nextId(); - const user = { id, ...params }; - this.users[id] = user; - - return user; - } - - public async findByName(name: string): Promise { - const storedUser = this.getUserByName(name); - if (!storedUser) { - throw new AppNotFoundError(name, UsersRepo.type); - } - return storedUser; - } - - private getUserByName(name: string): UserModel | undefined { - return Object.values(this.users).find(user => user.name === name); - } - - private nextId(): string { - this._nextId += 1; - - return String(this._nextId); - } -} diff --git a/src/datastore/postgres/entities/api-key.entity.ts b/src/datastore/postgres/entities/api-key.entity.ts deleted file mode 100644 index ab9bcfcc..00000000 --- a/src/datastore/postgres/entities/api-key.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Column, DataSource, Entity, Index, ManyToOne, Repository } from 'typeorm'; - -import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; -import { User } from '~/datastore/postgres/entities/user.entity'; - -@Entity() -export class ApiKey extends BaseEntity { - @Index() - @Column({ type: 'text' }) - secret: string; - - @ManyToOne(() => User) - @Column({ type: 'text' }) - user: User; -} - -export const apiKeyRepoProvider: FactoryProvider = { - provide: getRepositoryToken(ApiKey), - useFactory: async (dataSource: DataSource): Promise> => { - return dataSource.getRepository(ApiKey); - }, - inject: [DataSource], -}; diff --git a/src/datastore/postgres/entities/base.entity.ts b/src/datastore/postgres/entities/base.entity.ts deleted file mode 100644 index 21d61e1b..00000000 --- a/src/datastore/postgres/entities/base.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ - -import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; - -export abstract class BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) - createDateTime: Date; - - @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) - lastChangedDateTime: Date; -} diff --git a/src/datastore/postgres/entities/user.entity.ts b/src/datastore/postgres/entities/user.entity.ts deleted file mode 100644 index 95209e51..00000000 --- a/src/datastore/postgres/entities/user.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ - -import { FactoryProvider } from '@nestjs/common'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Check, Column, DataSource, Entity, Repository } from 'typeorm'; - -import { BaseEntity } from '~/datastore/postgres/entities/base.entity'; - -@Entity() -@Check('LENGTH(name) < 127') // arbitrary length sanity check -export class User extends BaseEntity { - @Column({ type: 'text', unique: true }) - name: string; -} - -export const userRepoProvider: FactoryProvider = { - provide: getRepositoryToken(User), - useFactory: async (dataSource: DataSource): Promise> => - dataSource.getRepository(User), - inject: [DataSource], -}; diff --git a/src/datastore/postgres/migrations/1666813826181-users.ts b/src/datastore/postgres/migrations/1666813826181-users.ts deleted file mode 100644 index 1983dfe8..00000000 --- a/src/datastore/postgres/migrations/1666813826181-users.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class users1666813826181 implements MigrationInterface { - name = 'users1666813826181'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" text NOT NULL, CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"), CONSTRAINT "CHK_98ce97014728b484ea0b40feb9" CHECK (LENGTH(name) < 127), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - 'CREATE TABLE "api_key" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastChangedDateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "secret" text NOT NULL, "user" text NOT NULL, "userId" uuid, CONSTRAINT "PK_b1bd840641b8acbaad89c3d8d11" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - 'CREATE INDEX "IDX_6eecb2200c16b5e6610fe33942" ON "api_key" ("secret") ' - ); - await queryRunner.query( - 'ALTER TABLE "api_key" ADD CONSTRAINT "FK_277972f4944205eb29127f9bb6c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'ALTER TABLE "api_key" DROP CONSTRAINT "FK_277972f4944205eb29127f9bb6c"' - ); - await queryRunner.query('DROP INDEX "public"."IDX_6eecb2200c16b5e6610fe33942"'); - await queryRunner.query('DROP TABLE "api_key"'); - await queryRunner.query('DROP TABLE "user"'); - } -} diff --git a/src/datastore/postgres/postgres.module.ts b/src/datastore/postgres/postgres.module.ts deleted file mode 100644 index 035ab89b..00000000 --- a/src/datastore/postgres/postgres.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { apiKeyRepoProvider } from '~/datastore/postgres/entities/api-key.entity'; -import { userRepoProvider } from '~/datastore/postgres/entities/user.entity'; -import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; -import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -/** - * providers Repos that use Postgres to store state - */ -@Module({ - providers: [ - apiKeyRepoProvider, - userRepoProvider, - { provide: UsersRepo, useClass: PostgresUsersRepo }, - { provide: ApiKeyRepo, useClass: PostgresApiKeyRepo }, - ], - exports: [ApiKeyRepo, UsersRepo], -}) -export class PostgresModule {} diff --git a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap deleted file mode 100644 index 527c4834..00000000 --- a/src/datastore/postgres/repos/__snapshots__/users.repo.spec.ts.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PostgresUsersRepo does not meet User requirements method: createUser should create a user 1`] = ` -UserModel { - "id": "1", - "name": "Alice", -} -`; - -exports[`PostgresUsersRepo does not meet User requirements method: findByName should find the created user 1`] = ` -UserModel { - "id": "1", - "name": "Alice", -} -`; diff --git a/src/datastore/postgres/repos/api-key.repo.spec.ts b/src/datastore/postgres/repos/api-key.repo.spec.ts deleted file mode 100644 index 20d88152..00000000 --- a/src/datastore/postgres/repos/api-key.repo.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { when } from 'jest-when'; -import { Repository } from 'typeorm'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { ApiKey } from '~/datastore/postgres/entities/api-key.entity'; -import { PostgresApiKeyRepo } from '~/datastore/postgres/repos/api-keys.repo'; -import { testValues } from '~/test-utils/consts'; -import { MockPostgresApiRepository } from '~/test-utils/repo-mocks'; - -const { user } = testValues; - -describe(`PostgresApiKeyRepo does not meet ${ApiKeyRepo.type} requirements`, () => { - const mockRepository = new MockPostgresApiRepository(); - const repo = new PostgresApiKeyRepo(mockRepository as unknown as Repository); - let _id = 1; - - when(mockRepository.create).mockImplementation(secret => { - when(mockRepository.findOneBy).calledWith({ secret }).mockResolvedValue({ user, id: _id++ }); - return { secret, user }; - }); - - mockRepository.delete.mockImplementation(({ secret }) => { - when(mockRepository.findOneBy).calledWith({ secret }).mockResolvedValue(null); - }); - - ApiKeyRepo.test(repo); -}); diff --git a/src/datastore/postgres/repos/api-keys.repo.ts b/src/datastore/postgres/repos/api-keys.repo.ts deleted file mode 100644 index 32f58c00..00000000 --- a/src/datastore/postgres/repos/api-keys.repo.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { ApiKeyModel } from '~/auth/models/api-key.model'; -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { ApiKey } from '~/datastore/postgres/entities/api-key.entity'; -import { UserModel } from '~/users/model/user.model'; - -@Injectable() -export class PostgresApiKeyRepo implements ApiKeyRepo { - constructor(@InjectRepository(ApiKey) private apiKeyRepo: Repository) {} - - public async createApiKey(user: UserModel): Promise { - const secret = await generateBase64Secret(32); - const key = this.apiKeyRepo.create({ secret, user }); - await this.apiKeyRepo.save(key); - - return this.toApiKey(key); - } - - public async getUserByApiKey(secret: string): Promise { - const key = await this.apiKeyRepo.findOneBy({ secret }); - if (!key) { - throw new AppNotFoundError('*REDACTED*', ApiKeyRepo.type); - } - return key.user; - } - - public async deleteApiKey(apiKey: string): Promise { - await this.apiKeyRepo.delete({ secret: apiKey }); - } - - private toApiKey(apiKey: ApiKey): ApiKeyModel { - const { - secret, - user: { id: userId }, - } = apiKey; - - return new ApiKeyModel({ - secret, - userId, - }); - } -} diff --git a/src/datastore/postgres/repos/users.repo.spec.ts b/src/datastore/postgres/repos/users.repo.spec.ts deleted file mode 100644 index ce5b94a4..00000000 --- a/src/datastore/postgres/repos/users.repo.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { when } from 'jest-when'; -import { Repository, TypeORMError } from 'typeorm'; - -import { AppConflictError } from '~/common/errors'; -import { User } from '~/datastore/postgres/entities/user.entity'; -import { PostgresUsersRepo } from '~/datastore/postgres/repos/users.repo'; -import { testValues } from '~/test-utils/consts'; -import { MockPostgresUserRepository } from '~/test-utils/repo-mocks'; -import { UsersRepo } from '~/users/repo/user.repo'; - -const uniqueViolation = new TypeORMError('duplicate key value violates unique constraint'); -const { user: testUser } = testValues; - -describe(`PostgresUsersRepo does not meet ${UsersRepo.type} requirements`, () => { - const mockRepository = new MockPostgresUserRepository(); - const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); - let _id = 1; - - mockRepository.create.mockImplementation(params => params); - mockRepository.findOneBy.mockResolvedValue(null); - - mockRepository.save.mockImplementation(async user => { - const { name } = user; - user.id = _id++; - when(mockRepository.save) - .calledWith(expect.objectContaining({ name })) - .mockRejectedValue(uniqueViolation); - when(mockRepository.findOneBy).calledWith({ name }).mockResolvedValue(user); - }); - - UsersRepo.test(repo); -}); - -describe('PostgresApiKeyRepo', () => { - const mockRepository = new MockPostgresUserRepository(); - const repo = new PostgresUsersRepo(mockRepository as unknown as Repository); - const name = testUser.name; - describe('method: createUser', () => { - it('should transform TypeORM unique violation into AppConflictError', () => { - mockRepository.save.mockRejectedValue(uniqueViolation); - - return expect(repo.createUser({ name })).rejects.toThrow(AppConflictError); - }); - - it('should not transform generic TypeORM errors', () => { - const typeOrmError = new TypeORMError('Test TypeORM error'); - - mockRepository.save.mockRejectedValue(typeOrmError); - - return expect(repo.createUser({ name })).rejects.toThrowError(typeOrmError); - }); - - it('should throw errors as they are', () => { - const error = new Error('Testing for when something goes wrong'); - mockRepository.save.mockRejectedValue(error); - - return expect(repo.createUser({ name })).rejects.toThrowError(error); - }); - }); -}); diff --git a/src/datastore/postgres/repos/users.repo.ts b/src/datastore/postgres/repos/users.repo.ts deleted file mode 100644 index 74c61d54..00000000 --- a/src/datastore/postgres/repos/users.repo.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { AppNotFoundError } from '~/common/errors'; -import { User as DBUser } from '~/datastore/postgres/entities/user.entity'; -import { convertTypeOrmErrorToAppError } from '~/datastore/postgres/repos/utils'; -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -@Injectable() -export class PostgresUsersRepo implements UsersRepo { - constructor(@InjectRepository(DBUser) private readonly usersRepo: Repository) {} - - public async createUser(params: CreateUserDto): Promise { - const { name } = params; - const entity = this.usersRepo.create(params); - await this.usersRepo.save(entity).catch(convertTypeOrmErrorToAppError(name, UsersRepo.type)); - return this.toUser(entity, params.name); - } - - public async findByName(name: string): Promise { - const entity = await this.usersRepo.findOneBy({ name }); - return this.toUser(entity, name); - } - - private toUser(user: DBUser | null, givenId: string): UserModel { - if (!user) { - throw new AppNotFoundError(givenId, UsersRepo.type); - } - const { id, name } = user; - - return new UserModel({ - id: String(id), - name, - }); - } -} diff --git a/src/datastore/postgres/repos/utils.ts b/src/datastore/postgres/repos/utils.ts deleted file mode 100644 index 22fdde9c..00000000 --- a/src/datastore/postgres/repos/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TypeORMError } from 'typeorm'; - -import { AppConflictError } from '~/common/errors'; - -const isTypeOrmError = (err: Error): err is TypeORMError => { - return err instanceof TypeORMError; -}; - -export const convertTypeOrmErrorToAppError = - (id: string, resourceType: string) => - (err: Error): void => { - if (isTypeOrmError(err)) { - const { message } = err; - if (message.includes('duplicate key value violates unique constraint')) { - throw new AppConflictError(id, resourceType); - } - } - - throw err; - }; diff --git a/src/datastore/postgres/source.ts b/src/datastore/postgres/source.ts deleted file mode 100644 index e75d9905..00000000 --- a/src/datastore/postgres/source.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import * as dotenv from 'dotenv'; -import path from 'path'; -import { DataSource } from 'typeorm'; - -import { readPostgresConfigFromEnv } from '~/datastore/postgres/utils'; - -dotenv.config(); // allows this file to be used with the TypeORM CLI directly for generating and running migrations -const pgConfig = readPostgresConfigFromEnv(); - -export const createDataSource = (): DataSource | undefined => { - const migrations = [path.join(__dirname, 'migrations/*.{ts,js}')]; - const entities = [path.join(__dirname, 'entities/*.entity.{ts,js}')]; - - if (!pgConfig) { - return undefined; - } - - return new DataSource({ - ...pgConfig, - entities, - migrations, - }); -}; - -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -export const dataSource: DataSource = createDataSource()!; diff --git a/src/datastore/postgres/utils.ts b/src/datastore/postgres/utils.ts deleted file mode 100644 index 63798c96..00000000 --- a/src/datastore/postgres/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -export interface PostgresConfig { - type: 'postgres'; - host: string; - username: string; - password: string; - database: string; - port: number; -} - -export const readPostgresConfigFromEnv = (): PostgresConfig | undefined => { - const { - REST_POSTGRES_HOST: host, - REST_POSTGRES_PORT: port, - REST_POSTGRES_USER: username, - REST_POSTGRES_PASSWORD: password, - REST_POSTGRES_DATABASE: database, - } = process.env; - - if (!host || !port || !username || !password || !database) { - return undefined; - } - - return { type: 'postgres', host, username, port: Number(port), password, database }; -}; diff --git a/src/developer-testing/developer-testing.controller.spec.ts b/src/developer-testing/developer-testing.controller.spec.ts deleted file mode 100644 index 59e4bfaa..00000000 --- a/src/developer-testing/developer-testing.controller.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; -import { when } from 'jest-when'; - -import { DeveloperTestingController } from '~/developer-testing/developer-testing.controller'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { testValues } from '~/test-utils/consts'; -import { mockDeveloperServiceProvider } from '~/test-utils/service-mocks'; - -describe('DeveloperTestingController', () => { - let controller: DeveloperTestingController; - let mockService: DeepMocked; - const { - testAccount: { address }, - } = testValues; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [DeveloperTestingController], - providers: [mockDeveloperServiceProvider], - }).compile(); - - mockService = mockDeveloperServiceProvider.useValue as DeepMocked; - controller = module.get(DeveloperTestingController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('handleWebhook', () => { - it('should return an empty object', async () => { - const mockResponse = createMock(); - - await controller.handleWebhook({}, '', mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(200); - }); - - it('should set the header if provided', async () => { - const mockResponse = createMock(); - const secret = 'someSecret'; - - await controller.handleWebhook({}, secret, mockResponse); - - expect(mockResponse.header).toHaveBeenCalledWith(HANDSHAKE_HEADER_KEY, secret); - }); - }); - - describe('createTestingAdmins', () => { - it('call the service with the params and return the result', async () => { - const serviceResponse: Identity[] = []; - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(10) }], - } as CreateTestAdminsDto; - - when(mockService.createTestAdmins).calledWith(params).mockResolvedValue(serviceResponse); - - const result = await controller.createTestAdmins(params); - - expect(result).toEqual({ results: serviceResponse }); - expect(mockService.createTestAdmins).toHaveBeenCalledWith(params); - }); - }); - - describe('createTestAccount', () => { - it('call the service with the params and return the result', async () => { - const serviceResponse: Identity[] = []; - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(10) }], - } as CreateTestAccountsDto; - - when(mockService.createTestAccounts).calledWith(params).mockResolvedValue(serviceResponse); - - const result = await controller.createTestAccounts(params); - - expect(result).toEqual({ results: serviceResponse }); - expect(mockService.createTestAccounts).toHaveBeenCalledWith(params); - }); - }); -}); diff --git a/src/developer-testing/developer-testing.controller.ts b/src/developer-testing/developer-testing.controller.ts deleted file mode 100644 index 60e1733c..00000000 --- a/src/developer-testing/developer-testing.controller.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Body, Controller, Headers, Post, Res } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; - -import { ApiArrayResponse } from '~/common/decorators/swagger'; -import { ResultsModel } from '~/common/models/results.model'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { createIdentityModel } from '~/identities/identities.util'; -import { IdentityModel } from '~/identities/models/identity.model'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; - -@ApiTags('developer-testing') -@Controller('developer-testing') -export class DeveloperTestingController { - constructor(private readonly developerTestingService: DeveloperTestingService) {} - - @ApiOperation({ - summary: `Returns a 200 response and echos ${HANDSHAKE_HEADER_KEY} if present in the request`, - description: - 'This endpoint is meant to aid testing webhook functionality for developers. It has no use for a regular user of the API (DEV ONLY)', - }) - @ApiResponse({ - description: - 'An empty object will be returned. The handshake secret given will be set in the response headers', - }) - @Post('/webhook') - async handleWebhook( - @Body() payload: Record, - @Headers(HANDSHAKE_HEADER_KEY) secret: string, - @Res() res: Response - ): Promise { - if (secret) { - res.header(HANDSHAKE_HEADER_KEY, secret); - } - res.status(200).send({}); - } - - @ApiOperation({ - summary: - 'Given a set of addresses this generates creates an Identity and transfers some POLYX to the address and makes them a CDD provider', - description: - 'This endpoint initializes a set of addresses to be chain admin accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initial amounts (DEV ONLY)', - }) - @ApiArrayResponse(IdentityModel, { - description: 'List of Identities that were made CDD providers and given POLYX', - paginated: true, - }) - @Post('/create-test-admins') - async createTestAdmins( - @Body() params: CreateTestAdminsDto - ): Promise> { - const identities = await this.developerTestingService.createTestAdmins(params); - const results = await Promise.all(identities.map(id => createIdentityModel(id))); - - return new ResultsModel({ - results, - }); - } - - @ApiOperation({ - summary: 'Creates a set of CDD claims for each address given', - description: - 'This endpoint creates Identities for multiple accounts. The signer must be a CDD provider and have sufficient POLYX to cover the initialPolyx amounts. (DEV ONLY)', - }) - @ApiArrayResponse(IdentityModel, { - description: 'List of Identities were created with a CDD claim by the signer', - paginated: true, - }) - @Post('/create-test-accounts') - async createTestAccounts( - @Body() params: CreateTestAccountsDto - ): Promise> { - const ids = await this.developerTestingService.createTestAccounts(params); - const results = await Promise.all(ids.map(id => createIdentityModel(id))); - - return new ResultsModel({ results }); - } -} diff --git a/src/developer-testing/developer-testing.module.ts b/src/developer-testing/developer-testing.module.ts deleted file mode 100644 index 99143708..00000000 --- a/src/developer-testing/developer-testing.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { DynamicModule, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { DeveloperTestingController } from '~/developer-testing/developer-testing.controller'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SigningModule } from '~/signing/signing.module'; - -@Module({}) -export class DeveloperTestingModule { - static register(): DynamicModule { - const controllers = []; - - const DEVELOPER_UTILS: boolean = JSON.parse(`${!!process.env.DEVELOPER_UTILS}`); - - if (DEVELOPER_UTILS) { - controllers.push(DeveloperTestingController); - } - - return { - module: DeveloperTestingModule, - imports: [PolymeshModule, AccountsModule, SigningModule, ConfigModule], - controllers, - providers: [DeveloperTestingService], - exports: [DeveloperTestingService], - }; - } -} diff --git a/src/developer-testing/developer-testing.service.spec.ts b/src/developer-testing/developer-testing.service.spec.ts deleted file mode 100644 index 1770e0fb..00000000 --- a/src/developer-testing/developer-testing.service.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { cryptoWaitReady } from '@polymeshassociation/polymesh-sdk/utils'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppInternalError } from '~/common/errors'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; -import { makeMockConfigProvider, MockAccountsService } from '~/test-utils/service-mocks'; - -const { - testAccount: { address }, -} = testValues; - -describe('DeveloperTestingService', () => { - let service: DeveloperTestingService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockAccountsService: MockAccountsService; - - beforeAll(async () => { - await cryptoWaitReady(); - }); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockAccountsService = new MockAccountsService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ - DeveloperTestingService, - AccountsService, - mockSigningProvider, - makeMockConfigProvider({ DEVELOPER_SUDO_MNEMONIC: '//Bob' }), - ], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - polymeshService = module.get(PolymeshService); - service = module.get(DeveloperTestingService); - - polymeshService.execTransaction = jest.fn(); - mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42)); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('createTestAdmins', () => { - it('should return test admin Identities', async () => { - const secondaryAddress = 'someSecondaryAddress'; - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - when(mockAccountsService.findOne) - .calledWith(secondaryAddress) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeSecondaryId'), - }); - - const params = { - accounts: [ - { address, initialPolyx: new BigNumber(100) }, - { address: secondaryAddress, initialPolyx: new BigNumber(0) }, - ], - }; - - const identities = await service.createTestAdmins(params); - - expect(identities).toEqual(['fakeId', 'fakeSecondaryId']); - }); - }); - - describe('createTestAccounts', () => { - it('should return test Identities', async () => { - mockAccountsService.findOne.mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(100) }], - signer: 'test-admin', - }; - - const identities = await service.createTestAccounts(params); - - expect(identities).toEqual(['fakeId']); - }); - - it('should throw an error if an Identity is not made', async () => { - mockAccountsService.findOne.mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue(null), - }); - - const params = { - accounts: [{ address, initialPolyx: new BigNumber(100) }], - signer: 'test-admin', - }; - - const expectedError = new AppInternalError( - 'At least one identity was not found which should have been made' - ); - - return expect(service.createTestAccounts(params)).rejects.toThrowError(expectedError); - }); - - it('should call execTransaction with the default sudo signer if `signer` is not specified', async () => { - const params = { accounts: [{ address, initialPolyx: new BigNumber(10) }] }; - - const defaultAdminAddress = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'; - - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - await service.createTestAccounts(params); - - expect(polymeshService.execTransaction).toHaveBeenCalledWith( - defaultAdminAddress, - expect.anything(), - expect.anything() - ); - }); - }); - - describe('createMockCdd', () => { - it('should return the created Identity', async () => { - const params = { - address, - initialPolyx: new BigNumber(10), - }; - mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42)); - - when(mockAccountsService.findOne) - .calledWith(address) - .mockResolvedValue({ - getIdentity: jest.fn().mockResolvedValue('fakeId'), - }); - - const result = await service.createMockCdd(params); - expect(result).toEqual('fakeId'); - }); - }); -}); diff --git a/src/developer-testing/developer-testing.service.ts b/src/developer-testing/developer-testing.service.ts deleted file mode 100644 index e31dd848..00000000 --- a/src/developer-testing/developer-testing.service.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { SubmittableExtrinsic } from '@polkadot/api-base/types'; -import { Keyring } from '@polkadot/keyring'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { Account, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppInternalError, AppValidationError } from '~/common/errors'; -import { isNotNull } from '~/common/utils'; -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; -import { CreateTestAccountsDto } from '~/developer-testing/dto/create-test-accounts.dto'; -import { CreateTestAdminsDto } from '~/developer-testing/dto/create-test-admins.dto'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services'; - -const unitsPerPolyx = 1000000; - -@Injectable() -export class DeveloperTestingService { - private _sudoPair: KeyringPair; - - constructor( - private readonly polymeshService: PolymeshService, - private readonly accountsService: AccountsService, - private readonly signingService: SigningService, - private readonly configService: ConfigService - ) {} - - /** - * @note relies on having a sudo account configured - */ - public async createTestAdmins({ accounts }: CreateTestAdminsDto): Promise { - const identities = await this.batchSudoInitIdentities(accounts); - - await this.createCddProvidersBatch(identities); - - return identities; - } - - /** - * @note the `signer` must be a CDD provider and have sufficient POLYX to cover the `initialPolyx` - */ - public async createTestAccounts({ - accounts, - signer, - }: CreateTestAccountsDto): Promise { - const { - _polkadotApi: { - tx: { utility, balances, identity }, - }, - } = this.polymeshService.polymeshApi; - - const signerAddress = signer - ? await this.signingService.getAddressByHandle(signer) - : this.sudoPair.address; - - // Create a DID to attach claim too - const createDidCalls = accounts.map(({ address }) => identity.cddRegisterDid(address, [])); - await this.polymeshService.execTransaction(signerAddress, utility.batchAtomic, createDidCalls); - - // Fetch the Account and Identity that was made - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - const identities = await this.fetchAccountsIdentities(madeAccounts); - - // Now create a CDD claim for each Identity - const createCddCalls = identities.map(({ did }) => - identity.addClaim(did, { CustomerDueDiligence: did }, null) - ); - - // and provide POLYX for those that are supposed to get some - const initialPolyxCalls = accounts - .filter(({ initialPolyx }) => initialPolyx.gt(0)) - .map(({ address, initialPolyx }) => - balances.transfer(address, initialPolyx.toNumber() * unitsPerPolyx) - ); - - await this.polymeshService.execTransaction(signerAddress, utility.batchAtomic, [ - ...createCddCalls, - ...initialPolyxCalls, - ]); - - return identities; - } - - /** - * @note relies on having a sudo account configured - */ - private async createCddProvidersBatch(identities: Identity[]): Promise { - const { - polymeshService: { - polymeshApi: { - _polkadotApi: { - tx: { cddServiceProviders, sudo, utility }, - }, - }, - }, - sudoPair, - } = this; - - const cddCalls = identities.map(({ did }) => { - return cddServiceProviders.addMember(did); - }); - - const batchTx = utility.batchAtomic(cddCalls); - - await this.polymeshService.execTransaction(sudoPair, sudo.sudo, batchTx); - } - - /** - * @note relies on having a sudo account configured - */ - private async batchSudoInitIdentities(accounts: CreateMockIdentityDto[]): Promise { - const { - polymeshService: { - polymeshApi: { - _polkadotApi: { - tx: { testUtils, utility, balances, sudo }, - }, - }, - }, - sudoPair, - } = this; - - const cddCalls: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; - const balanceCalls: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; - - accounts.forEach(({ address, initialPolyx }) => { - cddCalls.push(testUtils.mockCddRegisterDid(address)); - if (initialPolyx.gt(0)) { - const polyx = initialPolyx.toNumber() * unitsPerPolyx; - balanceCalls.push(balances.setBalance(address, polyx, 0)); - } - }); - - const balanceTx = sudo.sudo(utility.batchAtomic(balanceCalls)); - - await this.polymeshService.execTransaction(sudoPair, utility.batchAtomic, [ - ...cddCalls, - balanceTx, - ]); - - const madeAccounts = await this.fetchAccountForAccountParams(accounts); - - return this.fetchAccountsIdentities(madeAccounts); - } - - private get sudoPair(): KeyringPair { - if (!this._sudoPair) { - const sudoMnemonic = this.configService.getOrThrow('DEVELOPER_SUDO_MNEMONIC'); - const ss58Format = this.polymeshService.polymeshApi.network.getSs58Format().toNumber(); - const keyring = new Keyring({ type: 'sr25519', ss58Format }); - this._sudoPair = keyring.addFromUri(sudoMnemonic); - } - - return this._sudoPair; - } - - private async fetchAccountForAccountParams( - accounts: CreateMockIdentityDto[] - ): Promise { - return Promise.all(accounts.map(({ address }) => this.accountsService.findOne(address))); - } - - private async fetchAccountsIdentities(accounts: Account[]): Promise { - const potentialIdentities = await Promise.all(accounts.map(account => account.getIdentity())); - - const identities = potentialIdentities.filter(isNotNull); - if (identities.length !== potentialIdentities.length) { - throw new AppInternalError('At least one identity was not found which should have been made'); - } - - return identities; - } - - /** - * @deprecated Use @link{DeveloperTestingService.createAccount} (the batched version) instead - * @note intended for development chains only (i.e. Alice exists and can call `testUtils.createMockCddClaim`) - */ - public async createMockCdd({ address, initialPolyx }: CreateMockIdentityDto): Promise { - const { - _polkadotApi: { - tx: { testUtils, balances, sudo }, - }, - } = this.polymeshService.polymeshApi; - - if (!testUtils) { - throw new AppValidationError( - 'The chain does not have the `testUtils` pallet enabled. This endpoint is intended for development use only' - ); - } - - const targetAccount = await this.accountsService.findOne(address); - - await this.polymeshService.execTransaction( - this.sudoPair, - testUtils.mockCddRegisterDid, - address - ); - const setBalance = balances.setBalance(address, initialPolyx.shiftedBy(6).toNumber(), 0); - await this.polymeshService.execTransaction(this.sudoPair, sudo.sudo, setBalance); - - const id = await targetAccount.getIdentity(); - - if (!id) { - throw new AppInternalError('The Identity was not created'); - } - - return id; - } -} diff --git a/src/developer-testing/dto/create-mock-identity.dto.ts b/src/developer-testing/dto/create-mock-identity.dto.ts deleted file mode 100644 index b871bf82..00000000 --- a/src/developer-testing/dto/create-mock-identity.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class CreateMockIdentityDto { - @ApiProperty({ - description: 'Account address to create an Identity for', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly address: string; - - @ApiProperty({ - description: 'Starting POLYX balance to initialize the Account with', - example: 100000, - }) - @IsBigNumber({ min: 0 }) - @ToBigNumber() - readonly initialPolyx: BigNumber; -} diff --git a/src/developer-testing/dto/create-test-accounts.dto.ts b/src/developer-testing/dto/create-test-accounts.dto.ts deleted file mode 100644 index 237be1fd..00000000 --- a/src/developer-testing/dto/create-test-accounts.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; - -export class CreateTestAccountsDto { - @ApiProperty({ - description: - 'The `signer` to use. The account must have CDD provider permissions, and sufficient POLYX to seed account. Defaults to the configured sudo account', - example: 'alice', - }) - @IsOptional() - @IsString() - readonly signer?: string; - - @ApiProperty({ - description: 'The addresses for which to create Identities', - type: CreateMockIdentityDto, - isArray: true, - }) - @Type(() => CreateMockIdentityDto) - @IsArray() - @ValidateNested({ each: true }) - readonly accounts: CreateMockIdentityDto[]; -} diff --git a/src/developer-testing/dto/create-test-admins.dto.ts b/src/developer-testing/dto/create-test-admins.dto.ts deleted file mode 100644 index 21669515..00000000 --- a/src/developer-testing/dto/create-test-admins.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, ValidateNested } from 'class-validator'; - -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; - -export class CreateTestAdminsDto { - @ApiProperty({ - description: 'The addresses for which to create Identities and set their POLYX balances', - type: CreateMockIdentityDto, - isArray: true, - }) - @Type(() => CreateMockIdentityDto) - @IsArray() - @ValidateNested({ each: true }) - readonly accounts: CreateMockIdentityDto[]; -} diff --git a/src/developer-testing/dto/webhook.dto.ts b/src/developer-testing/dto/webhook.dto.ts deleted file mode 100644 index 3ee93252..00000000 --- a/src/developer-testing/dto/webhook.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class WebHookDto { - @ApiProperty({ - description: 'some text to send', - type: 'string', - example: '1', - }) - @IsString() - readonly text: string; - - constructor(dto: WebHookDto) { - Object.assign(this, dto); - } -} diff --git a/src/events/entities/event.entity.ts b/src/events/entities/event.entity.ts deleted file mode 100644 index 062c4841..00000000 --- a/src/events/entities/event.entity.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { EventPayload, EventType } from '~/events/types'; - -export class EventEntity { - public id: number; - - public type: EventType; - - /** - * scope of the event, helps narrow down subscriptions. For example, in a `transaction.update` event, - * the scope would be the identifier of the transaction that was updated. - */ - public scope: string; - - public createdAt: Date; - - /** - * event data that will be sent to subscribers (freeform, depends on the event) - */ - public payload: T; - - /** - * whether all required notifications for this event have been created (for recovery purposes) - */ - public processed: boolean; - - constructor(entity: EventEntity) { - Object.assign(this, entity); - } -} diff --git a/src/events/events.module.ts b/src/events/events.module.ts deleted file mode 100644 index 1e76a764..00000000 --- a/src/events/events.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { forwardRef, Module } from '@nestjs/common'; - -import { EventsService } from '~/events/events.service'; -import { NotificationsModule } from '~/notifications/notifications.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; - -@Module({ - imports: [forwardRef(() => NotificationsModule), SubscriptionsModule], - providers: [EventsService], - exports: [EventsService], -}) -export class EventsModule {} diff --git a/src/events/events.service.spec.ts b/src/events/events.service.spec.ts deleted file mode 100644 index 0a9d9c41..00000000 --- a/src/events/events.service.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppNotFoundError } from '~/common/errors'; -import { TransactionType } from '~/common/types'; -import { EventEntity } from '~/events/entities/event.entity'; -import { EventsService } from '~/events/events.service'; -import { EventType, TransactionUpdatePayload } from '~/events/types'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { MockNotificationsService, MockSubscriptionsService } from '~/test-utils/service-mocks'; - -describe('EventsService', () => { - let service: EventsService; - - let mockNotificationsService: MockNotificationsService; - let mockSubscriptionsService: MockSubscriptionsService; - - const event = new EventEntity({ - scope: '0x01', - type: EventType.TransactionUpdate, - processed: true, - id: 1, - createdAt: new Date('10/14/1987'), - payload: { - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterTicker, - status: TransactionStatus.Unapproved, - }, - }); - - beforeEach(async () => { - mockNotificationsService = new MockNotificationsService(); - mockSubscriptionsService = new MockSubscriptionsService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [EventsService, SubscriptionsService, NotificationsService], - }) - .overrideProvider(NotificationsService) - .useValue(mockNotificationsService) - .overrideProvider(SubscriptionsService) - .useValue(mockSubscriptionsService) - .compile(); - - service = module.get(EventsService); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsafeService: any = service; - unsafeService.events = { - 1: event, - }; - unsafeService.currentId = 1; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('createEvent', () => { - it('should create an event and its associated notifications, and return the new event ID', async () => { - const type = EventType.TransactionUpdate; - const scope = '0x02'; - const payload = { - type: TransactionType.Single, - transactionTag: TxTags.asset.CreateAsset, - status: TransactionStatus.Unapproved, - } as const; - - mockSubscriptionsService.findAll.mockReturnValue([ - { - id: 1, - }, - ]); - - const result = await service.createEvent({ - type, - scope, - payload, - }); - - expect(result).toBe(2); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt, ...updatedEvent } = await service.findOne(2); - - expect(updatedEvent).toEqual({ - type, - scope, - payload, - id: 2, - processed: true, - }); - expect(mockNotificationsService.createNotifications).toHaveBeenCalledWith([ - { - subscriptionId: 1, - eventId: 2, - }, - ]); - }); - }); - - describe('findOne', () => { - it('should return an event by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual(event); - }); - - it('should throw an error if the event does not exist', () => { - return expect(service.findOne(10)).rejects.toThrow(AppNotFoundError); - }); - }); -}); diff --git a/src/events/events.service.ts b/src/events/events.service.ts deleted file mode 100644 index 543dfd8f..00000000 --- a/src/events/events.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventEntity } from '~/events/entities/event.entity'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -@Injectable() -export class EventsService { - private events: Record; - private currentId: number; - - constructor( - private readonly subscriptionsService: SubscriptionsService, - @Inject(forwardRef(() => NotificationsService)) - private readonly notificationsService: NotificationsService - ) { - this.events = {}; - this.currentId = 0; - } - - /** - * Create an event and create notifications for all active subscriptions to the event - */ - public async createEvent( - eventData: Pick - ): Promise { - const { events } = this; - - this.currentId += 1; - - const id = this.currentId; - - const event = new EventEntity({ - id, - ...eventData, - createdAt: new Date(), - processed: false, - }); - - events[id] = event; - - await this.createEventNotifications(event); - - await this.markEventAsProcessed(id); - - return id; - } - - public async findOne(id: number): Promise { - const event = this.events[id]; - - if (!event) { - throw new AppNotFoundError(id.toString(), 'event'); - } - - return event; - } - - private async createEventNotifications(event: EventEntity): Promise { - const { type: eventType, scope: eventScope, id: eventId } = event; - const { subscriptionsService, notificationsService } = this; - - const affectedSubscriptions = await subscriptionsService.findAll({ - eventType, - eventScope, - status: SubscriptionStatus.Active, - excludeExpired: true, - }); - - const notifications = affectedSubscriptions.map(({ id: subscriptionId, nextNonce: nonce }) => ({ - subscriptionId, - eventId, - nonce, - })); - - await subscriptionsService.batchBumpNonce(affectedSubscriptions.map(({ id }) => id)); - - await notificationsService.createNotifications(notifications); - } - - private async markEventAsProcessed(id: number): Promise { - this.events[id] = { - ...this.events[id], - processed: true, - }; - } -} diff --git a/src/events/types.ts b/src/events/types.ts deleted file mode 100644 index ba893b0d..00000000 --- a/src/events/types.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { TransactionStatus, TxTag } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { EventEntity } from '~/events/entities/event.entity'; - -export enum EventType { - TransactionUpdate = 'transaction.update', -} - -// transaction.update -interface TransactionSignedPayload { - transactionHash: string; -} -interface TransactionErrorPayload { - error: string; -} - -interface TransactionInBlockPayload extends TransactionSignedPayload { - blockHash: string; - blockNumber: string; -} - -interface TransactionSucceededPayload extends TransactionInBlockPayload { - status: TransactionStatus.Succeeded; - result: unknown; -} -type TransactionFailedPayload = TransactionInBlockPayload & - TransactionErrorPayload & { - status: TransactionStatus.Failed; - }; -interface TransactionRejectedPayload extends TransactionErrorPayload { - status: TransactionStatus.Rejected; -} -type TransactionAbortedPayload = TransactionSignedPayload & - TransactionErrorPayload & { - status: TransactionStatus.Aborted; - }; -interface TransactionRunningPayload extends TransactionSignedPayload { - status: TransactionStatus.Running; -} - -type TransactionStatusPayload = - | TransactionSucceededPayload - | TransactionFailedPayload - | TransactionRunningPayload - | TransactionAbortedPayload - | TransactionRejectedPayload - | { - status: Exclude< - TransactionStatus, - | TransactionStatus.Succeeded - | TransactionStatus.Failed - | TransactionStatus.Running - | TransactionStatus.Aborted - >; - }; - -interface SingleTransactionPayload { - type: TransactionType.Single; - transactionTag: TxTag; -} -interface BatchTransactionPayload { - type: TransactionType.Batch; - transactionTags: TxTag[]; -} - -export type TransactionTypePayload = SingleTransactionPayload | BatchTransactionPayload; - -export type TransactionUpdatePayload = TransactionStatusPayload & TransactionTypePayload; - -export interface TransactionUpdateEvent extends EventEntity { - readonly type: EventType.TransactionUpdate; -} - -// payloads (can be extended in the future) -export type EventPayload = TransactionUpdatePayload; - -// maps types to payloads, should be extended -export type GetPayload = T extends EventType.TransactionUpdate - ? TransactionUpdatePayload - : EventPayload; diff --git a/src/identities/identities.consts.ts b/src/extended-identities/identities.consts.ts similarity index 100% rename from src/identities/identities.consts.ts rename to src/extended-identities/identities.consts.ts diff --git a/src/extended-identities/identities.controller.spec.ts b/src/extended-identities/identities.controller.spec.ts new file mode 100644 index 00000000..1ccbfbb9 --- /dev/null +++ b/src/extended-identities/identities.controller.spec.ts @@ -0,0 +1,103 @@ +import { DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialLegParty } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ExtendedIdentitiesController } from '~/extended-identities/identities.controller'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { testValues } from '~/test-utils/consts'; +import { createMockConfidentialTransaction, createMockConfidentialVenue } from '~/test-utils/mocks'; +import { + mockConfidentialTransactionsServiceProvider, + MockIdentitiesService, +} from '~/test-utils/service-mocks'; + +const { did } = testValues; + +describe('IdentitiesController', () => { + let controller: ExtendedIdentitiesController; + const mockIdentitiesService = new MockIdentitiesService(); + let mockConfidentialTransactionService: DeepMocked; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [ExtendedIdentitiesController], + providers: [ExtendedIdentitiesService, mockConfidentialTransactionsServiceProvider], + }) + .overrideProvider(ExtendedIdentitiesService) + .useValue(mockIdentitiesService) + .compile(); + + mockConfidentialTransactionService = module.get( + ConfidentialTransactionsService + ); + controller = module.get(ExtendedIdentitiesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getConfidentialVenues', () => { + it("should return the Identity's Confidential Venues", async () => { + const mockResults = [createMockConfidentialVenue()]; + mockConfidentialTransactionService.findVenuesByOwner.mockResolvedValue(mockResults); + + const result = await controller.getConfidentialVenues({ did }); + expect(result).toEqual({ + results: mockResults, + }); + }); + }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(0), + role: ConfidentialLegParty.Mediator, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + it('should return the list of involved confidential affirmations', async () => { + mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1) } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + mockIdentitiesService.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + const result = await controller.getInvolvedConfidentialTransactions( + { did }, + { size: new BigNumber(1), start: 'SOME_START_KEY' } + ); + + expect(result).toEqual( + new PaginatedResultsModel({ + results: expect.arrayContaining(mockAffirmations.data), + total: new BigNumber(mockAffirmations.count), + next: mockAffirmations.next, + }) + ); + }); + }); +}); diff --git a/src/extended-identities/identities.controller.ts b/src/extended-identities/identities.controller.ts new file mode 100644 index 00000000..491cb731 --- /dev/null +++ b/src/extended-identities/identities.controller.ts @@ -0,0 +1,80 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { ConfidentialVenue } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { ConfidentialAffirmationModel } from '~/confidential-transactions/models/confidential-affirmation.model'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { ApiArrayResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { PaginatedParamsDto } from '~/polymesh-rest-api/src/common/dto/paginated-params.dto'; +import { DidDto } from '~/polymesh-rest-api/src/common/dto/params.dto'; +import { PaginatedResultsModel } from '~/polymesh-rest-api/src/common/models/paginated-results.model'; +import { ResultsModel } from '~/polymesh-rest-api/src/common/models/results.model'; + +@ApiTags('identities') +@Controller('') +export class ExtendedIdentitiesController { + constructor( + private readonly identitiesService: ExtendedIdentitiesService, + private readonly confidentialTransactionService: ConfidentialTransactionsService + ) {} + + @ApiTags('confidential-venues') + @ApiOperation({ + summary: 'Get all Confidential Venues owned by an Identity', + description: 'This endpoint will provide list of confidential venues for an identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity whose Confidential Venues are to be fetched', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get('identities/:did/confidential-venues') + async getConfidentialVenues(@Param() { did }: DidDto): Promise> { + const results = await this.confidentialTransactionService.findVenuesByOwner(did); + return new ResultsModel({ results }); + } + + @ApiTags('confidential-transactions') + @ApiOperation({ + summary: 'Get all Confidential Transaction affirmations involving an Identity', + }) + @ApiParam({ + name: 'did', + description: 'The DID of the Identity', + type: 'string', + example: '0x0600000000000000000000000000000000000000000000000000000000000000', + }) + @ApiArrayResponse('string', { + description: 'List of IDs of all owned Confidential Venues', + paginated: false, + example: ['1', '2', '3'], + }) + @Get('identities/:did/involved-confidential-transactions') + async getInvolvedConfidentialTransactions( + @Param() { did }: DidDto, + @Query() { size, start }: PaginatedParamsDto + ): Promise> { + const { + data, + count: total, + next, + } = await this.identitiesService.getInvolvedConfidentialTransactions( + did, + size, + start?.toString() + ); + + return new PaginatedResultsModel({ + results: data.map(affirmation => new ConfidentialAffirmationModel(affirmation)), + total, + next, + }); + } +} diff --git a/src/extended-identities/identities.module.ts b/src/extended-identities/identities.module.ts new file mode 100644 index 00000000..963d3a14 --- /dev/null +++ b/src/extended-identities/identities.module.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ + +import { forwardRef, Module } from '@nestjs/common'; + +import { ConfidentialTransactionsModule } from '~/confidential-transactions/confidential-transactions.module'; +import { ExtendedIdentitiesController } from '~/extended-identities/identities.controller'; +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; + +@Module({ + imports: [PolymeshModule, forwardRef(() => ConfidentialTransactionsModule)], + controllers: [ExtendedIdentitiesController], + providers: [ExtendedIdentitiesService], + exports: [ExtendedIdentitiesService], +}) +export class ExtendedIdentitiesModule {} diff --git a/src/extended-identities/identities.service.spec.ts b/src/extended-identities/identities.service.spec.ts new file mode 100644 index 00000000..e0e28593 --- /dev/null +++ b/src/extended-identities/identities.service.spec.ts @@ -0,0 +1,102 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { ConfidentialLegParty } from '@polymeshassociation/polymesh-private-sdk/types'; + +import { ExtendedIdentitiesService } from '~/extended-identities/identities.service'; +import { POLYMESH_API } from '~/polymesh/polymesh.consts'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { createMockConfidentialTransaction, MockIdentity, MockPolymesh } from '~/test-utils/mocks'; +import * as transactionsUtilModule from '~/transactions/transactions.util'; + +describe('ExtendedIdentitiesService', () => { + let service: ExtendedIdentitiesService; + let mockPolymeshApi: MockPolymesh; + let polymeshService: PolymeshService; + + beforeEach(async () => { + mockPolymeshApi = new MockPolymesh(); + const module: TestingModule = await Test.createTestingModule({ + imports: [PolymeshModule], + providers: [ExtendedIdentitiesService], + }) + .overrideProvider(POLYMESH_API) + .useValue(mockPolymeshApi) + .compile(); + + mockPolymeshApi = module.get(POLYMESH_API); + polymeshService = module.get(PolymeshService); + + service = module.get(ExtendedIdentitiesService); + }); + + afterEach(async () => { + await polymeshService.close(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return the Identity for a valid DID', async () => { + const fakeResult = 'identity'; + + mockPolymeshApi.identities.getIdentity.mockResolvedValue(fakeResult); + + const result = await service.findOne('realDid'); + + expect(result).toBe(fakeResult); + }); + + describe('otherwise', () => { + it('should call the handleSdkError method and throw an error', async () => { + const mockError = new Error('Some Error'); + mockPolymeshApi.identities.getIdentity.mockRejectedValue(mockError); + + const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); + + await expect(() => service.findOne('invalidDID')).rejects.toThrowError(); + + expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); + }); + }); + }); + + describe('getInvolvedConfidentialTransactions', () => { + const mockAffirmations = { + data: [ + { + transaction: createMockConfidentialTransaction(), + legId: new BigNumber(1), + role: ConfidentialLegParty.Auditor, + affirmed: true, + }, + ], + next: '0xddddd', + count: new BigNumber(1), + }; + + beforeEach(() => { + const mockIdentity = new MockIdentity(); + mockIdentity.getInvolvedConfidentialTransactions.mockResolvedValue(mockAffirmations); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jest.spyOn(service, 'findOne').mockResolvedValue(mockIdentity as any); + }); + + it('should return the list of involved confidential affirmations', async () => { + const result = await service.getInvolvedConfidentialTransactions('0x01', new BigNumber(10)); + expect(result).toEqual(mockAffirmations); + }); + + it('should return the list of involved confidential affirmations from a start value', async () => { + const result = await service.getInvolvedConfidentialTransactions( + '0x01', + new BigNumber(10), + 'NEXT_KEY' + ); + expect(result).toEqual(mockAffirmations); + }); + }); +}); diff --git a/src/extended-identities/identities.service.ts b/src/extended-identities/identities.service.ts new file mode 100644 index 00000000..100c0b08 --- /dev/null +++ b/src/extended-identities/identities.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ConfidentialAffirmation, + Identity, + ResultSet, +} from '@polymeshassociation/polymesh-private-sdk/types'; + +import { PolymeshService } from '~/polymesh/polymesh.service'; +import { handleSdkError } from '~/transactions/transactions.util'; + +@Injectable() +export class ExtendedIdentitiesService { + constructor(private readonly polymeshService: PolymeshService) {} + + /** + * Method to get identity for a specific did + */ + public async findOne(did: string): Promise { + const { + polymeshService: { polymeshApi }, + } = this; + return await polymeshApi.identities.getIdentity({ did }).catch(error => { + throw handleSdkError(error); + }); + } + + public async getInvolvedConfidentialTransactions( + did: string, + size: BigNumber, + start?: string + ): Promise> { + const identity = await this.findOne(did); + + return identity.getInvolvedConfidentialTransactions({ size, start }); + } +} diff --git a/src/identities/models/identity.model.ts b/src/extended-identities/models/identity-details.model.ts similarity index 93% rename from src/identities/models/identity.model.ts rename to src/extended-identities/models/identity-details.model.ts index 1931f8bb..c71d9698 100644 --- a/src/identities/models/identity.model.ts +++ b/src/extended-identities/models/identity-details.model.ts @@ -5,7 +5,7 @@ import { Type } from 'class-transformer'; import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; -export class IdentityModel { +export class IdentityDetailsModel { @ApiProperty({ type: 'string', example: '0x0600000000000000000000000000000000000000000000000000000000000000', @@ -35,7 +35,7 @@ export class IdentityModel { }) readonly secondaryAccountsFrozen: boolean; - constructor(model: IdentityModel) { + constructor(model: IdentityDetailsModel) { Object.assign(this, model); } } diff --git a/src/identities/models/identity-signer.model.ts b/src/extended-identities/models/identity.model.ts similarity index 53% rename from src/identities/models/identity-signer.model.ts rename to src/extended-identities/models/identity.model.ts index ed6e5128..acd8a438 100644 --- a/src/identities/models/identity-signer.model.ts +++ b/src/extended-identities/models/identity.model.ts @@ -1,11 +1,8 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; -import { SignerModel } from '~/identities/models/signer.model'; - -export class IdentitySignerModel extends SignerModel { +export class IdentityModel { @ApiProperty({ type: 'string', example: '0x0600000000000000000000000000000000000000000000000000000000000000', @@ -13,8 +10,7 @@ export class IdentitySignerModel extends SignerModel { }) readonly did: string; - constructor(model: Omit) { - super({ signerType: SignerType.Identity }); + constructor(model: IdentityModel) { Object.assign(this, model); } } diff --git a/src/identities/models/signer.model.ts b/src/extended-identities/models/signer.model.ts similarity index 81% rename from src/identities/models/signer.model.ts rename to src/extended-identities/models/signer.model.ts index 8d0e08da..591bd6a9 100644 --- a/src/identities/models/signer.model.ts +++ b/src/extended-identities/models/signer.model.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; +import { SignerType } from '@polymeshassociation/polymesh-private-sdk/types'; export class SignerModel { @ApiProperty({ diff --git a/src/identities/decorators/validation.ts b/src/identities/decorators/validation.ts deleted file mode 100644 index 9fcf7277..00000000 --- a/src/identities/decorators/validation.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { registerDecorator, ValidationArguments } from 'class-validator'; - -export function IsPermissionsLike() { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isPermissionsLike', - target: object.constructor, - propertyName, - validator: { - validate(value: unknown) { - if (typeof value === 'object' && value) { - return !('transactions' in value && 'transactionGroups' in value); - } - return false; - }, - defaultMessage(args: ValidationArguments) { - return `${args.property} can have either 'transactions' or 'transactionGroups'`; - }, - }, - }); - }; -} diff --git a/src/identities/dto/add-secondary-account-params.dto.spec.ts b/src/identities/dto/add-secondary-account-params.dto.spec.ts deleted file mode 100644 index f2a811f3..00000000 --- a/src/identities/dto/add-secondary-account-params.dto.spec.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - ModuleName, - PermissionType, - TxGroup, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { testValues } from '~/test-utils/consts'; - -const { signer, did } = testValues; - -type ValidInviteCase = [string, Record]; -type InvalidInviteCase = [string, Record, string[]]; - -describe('addSecondaryAccountParamsDto', () => { - const target: ValidationPipe = new ValidationPipe({ transform: true }); - const metadata: ArgumentMetadata = { - type: 'body', - metatype: AddSecondaryAccountParamsDto, - data: '', - }; - describe('valid invites', () => { - const cases: ValidInviteCase[] = [ - [ - 'Invite a Secondary Account', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - signer, - }, - ], - [ - 'Invite with Asset permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER123456', 'TICKER456789'], - type: PermissionType.Include, - }, - }, - signer, - }, - ], - [ - 'Invite with full assets permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: null, - }, - signer, - }, - ], - [ - 'Invite with full portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: null, - }, - signer, - }, - ], - [ - 'Invite with portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [ - { - id: new BigNumber(1), - did, - }, - { - id: new BigNumber(2), - did, - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ], - [ - 'Invite with both assets and portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER123456', 'TICKER456789'], - type: PermissionType.Include, - }, - portfolios: { - values: [ - { - id: new BigNumber(1), - did, - }, - { - id: new BigNumber(2), - did, - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ], - [ - 'Invite with full assets and portfolios permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: null, - portfolios: null, - }, - signer, - }, - ], - [ - 'Invite with transactionGroups permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactionGroups: [TxGroup.PortfolioManagement, TxGroup.AssetManagement], - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with TxTags and without exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [TxTags.identity.FreezeSecondaryKeys, TxTags.asset.RegisterTicker], - }, - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with ModuleNames along with TxTags and without exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Identity, TxTags.asset.RegisterTicker], - }, - }, - signer, - }, - ], - [ - 'Invite with transactions permissions with ModuleNames along with TxTags and with exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Identity, TxTags.asset.RegisterTicker], - exceptions: [TxTags.identity.LeaveIdentityAsKey], - }, - }, - signer, - }, - ], - ]; - test.each(cases)('%s', async (_, input) => { - await target.transform(input, metadata).catch(err => { - fail(`should not make any errors, received: ${JSON.stringify(err.getResponse())}`); - }); - }); - }); - - describe('invalid invites', () => { - const cases: InvalidInviteCase[] = [ - [ - 'Invite with incorrect secondaryAccount', - { - secondaryAccount: 123, - signer, - }, - ['secondaryAccount must be a string'], - ], - [ - 'Invite with assets permission with incorrect permission type', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['TICKER', 'NEWTICKER'], - type: 'INCORRECT', - }, - }, - signer, - }, - ['permissions.assets.type must be one of the following values: Include, Exclude'], - ], - [ - 'Invite with assets permission with incorrect ticker value', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - assets: { - values: ['invalid', 'TICKERVALUES'], - type: PermissionType.Exclude, - }, - }, - signer, - }, - ['permissions.assets.each value in values must be uppercase'], - ], - [ - 'Invite with portfolios permissions with no portfolio details', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [], - type: PermissionType.Include, - }, - }, - signer, - }, - ['permissions.portfolios.values should not be empty'], - ], - [ - 'Invite with portfolios permissions with incorrect DID', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - portfolios: { - values: [ - { - id: new BigNumber(1), - did: '0x6', - }, - { - id: new BigNumber(2), - did: 'DID', - }, - ], - type: PermissionType.Exclude, - }, - }, - signer, - }, - [ - 'permissions.portfolios.values.0.DID must be 66 characters long', - 'permissions.portfolios.values.1.DID must be a hexadecimal number', - 'permissions.portfolios.values.1.DID must start with "0x"', - 'permissions.portfolios.values.1.DID must be 66 characters long', - ], - ], - [ - 'Invite with transactionGroups permissions with incorrect groups', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactionGroups: ['Incorrect'], - }, - signer, - }, - [ - 'permissions.each value in transactionGroups must be one of the following values: PortfolioManagement, AssetManagement, AdvancedAssetManagement, Distribution, Issuance, TrustedClaimIssuersManagement, ClaimsManagement, ComplianceRequirementsManagement, CorporateActionsManagement, StoManagement', - ], - ], - [ - 'Invite with transactions permissions with incorrect TxTags', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [TxTags.asset], - }, - }, - signer, - }, - [ - 'permissions.transactions.values must have all valid enum values from "ModuleName" or "TxTags"', - ], - ], - [ - 'Invite with transactions permissions with incorrect exceptions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Asset], - exceptions: [TxTags.asset], - }, - }, - signer, - }, - ['permissions.transactions.exceptions must have all valid enum values'], - ], - [ - 'Invite with transactions permissions with empty transactionGroups', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: { - type: PermissionType.Include, - values: [ModuleName.Asset], - exceptions: [TxTags.asset.RegisterTicker], - }, - transactionGroups: [], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - [ - 'Invite with transactionGroups permissions and null transactions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: null, - transactionGroups: [TxGroup.PortfolioManagement], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - [ - 'Invite with null transactions & empty transactionGroups permissions', - { - secondaryAccount: '5G9cwcbnffjh9nBnRF1mjr5su78GRcP6tbqrRkVCFhRn1URv', - permissions: { - transactions: null, - transactionGroups: [], - }, - signer, - }, - ["permissions can have either 'transactions' or 'transactionGroups'"], - ], - ]; - - test.each(cases)('%s', async (_, input, expected) => { - let error; - await target.transform(input, metadata).catch(err => { - error = err.getResponse().message; - }); - expect(error).toEqual(expected); - }); - }); -}); diff --git a/src/identities/dto/add-secondary-account-params.dto.ts b/src/identities/dto/add-secondary-account-params.dto.ts deleted file mode 100644 index 58b14de4..00000000 --- a/src/identities/dto/add-secondary-account-params.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { IsPermissionsLike } from '~/identities/decorators/validation'; -import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto'; - -export class AddSecondaryAccountParamsDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account address to be invited', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly secondaryAccount: string; - - @ApiProperty({ - description: 'Permissions to be granted to the `secondaryAccount`', - type: PermissionsLikeDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => PermissionsLikeDto) - @IsPermissionsLike() - readonly permissions?: PermissionsLikeDto; - - @ApiPropertyOptional({ - description: 'Expiry date of the `permissions`', - example: new Date('05/23/2021').toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/identities/dto/asset-permissions.dto.ts b/src/identities/dto/asset-permissions.dto.ts deleted file mode 100644 index 1e8fd42b..00000000 --- a/src/identities/dto/asset-permissions.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SectionPermissions } from '@polymeshassociation/polymesh-sdk/types'; -import { IsArray } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; - -export class AssetPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'List of assets to be included or excluded in the permissions', - type: 'string', - isArray: true, - example: ['TICKER123456'], - }) - @IsArray() - @IsTicker({ each: true }) - readonly values: string[]; - - public toAssetPermissions(): SectionPermissions | null { - const { values, type } = this; - - return { - values, - type, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/permission-type.dto.ts b/src/identities/dto/permission-type.dto.ts deleted file mode 100644 index a341b88e..00000000 --- a/src/identities/dto/permission-type.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PermissionType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -export class PermissionTypeDto { - @ApiProperty({ - description: 'Indicates whether the permissions are inclusive or exclusive', - enum: PermissionType, - example: PermissionType.Include, - }) - @IsEnum(PermissionType) - readonly type: PermissionType; -} diff --git a/src/identities/dto/permissions-like.dto.ts b/src/identities/dto/permissions-like.dto.ts deleted file mode 100644 index dab0dc2b..00000000 --- a/src/identities/dto/permissions-like.dto.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { PermissionsLike, TxGroup } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsArray, IsEnum, IsOptional, ValidateNested } from 'class-validator'; - -import { AssetPermissionsDto } from '~/identities/dto/asset-permissions.dto'; -import { PortfolioPermissionsDto } from '~/identities/dto/portfolio-permissions.dto'; -import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto'; - -export class PermissionsLikeDto { - @ApiPropertyOptional({ - description: 'Assets on which to grant permissions. A null value represents full permissions', - type: AssetPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => AssetPermissionsDto) - readonly assets?: AssetPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Portfolios on which to grant permissions. A null value represents full permissions', - type: PortfolioPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => PortfolioPermissionsDto) - readonly portfolios?: PortfolioPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Transactions that the `secondaryAccount` has permission to execute. A null value represents full permissions. This value should not be passed along with the `transactionGroups`.', - type: TransactionPermissionsDto, - nullable: true, - }) - @IsOptional() - @ValidateNested() - @Type(() => TransactionPermissionsDto) - readonly transactions?: TransactionPermissionsDto | null; - - @ApiPropertyOptional({ - description: - 'Transaction Groups that `secondaryAccount` has permission to execute. This value should not be passed along with the `transactions`.', - isArray: true, - enum: TxGroup, - example: [TxGroup.PortfolioManagement], - }) - @IsArray() - @IsEnum(TxGroup, { each: true }) - @IsOptional() - readonly transactionGroups?: TxGroup[]; - - public toPermissionsLike(): PermissionsLike { - const { assets, portfolios, transactions, transactionGroups } = this; - - let permissionsLike: PermissionsLike = { - assets: assets === null ? null : assets?.toAssetPermissions(), - portfolios: portfolios === null ? null : portfolios?.toPortfolioPermissions(), - }; - - if (transactions === null) { - permissionsLike = { ...permissionsLike, transactions: null }; - } else if (transactions) { - permissionsLike = { - ...permissionsLike, - transactions: transactions.toTransactionPermissions(), - }; - } else if (transactionGroups) { - permissionsLike = { ...permissionsLike, transactionGroups }; - } - - return permissionsLike; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/portfolio-permissions.dto.ts b/src/identities/dto/portfolio-permissions.dto.ts deleted file mode 100644 index 02fda2dc..00000000 --- a/src/identities/dto/portfolio-permissions.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { PortfolioLike, SectionPermissions } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator'; - -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class PortfolioPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'List of Portfolios to be included or excluded in the permissions', - isArray: true, - type: () => PortfolioDto, - }) - @IsArray() - @ArrayNotEmpty() - @ValidateNested({ each: true }) - @Type(() => PortfolioDto) - readonly values: PortfolioDto[]; - - public toPortfolioPermissions(): SectionPermissions | null { - const { values, type } = this; - - return { - values: values.map(portfolio => portfolio.toPortfolioLike()), - type, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/dto/register-identity.dto.ts b/src/identities/dto/register-identity.dto.ts deleted file mode 100644 index a35df8ba..00000000 --- a/src/identities/dto/register-identity.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { PermissionedAccountDto } from '~/accounts/dto/permissioned-account.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RegisterIdentityDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Account address for which to create an Identity', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - @IsString() - readonly targetAccount: string; - - @ApiPropertyOptional({ - description: 'Secondary Accounts and their permissions to be added to the Identity', - type: PermissionedAccountDto, - nullable: true, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => PermissionedAccountDto) - readonly secondaryAccounts?: PermissionedAccountDto[]; - - @ApiProperty({ - description: - 'Issue a CDD claim for the created DID, completing the onboarding process for the Account', - type: 'boolean', - example: false, - }) - readonly createCdd: boolean; - - @ApiPropertyOptional({ - description: 'Date at which the Identity will expire (to be used together with createCdd)', - example: new Date(new Date().getTime() + +365 * 24 * 60 * 60 * 1000).toISOString(), - type: 'string', - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/identities/dto/transaction-permissions.dto.ts b/src/identities/dto/transaction-permissions.dto.ts deleted file mode 100644 index d11bf534..00000000 --- a/src/identities/dto/transaction-permissions.dto.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { - ModuleName, - TransactionPermissions, - TxTag, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { ArrayNotEmpty, IsArray, IsOptional } from 'class-validator'; - -import { IsTxTag, IsTxTagOrModuleName } from '~/common/decorators/validation'; -import { getTxTags, getTxTagsWithModuleNames } from '~/common/utils'; -import { PermissionTypeDto } from '~/identities/dto/permission-type.dto'; - -export class TransactionPermissionsDto extends PermissionTypeDto { - @ApiProperty({ - description: 'Transactions to be included/excluded', - isArray: true, - enum: getTxTagsWithModuleNames(), - example: [ModuleName.Asset, TxTags.checkpoint.CreateCheckpoint], - }) - @IsArray() - @ArrayNotEmpty() - @IsTxTagOrModuleName({ each: true }) - readonly values: (TxTag | ModuleName)[]; - - @ApiProperty({ - description: - 'Transactions to be exempted from inclusion/exclusion. For example, if you wish to exclude the entire `asset` module except for `asset.createAsset`, you would pass `ModuleName.Asset` as part of the `values` array, and `TxTags.asset.CreateAsset` as part of the `exceptions` array', - isArray: true, - enum: getTxTags(), - example: [TxTags.asset.RegisterTicker], - }) - @IsArray() - @ArrayNotEmpty() - @IsTxTag({ each: true }) - @IsOptional() - readonly exceptions?: TxTag[]; - - public toTransactionPermissions(): TransactionPermissions | null { - const { values, type, exceptions } = this; - - return { - values, - type, - exceptions, - }; - } - - constructor(dto: Omit) { - super(); - Object.assign(this, dto); - } -} diff --git a/src/identities/identities.controller.spec.ts b/src/identities/identities.controller.spec.ts deleted file mode 100644 index 425bed0a..00000000 --- a/src/identities/identities.controller.spec.ts +++ /dev/null @@ -1,660 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationType, - CddClaim, - ClaimData, - ClaimScope, - ClaimType, - GenericAuthorizationData, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { PermissionedAccountModel } from '~/accounts/models/permissioned-account.model'; -import { PermissionsModel } from '~/accounts/models/permissions.model'; -import { AssetsService } from '~/assets/assets.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { PendingAuthorizationsModel } from '~/authorizations/models/pending-authorizations.model'; -import { ClaimsService } from '~/claims/claims.service'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesController } from '~/identities/identities.controller'; -import { IdentitiesService } from '~/identities/identities.service'; -import * as identityUtil from '~/identities/identities.util'; -import { AccountModel } from '~/identities/models/account.model'; -import { IdentityModel } from '~/identities/models/identity.model'; -import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAuthorizationRequest, - MockIdentity, - MockTickerReservation, - MockVenue, -} from '~/test-utils/mocks'; -import { - MockAssetService, - MockAuthorizationsService, - mockClaimsServiceProvider, - mockDeveloperServiceProvider, - MockIdentitiesService, - MockSettlementsService, - MockTickerReservationsService, -} from '~/test-utils/service-mocks'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -const { did, txResult, ticker } = testValues; - -describe('IdentitiesController', () => { - let controller: IdentitiesController; - const mockAssetsService = new MockAssetService(); - - const mockSettlementsService = new MockSettlementsService(); - - const mockIdentitiesService = new MockIdentitiesService(); - - const mockAuthorizationsService = new MockAuthorizationsService(); - - let mockClaimsService: DeepMocked; - - const mockTickerReservationsService = new MockTickerReservationsService(); - - const mockDeveloperTestingService = mockDeveloperServiceProvider.useValue; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - controllers: [IdentitiesController], - providers: [ - AssetsService, - SettlementsService, - IdentitiesService, - AuthorizationsService, - mockClaimsServiceProvider, - TickerReservationsService, - mockPolymeshLoggerProvider, - mockDeveloperServiceProvider, - ], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .overrideProvider(SettlementsService) - .useValue(mockSettlementsService) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AuthorizationsService) - .useValue(mockAuthorizationsService) - .overrideProvider(TickerReservationsService) - .useValue(mockTickerReservationsService) - .compile(); - - mockClaimsService = mockClaimsServiceProvider.useValue as DeepMocked; - controller = module.get(IdentitiesController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getAssets', () => { - it("should return the Identity's Assets", async () => { - const assets = ['FOO', 'BAR', 'BAZ']; - mockAssetsService.findAllByOwner.mockResolvedValue(assets); - - const result = await controller.getAssets({ did: '0x1' }); - - expect(result).toEqual({ results: assets }); - }); - }); - - describe('getHeldAssets', () => { - it('should return a paginated list of held Assets', async () => { - const mockResults = ['TICKER', 'TICKER2']; - const mockAssets = { - data: mockResults.map(asset => ({ ticker: asset })), - next: new BigNumber(2), - count: new BigNumber(2), - }; - - mockIdentitiesService.findHeldAssets.mockResolvedValue(mockAssets); - - const result = await controller.getHeldAssets( - { did: '0x1' }, - { start: new BigNumber(0), size: new BigNumber(2) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockResults, - total: new BigNumber(mockAssets.count), - next: mockAssets.next, - }) - ); - }); - }); - - describe('getPendingInstructions', () => { - it("should return the Identity's pending Instructions", async () => { - const expectedInstructionIds = [new BigNumber(1), new BigNumber(2), new BigNumber(3)]; - mockSettlementsService.findGroupedInstructionsByDid.mockResolvedValue({ - pending: expectedInstructionIds.map(id => ({ id })), - }); - - const result = await controller.getPendingInstructions({ did: '0x1' }); - - expect(result).toEqual({ results: expectedInstructionIds }); - }); - }); - - describe('getVenues', () => { - it("should return the Identity's Venues", async () => { - const mockResults = [new MockVenue()]; - mockSettlementsService.findVenuesByOwner.mockResolvedValue(mockResults); - - const result = await controller.getVenues({ did }); - expect(result).toEqual({ - results: [ - expect.objectContaining({ - id: new BigNumber(1), - }), - ], - }); - }); - }); - - describe('getIdentityDetails', () => { - it("should return the Identity's details", async () => { - const mockIdentityDetails = new IdentityModel({ - did, - primaryAccount: { - account: new AccountModel({ - address: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }), - permissions: { - portfolios: null, - assets: null, - transactions: null, - transactionGroups: [], - }, - }, - secondaryAccountsFrozen: false, - secondaryAccounts: [], - }); - - const mockIdentity = new MockIdentity(); - mockIdentity.did = did; - mockIdentity.getPrimaryAccount.mockResolvedValue({ - account: { - address: '5GNWrbft4pJcYSak9tkvUy89e2AKimEwHb6CKaJq81KHEj8e', - }, - permissions: { - portfolios: null, - assets: null, - transactions: null, - transactionGroups: [], - }, - }); - mockIdentity.areSecondaryAccountsFrozen.mockResolvedValue(false); - mockIdentity.getSecondaryAccounts.mockResolvedValue({ data: [], next: null, count: 0 }); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - - const result = await controller.getIdentityDetails({ did }); - - expect(result).toEqual(mockIdentityDetails); - }); - }); - - describe('getPendingAuthorizations', () => { - const targetDid = '0x1'.padEnd(66, '0'); - const pendingAuthorization = { - authId: new BigNumber(2236), - issuer: { - did: targetDid, - }, - data: { - type: AuthorizationType.TransferTicker, - value: 'FOO', - } as unknown as GenericAuthorizationData, - expiry: null, - target: { - did, - }, - }; - - const issuedAuthorization = { - authId: new BigNumber(2237), - issuer: { - did, - }, - data: { - type: AuthorizationType.TransferAssetOwnership, - value: 'FOO2', - } as unknown as GenericAuthorizationData, - expiry: new Date('10/14/1987'), - target: { - did: targetDid, - }, - isExpired: jest.fn().mockReturnValue(true), - }; - - const mockReceivedAuthorization = new AuthorizationRequestModel({ - id: pendingAuthorization.authId, - issuer: expect.objectContaining({ - did: targetDid, - }), - data: pendingAuthorization.data, - expiry: null, - target: new IdentitySignerModel({ did }), - }); - - const mockSentAuthorization = new AuthorizationRequestModel({ - id: issuedAuthorization.authId, - issuer: expect.objectContaining({ - did, - }), - data: issuedAuthorization.data, - expiry: new Date('10/14/1987'), - target: new IdentitySignerModel({ did: targetDid }), - }); - - beforeEach(() => { - mockAuthorizationsService.findPendingByDid.mockResolvedValue([pendingAuthorization]); - mockAuthorizationsService.findIssuedByDid.mockResolvedValue({ data: [issuedAuthorization] }); - }); - - it('should return list of pending authorizations for a given Identity', async () => { - let result = await controller.getPendingAuthorizations({ did }, { includeExpired: true }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [mockSentAuthorization], - }) - ); - - mockAuthorizationsService.findIssuedByDid.mockResolvedValue({ data: [] }); - result = await controller.getPendingAuthorizations({ did }, {}); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - }); - - it('should support filtering pending authorizations by authorization type', async () => { - const result = await controller.getPendingAuthorizations( - { did }, - { type: AuthorizationType.TransferTicker } - ); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - }); - - it('should support filtering pending Authorizations by whether they have expired or not', async () => { - let result = await controller.getPendingAuthorizations({ did }, { includeExpired: false }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [], - }) - ); - - result = await controller.getPendingAuthorizations({ did }, { includeExpired: true }); - expect(result).toEqual( - new PendingAuthorizationsModel({ - received: [mockReceivedAuthorization], - sent: [mockSentAuthorization], - }) - ); - }); - }); - - describe('getPendingAuthorization', () => { - it('should call the service and return the AuthorizationRequest details', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - mockAuthorizationsService.findOneByDid.mockResolvedValue(mockAuthorization); - const result = await controller.getPendingAuthorization({ - did, - id: new BigNumber(1), - }); - expect(result).toEqual({ - id: mockAuthorization.authId, - expiry: null, - data: { - type: 'PortfolioCustody', - value: { - did: mockAuthorization.data.value.did, - id: new BigNumber(1), - }, - }, - issuer: mockAuthorization.issuer, - target: new IdentitySignerModel({ did: mockAuthorization.target.did }), - }); - }); - }); - - describe('getIssuedClaims', () => { - const targetDid = '0x6'.padEnd(66, '1'); - const claims = [ - { - issuedAt: new Date(), - expiry: null, - claim: { - type: 'CustomerDueDiligence', - id: '0xcc32ef7ab217d4f1f8cc2ecea89e09234f3bbf8f96af56d55c819037d4603552', - }, - target: { - did: targetDid, - }, - issuer: { - did, - }, - }, - ]; - const paginatedResult = { - data: claims, - next: null, - count: new BigNumber(1), - }; - it('should give issued Claims with no start value', async () => { - mockClaimsService.findIssuedByDid.mockResolvedValue(paginatedResult as ResultSet); - - const result = await controller.getIssuedClaims( - { did }, - { size: new BigNumber(10) }, - { includeExpired: false } - ); - expect(result).toEqual({ - total: paginatedResult.count, - next: paginatedResult.next, - results: paginatedResult.data, - }); - }); - - it('should give issued Claims with start value', async () => { - mockClaimsService.findIssuedByDid.mockResolvedValue(paginatedResult as ResultSet); - const result = await controller.getIssuedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { includeExpired: false } - ); - expect(result).toEqual({ - total: paginatedResult.count, - next: paginatedResult.next, - results: paginatedResult.data, - }); - }); - }); - - describe('getAssociatedClaims', () => { - const mockAssociatedClaims = { - data: [ - { - issuedAt: '2020-08-21T16:36:55.000Z', - expiry: null, - claim: { - type: ClaimType.Accredited, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - target: { - did, - }, - issuer: { - did: '0x6'.padEnd(66, '1'), - }, - }, - ], - next: null, - count: new BigNumber(1), - }; - - it('should give associated Claims with no start value', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims({ did }, { size: new BigNumber(10) }, {}); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims with start value', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - {} - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims with claim type filter', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { claimTypes: [ClaimType.Accredited] } - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - - it('should give associated Claims by whether they have expired or not', async () => { - mockClaimsService.findAssociatedByDid.mockResolvedValue( - mockAssociatedClaims as unknown as ResultSet - ); - const result = await controller.getAssociatedClaims( - { did }, - { size: new BigNumber(10), start: new BigNumber(1) }, - { includeExpired: true } - ); - expect(result).toEqual(new ResultsModel({ results: mockAssociatedClaims.data })); - }); - }); - - describe('getTrustingAssets', () => { - it('should return the list of Assets for which the Identity is a default trusted Claim Issuer', async () => { - const mockAssets = [ - { - ticker: 'BAR_TICKER', - }, - { - ticker: 'FOO_TICKER', - }, - ]; - mockIdentitiesService.findTrustingAssets.mockResolvedValue(mockAssets); - - const result = await controller.getTrustingAssets({ did }); - - expect(result).toEqual(new ResultsModel({ results: mockAssets })); - }); - }); - - describe('addSecondaryAccount', () => { - it('should return the transaction details on adding a Secondary Account', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockIdentitiesService.addSecondaryAccount.mockResolvedValue(mockData); - - const result = await controller.addSecondaryAccount({ - signer: 'Ox60', - secondaryAccount: '5xdd', - }); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - }); - }); - - describe('getTickerReservations', () => { - it('should call the service and return all the reserved tickers', async () => { - const mockTickerReservation = new MockTickerReservation(); - - mockTickerReservationsService.findAllByOwner.mockResolvedValue([mockTickerReservation]); - - const result = await controller.getTickerReservations({ did }); - expect(result).toEqual({ - results: [mockTickerReservation], - }); - expect(mockTickerReservationsService.findAllByOwner).toHaveBeenCalledWith(did); - }); - }); - - describe('mockCdd', () => { - it('should call the service and return the Identity', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fakeIdentityModel = 'fakeIdentityModel' as any; - jest.spyOn(identityUtil, 'createIdentityModel').mockResolvedValue(fakeIdentityModel); - - const params = { - address: '5abc', - initialPolyx: new BigNumber(10), - }; - - const result = await controller.createMockCdd(params); - expect(result).toEqual(fakeIdentityModel); - expect(mockDeveloperTestingService.createMockCdd).toHaveBeenCalledWith(params); - }); - }); - - describe('getClaimScopes', () => { - it('should call the service and return the list of claim scopes', async () => { - const params = { - did, - }; - const mockClaims = [ - { - ticker, - scope: { - type: 'Identity', - value: '0x9'.padEnd(66, '1'), - }, - }, - ] as unknown as ClaimScope[]; - - mockClaimsService.findClaimScopesByDid.mockResolvedValue(mockClaims); - - const { results } = await controller.getClaimScopes(params); - expect(results).toEqual(mockClaims); - expect(mockClaimsService.findClaimScopesByDid).toHaveBeenCalledWith(did); - }); - }); - - describe('getCddClaims', () => { - const date = new Date().toISOString(); - const mockCddClaims = [ - { - target: did, - issuer: did, - issuedAt: date, - expiry: date, - claim: { - type: 'Accredited', - scope: { - type: 'Identity', - value: did, - }, - }, - }, - ] as unknown as ClaimData[]; - - it('should call the service and return list of CDD Claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: false }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, false); - }); - - it('should call the service and return list of CDD Claims including expired claims', async () => { - mockClaimsService.findCddClaimsByDid.mockResolvedValue(mockCddClaims); - const result = await controller.getCddClaims({ did }, { includeExpired: true }); - expect(result).toEqual(new ResultsModel({ results: mockCddClaims })); - expect(mockClaimsService.findCddClaimsByDid).toHaveBeenCalledWith(did, true); - }); - }); - - describe('registerIdentity', () => { - it('should return the transaction details on adding registering an Identity', async () => { - const identity = new MockIdentity(); - const address = 'address'; - identity.getPrimaryAccount.mockResolvedValue({ - account: { address }, - permissions: [], - }); - identity.areSecondaryAccountsFrozen.mockResolvedValue(false); - identity.getSecondaryAccounts.mockResolvedValue({ data: [] }); - - const identityData = new IdentityModel({ - did, - primaryAccount: new PermissionedAccountModel({ - account: new AccountModel({ address }), - permissions: new PermissionsModel({ - assets: null, - portfolios: null, - transactionGroups: [], - transactions: null, - }), - }), - secondaryAccounts: [], - secondaryAccountsFrozen: false, - }); - - const mockData = { - ...txResult, - result: identity, - }; - mockIdentitiesService.registerDid.mockResolvedValue(mockData); - - const data: RegisterIdentityDto = { - signer: 'Ox60', - targetAccount: 'address', - createCdd: false, - }; - - const result = await controller.registerIdentity(data); - - expect(result).toEqual({ - ...txResult, - identity: identityData, - }); - }); - }); - - describe('getGroupedInstructions', () => { - it("should return the Identity's Instructions", async () => { - const expectedInstructionIds = [new BigNumber(1), new BigNumber(2), new BigNumber(3)]; - - mockSettlementsService.findGroupedInstructionsByDid.mockResolvedValue({ - affirmed: expectedInstructionIds.map(id => id), - pending: expectedInstructionIds.map(id => id), - failed: expectedInstructionIds.map(id => id), - }); - - const result = await controller.getGroupedInstructions({ did: '0x1' }); - - expect(result).toEqual({ - affirmed: expectedInstructionIds, - pending: expectedInstructionIds, - failed: expectedInstructionIds, - }); - }); - }); -}); diff --git a/src/identities/identities.controller.ts b/src/identities/identities.controller.ts deleted file mode 100644 index db51f76c..00000000 --- a/src/identities/identities.controller.ts +++ /dev/null @@ -1,625 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiInternalServerErrorResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationType, - Claim, - ClaimScope, - ClaimType, - FungibleAsset, - NftCollection, - TickerReservation, - Venue, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AuthorizationsService } from '~/authorizations/authorizations.service'; -import { - authorizationRequestResolver, - createAuthorizationRequestModel, -} from '~/authorizations/authorizations.util'; -import { AuthorizationParamsDto } from '~/authorizations/dto/authorization-params.dto'; -import { AuthorizationsFilterDto } from '~/authorizations/dto/authorizations-filter.dto'; -import { AuthorizationRequestModel } from '~/authorizations/models/authorization-request.model'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { PendingAuthorizationsModel } from '~/authorizations/models/pending-authorizations.model'; -import { ClaimsService } from '~/claims/claims.service'; -import { ClaimsFilterDto } from '~/claims/dto/claims-filter.dto'; -import { CddClaimModel } from '~/claims/models/cdd-claim.model'; -import { ClaimModel } from '~/claims/models/claim.model'; -import { ClaimScopeModel } from '~/claims/models/claim-scope.model'; -import { - ApiArrayResponse, - ApiArrayResponseReplaceModelProperties, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto'; -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesService } from '~/identities/identities.service'; -import { createIdentityModel } from '~/identities/identities.util'; -import { CreatedIdentityModel } from '~/identities/models/created-identity.model'; -import { IdentityModel } from '~/identities/models/identity.model'; -import { createIdentityResolver } from '~/identities/models/identity.util'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { GroupedInstructionModel } from '~/settlements/models/grouped-instructions.model'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -@ApiTags('identities') -@Controller('identities') -export class IdentitiesController { - constructor( - private readonly assetsService: AssetsService, - private readonly settlementsService: SettlementsService, - private readonly identitiesService: IdentitiesService, - private readonly authorizationsService: AuthorizationsService, - private readonly claimsService: ClaimsService, - private readonly tickerReservationsService: TickerReservationsService, - private readonly developerTestingService: DeveloperTestingService, - private readonly logger: PolymeshLogger - ) { - logger.setContext(IdentitiesController.name); - } - - @Post('register') - @ApiOperation({ - summary: 'Register Identity', - description: - 'This endpoint allows registering a new Identity. The transaction signer must be a CDD provider. This will create Authorization Requests which have to be accepted by any secondary accounts if they were specified.', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedIdentityModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: ['Expiry cannot be set unless a CDD claim is being created'], - }) - async registerIdentity( - @Body() registerIdentityDto: RegisterIdentityDto - ): Promise { - this.logger.debug('Registering new identity'); - const serviceResult = await this.identitiesService.registerDid(registerIdentityDto); - - return handleServiceResult(serviceResult, createIdentityResolver); - } - - @Get(':did') - @ApiOperation({ - summary: 'Get Identity details', - description: 'This endpoint will allow you to give the basic details of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose details are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Returns basic details of the Identity', - type: IdentityModel, - }) - async getIdentityDetails(@Param() { did }: DidDto): Promise { - this.logger.debug(`Get identity details for did ${did}`); - const identity = await this.identitiesService.findOne(did); - return createIdentityModel(identity); - } - - @ApiTags('authorizations') - @ApiOperation({ - summary: 'Get pending Authorizations received by an Identity', - description: - 'This endpoint will provide list of all the pending Authorizations received by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose pending Authorizations are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'type', - description: 'Authorization type to be filtered', - type: 'string', - enum: AuthorizationType, - required: false, - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired authorizations or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiOkResponse({ - description: - 'List of all pending Authorizations for which the given Identity is either the issuer or the target', - type: PendingAuthorizationsModel, - }) - @Get(':did/pending-authorizations') - async getPendingAuthorizations( - @Param() { did }: DidDto, - @Query() { type, includeExpired }: AuthorizationsFilterDto - ): Promise { - const [pending, issued] = await Promise.all([ - this.authorizationsService.findPendingByDid(did, includeExpired, type), - this.authorizationsService.findIssuedByDid(did), - ]); - - let { data: sent } = issued; - if (sent.length > 0) { - sent = sent.filter( - ({ isExpired, data: { type: authType } }) => - (includeExpired || !isExpired()) && (!type || type === authType) - ); - } - - return new PendingAuthorizationsModel({ - received: pending.map(createAuthorizationRequestModel), - sent: sent.map(createAuthorizationRequestModel), - }); - } - - @ApiTags('authorizations') - @ApiOperation({ - summary: 'Get a pending Authorization', - description: - 'This endpoint will return a specific Authorization issued by or targeting an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the issuer or target Identity of the Authorization being fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Authorization to be fetched', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of the Authorization', - type: AuthorizationRequestModel, - }) - @Get(':did/pending-authorizations/:id') - async getPendingAuthorization( - @Param() { did, id }: AuthorizationParamsDto - ): Promise { - const authorizationRequest = await this.authorizationsService.findOneByDid(did, id); - return createAuthorizationRequestModel(authorizationRequest); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets owned by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - paginated: false, - example: ['FOO_TICKER', 'BAR_TICKER', 'BAZ_TICKER'], - }) - @Get(':did/assets') - public async getAssets( - @Param() { did }: DidDto - ): Promise> { - const results = await this.assetsService.findAllByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets held by an Identity', - description: - 'This endpoint returns a list of all Assets which were held at one point by the given Identity. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity for which held Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of all the held Assets', - paginated: true, - example: ['FOO_TICKER', 'BAR_TICKER', 'BAZ_TICKER'], - }) - @Get(':did/held-assets') - public async getHeldAssets( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { data, count, next } = await this.identitiesService.findHeldAssets( - did, - size, - new BigNumber(start || 0) - ); - return new PaginatedResultsModel({ - results: data.map(({ ticker }) => ticker), - total: count, - next, - }); - } - - @ApiTags('settlements', 'instructions') - @ApiOperation({ - summary: 'Fetch all pending settlement Instructions where an Identity is involved', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose pending settlement Instructions are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all pending Instructions', - paginated: false, - example: ['123', '456', '789'], - }) - @Get(':did/pending-instructions') - public async getPendingInstructions(@Param() { did }: DidDto): Promise> { - const { pending } = await this.settlementsService.findGroupedInstructionsByDid(did); - - return new ResultsModel({ results: pending.map(({ id }) => id) }); - } - - @ApiTags('settlements') - @ApiOperation({ - summary: 'Get all Venues owned by an Identity', - description: 'This endpoint will provide list of venues for an identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Venues are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of IDs of all owned Venues', - paginated: false, - example: ['123', '456', '789'], - }) - @Get(':did/venues') - async getVenues(@Param() { did }: DidDto): Promise> { - const results = await this.settlementsService.findVenuesByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Get all issued Claims', - description: 'This endpoint will provide a list of all the Claims issued by an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose issued Claims are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Claims to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which Claims are to be fetched', - type: 'string', - required: false, - example: '0', - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired Claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiArrayResponse(ClaimModel, { - description: 'List of issued Claims for the given DID', - paginated: true, - }) - @Get(':did/issued-claims') - async getIssuedClaims( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto, - @Query() { includeExpired }: IncludeExpiredFilterDto - ): Promise>> { - this.logger.debug( - `Fetch ${size} issued claims for did ${did} starting from ${size} with include expired ${includeExpired}` - ); - - const claimsResultSet = await this.claimsService.findIssuedByDid( - did, - includeExpired, - size, - new BigNumber(start || 0) - ); - - const claimsData = claimsResultSet.data.map( - ({ issuedAt, expiry, claim, target, issuer }) => - new ClaimModel({ - issuedAt, - expiry, - claim, - target, - issuer, - }) - ); - - return new PaginatedResultsModel({ - results: claimsData, - next: claimsResultSet.next, - total: claimsResultSet.count, - }); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Get all Claims targeting an Identity', - description: 'This endpoint will provide a list of all the Claims made about an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose associated Claims are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Claims to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which Claims are to be fetched', - type: 'string', - required: false, - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired Claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiQuery({ - name: 'claimTypes', - description: 'Claim types for filtering associated Claims', - type: 'string', - required: false, - isArray: true, - enum: ClaimType, - example: [ClaimType.Accredited, ClaimType.CustomerDueDiligence], - }) - @ApiArrayResponse(ClaimModel, { - description: 'List of associated Claims for the given DID', - paginated: true, - }) - @Get(':did/associated-claims') - async getAssociatedClaims( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto, - @Query() { claimTypes, includeExpired }: ClaimsFilterDto - ): Promise> { - const claimsResultSet = await this.claimsService.findAssociatedByDid( - did, - undefined, - claimTypes, - includeExpired, - size, - new BigNumber(start || 0) - ); - const results = claimsResultSet.data.map( - ({ issuedAt, expiry, claim, target, issuer }) => - new ClaimModel({ - issuedAt, - expiry, - claim, - target, - issuer, - }) - ); - - return new ResultsModel({ results }); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch all Assets for which an Identity is a trusted Claim Issuer', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Claim Issuer for which the Assets are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of Assets for which the Identity is a trusted Claim Issuer', - paginated: false, - example: ['SOME_TICKER', 'RANDOM_TICKER'], - }) - @Get(':did/trusting-assets') - async getTrustingAssets(@Param() { did }: DidDto): Promise> { - const results = await this.identitiesService.findTrustingAssets(did); - return new ResultsModel({ results }); - } - - // TODO @prashantasdeveloper Update the response codes on the error codes are finalized in SDK - @ApiOperation({ - summary: 'Add Secondary Account', - description: - 'This endpoint will send an invitation to a Secondary Account to join the Identity of the signer. It also defines the set of permissions the Secondary Account will have.', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiInternalServerErrorResponse({ - description: "The supplied address is not encoded with the chain's SS58 format", - }) - @ApiBadRequestResponse({ - description: - 'The target Account is already part of an Identity or already has a pending invitation to join this Identity', - }) - @Post('/secondary-accounts/invite') - async addSecondaryAccount( - @Body() addSecondaryAccountParamsDto: AddSecondaryAccountParamsDto - ): Promise { - const serviceResult = await this.identitiesService.addSecondaryAccount( - addSecondaryAccountParamsDto - ); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiTags('ticker-reservations') - @ApiOperation({ - summary: 'Fetch all tickers reserved by an Identity', - description: - "This endpoint provides all the tickers currently reserved by an Identity. This doesn't include Assets that have already been created. Tickers with unreadable characters in them will be left out", - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose reserved tickers are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse('string', { - description: 'List of tickers', - paginated: false, - example: ['SOME_TICKER', 'RANDOM_TICKER'], - }) - @Get(':did/ticker-reservations') - public async getTickerReservations( - @Param() { did }: DidDto - ): Promise> { - const results = await this.tickerReservationsService.findAllByOwner(did); - return new ResultsModel({ results }); - } - - @ApiTags('developer-testing') - @ApiOperation({ - summary: - 'Creates a fake Identity for an Account and sets its POLYX balance (DEPRECATED: Use `/developer-testing/create-test-account` instead)', - description: - 'This endpoint creates a Identity for an Account and sets its POLYX balance. A sudo account must be configured.', - }) - @ApiOkResponse({ description: 'The details of the newly created Identity' }) - @ApiBadRequestResponse({ - description: - 'This instance of the REST API is pointing to a chain that lacks development features. A proper CDD provider must be used instead', - }) - @ApiInternalServerErrorResponse({ - description: 'Failed to execute an extrinsic, or something unexpected', - }) - @Post('/mock-cdd') - public async createMockCdd(@Body() params: CreateMockIdentityDto): Promise { - const identity = await this.developerTestingService.createMockCdd(params); - return createIdentityModel(identity); - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Fetch all CDD claims for an Identity', - description: 'This endpoint will fetch the list of CDD claims for a target DID', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose CDD claims are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiQuery({ - name: 'includeExpired', - description: 'Indicates whether to include expired CDD claims or not. Defaults to true', - type: 'boolean', - required: false, - }) - @ApiArrayResponseReplaceModelProperties( - ClaimModel, - { - description: 'List of CDD claims for the target DID', - paginated: false, - }, - { claim: CddClaimModel } - ) - @Get(':did/cdd-claims') - public async getCddClaims( - @Param() { did }: DidDto, - @Query() { includeExpired }: IncludeExpiredFilterDto - ): Promise>> { - const cddClaims = await this.claimsService.findCddClaimsByDid(did, includeExpired); - - const results = cddClaims.map(claim => new ClaimModel(claim)); - - return { results }; - } - - @ApiTags('claims') - @ApiOperation({ - summary: 'Fetch all claim scopes for an Identity', - description: - 'This endpoint will fetch all scopes in which claims have been made for the given DID.', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose claim scopes are to be fetched', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(ClaimScopeModel, { - description: 'List of claim scopes', - paginated: false, - }) - @Get(':did/claim-scopes') - public async getClaimScopes(@Param() { did }: DidDto): Promise> { - const claimResultSet = await this.claimsService.findClaimScopesByDid(did); - - const results = claimResultSet.map(claimScope => new ClaimScopeModel(claimScope)); - - return new ResultsModel({ results }); - } - - @Get(':did/grouped-instructions') - @ApiParam({ - name: 'did', - description: 'The DID of the Identity for which to get grouped Instructions', - type: 'string', - required: true, - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Returns grouped Instructions for the Identity', - type: GroupedInstructionModel, - }) - public async getGroupedInstructions(@Param() { did }: DidDto): Promise { - const result = await this.settlementsService.findGroupedInstructionsByDid(did); - - return new GroupedInstructionModel(result); - } -} diff --git a/src/identities/identities.module.ts b/src/identities/identities.module.ts deleted file mode 100644 index acc9af33..00000000 --- a/src/identities/identities.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { AssetsModule } from '~/assets/assets.module'; -import { AuthorizationsModule } from '~/authorizations/authorizations.module'; -import { ClaimsModule } from '~/claims/claims.module'; -import { DeveloperTestingModule } from '~/developer-testing/developer-testing.module'; -import { IdentitiesController } from '~/identities/identities.controller'; -import { IdentitiesService } from '~/identities/identities.service'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosModule } from '~/portfolios/portfolios.module'; -import { SettlementsModule } from '~/settlements/settlements.module'; -import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservations.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [ - PolymeshModule, - LoggerModule, - TransactionsModule, - forwardRef(() => AssetsModule), - forwardRef(() => SettlementsModule), - forwardRef(() => AuthorizationsModule), - forwardRef(() => PortfoliosModule), - DeveloperTestingModule.register(), - AccountsModule, - ClaimsModule, - TickerReservationsModule, - ], - controllers: [IdentitiesController], - providers: [IdentitiesService], - exports: [IdentitiesService], -}) -export class IdentitiesModule {} diff --git a/src/identities/identities.service.spec.ts b/src/identities/identities.service.spec.ts deleted file mode 100644 index fcbb8a02..00000000 --- a/src/identities/identities.service.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { IdentitiesService } from '~/identities/identities.service'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; -import { MockIdentity, MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { - MockAccountsService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -jest.mock('@polkadot/keyring', () => ({ - ...jest.requireActual('@polkadot/keyring'), - Keyring: jest.fn().mockImplementation(() => { - return { - addFromUri: jest.fn(), - }; - }), -})); - -describe('IdentitiesService', () => { - let service: IdentitiesService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - const mockAccountsService = new MockAccountsService(); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [ - IdentitiesService, - AccountsService, - mockPolymeshLoggerProvider, - mockSigningProvider, - mockTransactionsProvider, - ], - }) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - service = module.get(IdentitiesService); - polymeshService = module.get(PolymeshService); - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the Identity for a valid DID', async () => { - const fakeResult = 'identity'; - - mockPolymeshApi.identities.getIdentity.mockResolvedValue(fakeResult); - - const result = await service.findOne('realDid'); - - expect(result).toBe(fakeResult); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.identities.getIdentity.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne('invalidDID')).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findTrustingAssets', () => { - it('should return the list of Assets for which the Identity is a default trusted Claim Issuer', async () => { - const mockAssets = [ - { - ticker: 'FAKE_TICKER', - }, - { - ticker: 'RANDOM_TICKER', - }, - ]; - const mockIdentity = new MockIdentity(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockIdentity as any); - mockIdentity.getTrustingAssets.mockResolvedValue(mockAssets); - - const result = await service.findTrustingAssets('TICKER'); - expect(result).toEqual(mockAssets); - }); - }); - - describe('findHeldAssets', () => { - it('should return the list of Assets held by an Identity', async () => { - const mockAssets = { - data: [ - { - ticker: 'TICKER', - }, - { - ticker: 'TICKER2', - }, - ], - next: new BigNumber(2), - count: new BigNumber(2), - }; - const mockIdentity = new MockIdentity(); - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockIdentity as any); - mockIdentity.getHeldAssets.mockResolvedValue(mockAssets); - - const result = await service.findHeldAssets('0x01', new BigNumber(2), new BigNumber(0)); - expect(result).toEqual(mockAssets); - }); - }); - - describe('addSecondaryAccount', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.JoinIdentityAsKey, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - secondaryAccount: 'address', - }; - - const result = await service.addSecondaryAccount(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalled(); - }); - }); - }); - - describe('registerDid', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.CddRegisterDid, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body: RegisterIdentityDto = { - signer, - targetAccount: 'address', - createCdd: false, - }; - - const result = await service.registerDid(body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/identities/identities.service.ts b/src/identities/identities.service.ts deleted file mode 100644 index 757baa3c..00000000 --- a/src/identities/identities.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationRequest, - FungibleAsset, - Identity, - RegisterIdentityParams, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto'; -import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class IdentitiesService { - private alicePair: KeyringPair; - - constructor( - private readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger, - private readonly transactionsService: TransactionsService - ) { - logger.setContext(IdentitiesService.name); - } - - /** - * Method to get identity for a specific did - */ - public async findOne(did: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return await polymeshApi.identities.getIdentity({ did }).catch(error => { - throw handleSdkError(error); - }); - } - - /** - * Method to get trusting Assets for a specific did - */ - public async findTrustingAssets(did: string): Promise { - const identity = await this.findOne(did); - return identity.getTrustingAssets(); - } - - public async findHeldAssets( - did: string, - size?: BigNumber, - start?: BigNumber - ): Promise> { - const identity = await this.findOne(did); - return identity.getHeldAssets({ size, start }); - } - - public async addSecondaryAccount( - addSecondaryAccountParamsDto: AddSecondaryAccountParamsDto - ): ServiceReturn { - const { - base, - args: { secondaryAccount: targetAccount, permissions, expiry }, - } = extractTxBase(addSecondaryAccountParamsDto); - const params = { - targetAccount, - permissions: permissions?.toPermissionsLike(), - expiry, - }; - const { inviteAccount } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(inviteAccount, params, base); - } - - public async registerDid(registerIdentityDto: RegisterIdentityDto): ServiceReturn { - const { - polymeshService: { polymeshApi }, - } = this; - - const { - base, - args: { targetAccount, secondaryAccounts, createCdd, expiry }, - } = extractTxBase(registerIdentityDto); - - const params = { - targetAccount, - secondaryAccounts: secondaryAccounts?.map(({ secondaryAccount, permissions }) => ({ - secondaryAccount, - permissions: permissions?.toPermissionsLike(), - })), - createCdd, - expiry, - } as RegisterIdentityParams; - - const { registerIdentity } = polymeshApi.identities; - - return this.transactionsService.submit(registerIdentity, params, base); - } -} diff --git a/src/identities/identities.util.ts b/src/identities/identities.util.ts deleted file mode 100644 index ddda70ef..00000000 --- a/src/identities/identities.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** istanbul ignore file */ - -import { Identity, Signer } from '@polymeshassociation/polymesh-sdk/types'; -import { isAccount } from '@polymeshassociation/polymesh-sdk/utils'; - -import { createPermissionedAccountModel } from '~/accounts/accounts.util'; -import { AccountModel } from '~/identities/models/account.model'; -import { IdentityModel } from '~/identities/models/identity.model'; -import { IdentitySignerModel } from '~/identities/models/identity-signer.model'; -import { SignerModel } from '~/identities/models/signer.model'; - -/** - * Fetch and assemble data for an Identity - */ -export async function createIdentityModel(identity: Identity): Promise { - const [primaryAccount, secondaryAccountsFrozen, { data: secondaryAccounts }] = await Promise.all([ - identity.getPrimaryAccount(), - identity.areSecondaryAccountsFrozen(), - identity.getSecondaryAccounts(), - ]); - return new IdentityModel({ - did: identity.did, - primaryAccount: createPermissionedAccountModel(primaryAccount), - secondaryAccountsFrozen, - secondaryAccounts: secondaryAccounts.map(createPermissionedAccountModel), - }); -} - -/** - * Create signer based on account/identity - */ -export function createSignerModel(signer: Signer): SignerModel { - if (isAccount(signer)) { - return new AccountModel({ - address: signer.address, - }); - } - return new IdentitySignerModel({ - did: signer.did, - }); -} diff --git a/src/identities/models/account-data.model.ts b/src/identities/models/account-data.model.ts deleted file mode 100644 index 75cb1266..00000000 --- a/src/identities/models/account-data.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PickType } from '@nestjs/swagger'; - -import { AccountModel } from '~/identities/models/account.model'; - -export class AccountDataModel extends PickType(AccountModel, ['address'] as const) {} diff --git a/src/identities/models/account.model.ts b/src/identities/models/account.model.ts deleted file mode 100644 index f09da1e9..00000000 --- a/src/identities/models/account.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SignerType } from '@polymeshassociation/polymesh-sdk/types'; - -import { SignerModel } from '~/identities/models/signer.model'; - -export class AccountModel extends SignerModel { - @ApiProperty({ - type: 'string', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - readonly address: string; - - constructor(model: Omit) { - super({ signerType: SignerType.Account }); - Object.assign(this, model); - } -} diff --git a/src/identities/models/created-identity.model.ts b/src/identities/models/created-identity.model.ts deleted file mode 100644 index ed6b7c7e..00000000 --- a/src/identities/models/created-identity.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { IdentityModel } from '~/identities/models/identity.model'; - -export class CreatedIdentityModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Static data (and identifiers) of the newly created Identity', - type: IdentityModel, - }) - @Type(() => IdentityModel) - readonly identity: IdentityModel; - - constructor(model: CreatedIdentityModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/identities/models/identity.util.ts b/src/identities/models/identity.util.ts deleted file mode 100644 index a55c294d..00000000 --- a/src/identities/models/identity.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** istanbul ignore file */ - -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionResolver } from '~/common/utils'; -import { createIdentityModel } from '~/identities/identities.util'; -import { CreatedIdentityModel } from '~/identities/models/created-identity.model'; - -export const createIdentityResolver: TransactionResolver = async ({ - transactions, - details, - result, -}) => { - const identity = await createIdentityModel(result); - - return new CreatedIdentityModel({ - transactions, - details, - identity, - }); -}; diff --git a/src/logger/logger.module.ts b/src/logger/logger.module.ts deleted file mode 100644 index ed5478ed..00000000 --- a/src/logger/logger.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -@Module({ - providers: [PolymeshLogger], - exports: [PolymeshLogger], -}) -export class LoggerModule {} diff --git a/src/logger/mock-polymesh-logger.ts b/src/logger/mock-polymesh-logger.ts deleted file mode 100644 index f4331f33..00000000 --- a/src/logger/mock-polymesh-logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { LoggerService } from '@nestjs/common'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -class MockPolymeshLogger implements LoggerService { - log = jest.fn(); - error = jest.fn(); - warn = jest.fn(); - debug = jest.fn(); - verbose = jest.fn(); - setContext = jest.fn(); -} - -export const mockPolymeshLoggerProvider = { - provide: PolymeshLogger, - useValue: new MockPolymeshLogger(), -}; diff --git a/src/logger/polymesh-logger.service.ts b/src/logger/polymesh-logger.service.ts deleted file mode 100644 index e1cd35a8..00000000 --- a/src/logger/polymesh-logger.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; - -@Injectable({ scope: Scope.TRANSIENT }) -export class PolymeshLogger extends ConsoleLogger {} diff --git a/src/main.ts b/src/main.ts index b50685e4..0c1e875e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,14 +5,16 @@ import { ConfigService } from '@nestjs/config'; import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AppModule } from '~/app.module'; -import { parseAuthStrategyConfig } from '~/auth/auth.utils'; -import { AuthStrategy } from '~/auth/strategies/strategies.consts'; -import { AppErrorToHttpResponseFilter } from '~/common/filters/app-error-to-http-response.filter'; -import { LoggingInterceptor } from '~/common/interceptors/logging.interceptor'; -import { WebhookResponseCodeInterceptor } from '~/common/interceptors/webhook-response-code.interceptor'; -import { swaggerDescription, swaggerTitle } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { parseAuthStrategyConfig } from '~/polymesh-rest-api/src/auth/auth.utils'; +import { AuthStrategy } from '~/polymesh-rest-api/src/auth/strategies/strategies.consts'; +import { AppErrorToHttpResponseFilter } from '~/polymesh-rest-api/src/common/filters/app-error-to-http-response.filter'; +import { LoggingInterceptor } from '~/polymesh-rest-api/src/common/interceptors/logging.interceptor'; +import { WebhookResponseCodeInterceptor } from '~/polymesh-rest-api/src/common/interceptors/webhook-response-code.interceptor'; +import { swaggerDescription, swaggerTitle } from '~/polymesh-rest-api/src/common/utils/consts'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; + +// eslint-disable-next-line no-restricted-imports +import { AppModule } from './app.module'; // This service was originally designed with node v14, this ensures a backwards compatible run time // Ideally we wouldn't need this function, but I am unable to find the cause when submitting SDK transactions that fail validation @@ -47,7 +49,7 @@ async function bootstrap(): Promise { const options = new DocumentBuilder() .setTitle(swaggerTitle) .setDescription(swaggerDescription) - .setVersion('5.0.0-alpha.1'); + .setVersion('1.0.0-alpha.7'); const configService = app.get(ConfigService); @@ -67,10 +69,19 @@ async function bootstrap(): Promise { if (isApiKeyStrategyConfigured) { document.security = [{ api_key: [] }]; // Apply the API key globally to all operations } - SwaggerModule.setup('/', app, document); + SwaggerModule.setup('/', app, document, { + swaggerOptions: { + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + }); // Fetch port from env and listen + const port = configService.get('PORT', 3000); + + app.enableShutdownHooks(); + await app.listen(port); } bootstrap(); diff --git a/src/metadata/dto/create-metadata.dto.ts b/src/metadata/dto/create-metadata.dto.ts deleted file mode 100644 index f92fd531..00000000 --- a/src/metadata/dto/create-metadata.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataSpecDto } from '~/metadata/dto/metadata-spec.dto'; -import { MetadataValueDetailsDto } from '~/metadata/dto/metadata-value-details.dto'; - -export class CreateMetadataDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Name of the Asset Metadata', - example: 'Maturity', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: 'Details about the Asset Metadata', - type: MetadataSpecDto, - }) - @ValidateNested() - @Type(() => MetadataSpecDto) - readonly specs: MetadataSpecDto; - - @ApiPropertyOptional({ - description: 'Value for the Asset Metadata', - type: 'string', - example: 'Some value', - }) - @ValidateIf(({ details, value }: CreateMetadataDto) => !!value || !!details) - @IsString() - readonly value?: string; - - @ApiPropertyOptional({ - description: 'Details about the Asset Metadata value', - type: MetadataValueDetailsDto, - }) - @IsOptional() - @ValidateNested() - @Type(() => MetadataValueDetailsDto) - readonly details?: MetadataValueDetailsDto; -} diff --git a/src/metadata/dto/metadata-params.dto.ts b/src/metadata/dto/metadata-params.dto.ts deleted file mode 100644 index a1cbec03..00000000 --- a/src/metadata/dto/metadata-params.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; - -export class MetadataParamsDto extends IdParamsDto { - @IsTicker() - readonly ticker: string; - - @IsEnum(MetadataType) - readonly type: MetadataType; -} diff --git a/src/metadata/dto/metadata-spec.dto.ts b/src/metadata/dto/metadata-spec.dto.ts deleted file mode 100644 index 1d3aa731..00000000 --- a/src/metadata/dto/metadata-spec.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; - -export class MetadataSpecDto { - @ApiPropertyOptional({ - description: 'Off-chain specs or documentation link', - type: 'string', - example: 'https://example.com', - }) - @IsOptional() - @IsString() - readonly url?: string; - - @ApiPropertyOptional({ - description: 'Description of the Metadata type', - type: 'string', - example: 'Some description', - }) - @IsOptional() - @IsString() - readonly description?: string; - - @ApiPropertyOptional({ - description: - '[SCALE](https://wiki.polkadot.network/docs/build-tools-index#scale-codec) encoded `AssetMetadataTypeDef`', - type: 'string', - example: 'Some example', - }) - @IsOptional() - @IsString() - readonly typedef?: string; -} diff --git a/src/metadata/dto/metadata-value-details.dto.ts b/src/metadata/dto/metadata-value-details.dto.ts deleted file mode 100644 index 1d09e083..00000000 --- a/src/metadata/dto/metadata-value-details.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { IsDate, IsEnum, IsOptional, ValidateIf } from 'class-validator'; - -export class MetadataValueDetailsDto { - @ApiProperty({ - description: 'Date at which the Metadata value expires, null if it never expires', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - default: null, - }) - @IsOptional() - @IsDate() - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Lock status of Metadata value', - enum: MetadataLockStatus, - example: MetadataLockStatus.LockedUntil, - }) - @IsEnum(MetadataLockStatus) - readonly lockStatus: MetadataLockStatus; - - @ApiPropertyOptional({ - description: - 'Date till which the Metadata value will be locked. This is required when `status` is `LockedUntil`', - type: 'string', - example: new Date('05/23/2021').toISOString(), - }) - @ValidateIf( - ({ lockStatus }: MetadataValueDetailsDto) => lockStatus === MetadataLockStatus.LockedUntil - ) - @IsDate() - readonly lockedUntil?: Date; -} diff --git a/src/metadata/dto/set-metadata.dto.ts b/src/metadata/dto/set-metadata.dto.ts deleted file mode 100644 index 6686ff07..00000000 --- a/src/metadata/dto/set-metadata.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsString, ValidateIf, ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataValueDetailsDto } from '~/metadata/dto/metadata-value-details.dto'; - -export class SetMetadataDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Value for the Asset Metadata', - type: 'string', - example: 'Some value', - }) - @ValidateIf(({ value }: SetMetadataDto) => !!value) - @IsString() - readonly value?: string; - - @ApiPropertyOptional({ - description: - 'Details about the Asset Metadata value which includes expiry and lock status of the `value`', - type: MetadataValueDetailsDto, - }) - @ValidateIf(({ details }: SetMetadataDto) => !!details) - @ValidateNested() - @Type(() => MetadataValueDetailsDto) - readonly details?: MetadataValueDetailsDto; -} diff --git a/src/metadata/metadata.controller.spec.ts b/src/metadata/metadata.controller.spec.ts deleted file mode 100644 index 17b366c5..00000000 --- a/src/metadata/metadata.controller.spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - MetadataEntry, - MetadataLockStatus, - MetadataType, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { TransactionType } from '~/common/types'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { MetadataController } from '~/metadata/metadata.controller'; -import { MetadataService } from '~/metadata/metadata.service'; -import { CreatedMetadataEntryModel } from '~/metadata/models/created-metadata-entry.model'; -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; -import { testValues } from '~/test-utils/consts'; -import { createMockMetadataEntry, createMockTransactionResult } from '~/test-utils/mocks'; -import { mockMetadataServiceProvider } from '~/test-utils/service-mocks'; - -describe('MetadataController', () => { - const { txResult } = testValues; - let controller: MetadataController; - let mockService: DeepMocked; - let ticker: string; - let type: MetadataType; - let id: BigNumber; - - beforeEach(async () => { - ticker = 'TICKER'; - type = MetadataType.Local; - id = new BigNumber(1); - - const module: TestingModule = await Test.createTestingModule({ - controllers: [MetadataController], - providers: [mockMetadataServiceProvider], - }).compile(); - - mockService = mockMetadataServiceProvider.useValue as DeepMocked; - - controller = module.get(MetadataController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getMetadata', () => { - it('should return the list of all metadata for a given ticker', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - when(mockService.findAll).calledWith(ticker).mockResolvedValue([mockMetadataEntry]); - - const result = await controller.getMetadata({ ticker }); - - expect(result).toEqual({ - results: [new MetadataEntryModel({ asset: ticker, type, id })], - }); - }); - }); - - describe('getSingleMetadata', () => { - it('should return the Metadata details for a specific type and ID', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockDetails = { - name: 'Some metadata', - specs: { - description: 'Some description', - }, - }; - - const mockValue = { - value: 'Some Value', - expiry: new Date('2099/01/01'), - lockStatus: MetadataLockStatus.LockedUntil, - lockedUntil: new Date('2030/01/01'), - }; - - mockMetadataEntry.details.mockResolvedValue(mockDetails); - mockMetadataEntry.value.mockResolvedValue(mockValue); - - when(mockService.findOne) - .calledWith({ ticker, type, id }) - .mockResolvedValue(mockMetadataEntry); - - const result = await controller.getSingleMetadata({ ticker, type, id }); - - expect(result).toEqual( - new MetadataDetailsModel({ - asset: ticker, - type, - id, - ...mockDetails, - value: new MetadataValueModel(mockValue), - }) - ); - }); - }); - - describe('createMetadata', () => { - it('should accept CreateMetadataDto and create a local Asset Metadata for the given ticker', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const mockMetadataEntry = createMockMetadataEntry(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - result: mockMetadataEntry, - }); - const mockPayload: CreateMetadataDto = { - name: 'Some Metadata', - specs: { - description: 'Some description', - }, - signer: 'Alice', - }; - - when(mockService.create).calledWith(ticker, mockPayload).mockResolvedValue(testTxResult); - - const result = await controller.createMetadata({ ticker }, mockPayload); - - expect(result).toEqual( - new CreatedMetadataEntryModel({ - ...txResult, - transactions: [transaction], - metadata: new MetadataEntryModel({ asset: ticker, type, id }), - }) - ); - }); - }); - - describe('setMetadata', () => { - it('should accept SetMetadataDto and set the value of the Asset Metadata', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: SetMetadataDto = { - value: 'some value', - signer: 'Alice', - }; - - when(mockService.setValue) - .calledWith({ ticker, type, id }, mockPayload) - .mockResolvedValue(testTxResult); - - const result = await controller.setMetadata({ ticker, type, id }, mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/metadata/metadata.controller.ts b/src/metadata/metadata.controller.ts deleted file mode 100644 index 2148304a..00000000 --- a/src/metadata/metadata.controller.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { MetadataEntry, MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { MetadataService } from '~/metadata/metadata.service'; -import { createMetadataDetailsModel } from '~/metadata/metadata.util'; -import { CreatedMetadataEntryModel } from '~/metadata/models/created-metadata-entry.model'; -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; - -@ApiTags('asset', 'metadata') -@Controller('assets/:ticker/metadata') -export class MetadataController { - constructor(private readonly metadataService: MetadataService) {} - - @ApiOperation({ - summary: "Fetch an Asset's Metadata", - description: 'This endpoint retrieves all the Metadata entries for a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose metadata are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(MetadataEntryModel, { - description: 'List of Metadata entries distinguished by id, type and ticker', - paginated: false, - }) - @Get() - public async getMetadata( - @Param() { ticker }: TickerParamsDto - ): Promise> { - const result = await this.metadataService.findAll(ticker); - - return new ResultsModel({ - results: result.map( - ({ asset: { ticker: asset }, id, type }) => new MetadataEntryModel({ asset, id, type }) - ), - }); - } - - @ApiOperation({ - summary: 'Fetch a specific Metadata entry for any Asset', - description: - 'This endpoint retrieves the details of an Asset Metadata entry by its type and ID', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose metadata is to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata to be filtered', - enum: MetadataType, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata to be filtered', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of an Asset Metadata including name, specs and value', - type: MetadataDetailsModel, - }) - @ApiNotFoundResponse({ - description: 'Asset Metadata does not exists', - }) - @Get(':type/:id') - public async getSingleMetadata( - @Param() params: MetadataParamsDto - ): Promise { - const metadataEntry = await this.metadataService.findOne(params); - - return createMetadataDetailsModel(metadataEntry); - } - - @ApiOperation({ - summary: 'Create a local metadata for an Asset and optionally set its value.', - description: - 'This endpoint creates a local metadata for the given Asset. The metadata value can be set by passing `value` parameter and specifying other optional `details` about the value', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the metadata is to be created', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'The newly created Metadata entry along with transaction details', - type: CreatedMetadataEntryModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found'], - [HttpStatus.BAD_REQUEST]: [ - 'Asset Metadata name length exceeded', - 'Asset Metadata value length exceeded', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Asset Metadata with the given name already exists', - 'Locked until date of the Metadata value must be in the future', - 'Expiry date for the Metadata value must be in the future', - ], - }) - @Post('create') - public async createMetadata( - @Param() { ticker }: TickerParamsDto, - @Body() params: CreateMetadataDto - ): Promise { - const serviceResult = await this.metadataService.create(ticker, params); - - const resolver: TransactionResolver = ({ - details, - transactions, - result: { - asset: { ticker: assetTicker }, - id, - type, - }, - }) => - new CreatedMetadataEntryModel({ - details, - transactions, - metadata: new MetadataEntryModel({ asset: assetTicker, id, type }), - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: "Set an Asset's Metadata value and details", - description: - 'This endpoint assigns a new value for the Metadata along with its expiry and lock status (when provided with `details`) of the Metadata value. Note that the value of a locked Metadata cannot be altered', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset for which the Metadata value is to be set', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'type', - description: 'The type of Asset Metadata', - enum: MetadataType, - example: MetadataType.Local, - }) - @ApiParam({ - name: 'id', - description: 'The ID of Asset Metadata', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Asset was not found', 'Asset Metadata does not exists'], - [HttpStatus.BAD_REQUEST]: ['Asset Metadata name length exceeded'], - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Details cannot be set for a locked Metadata value', - 'Metadata value is currently locked', - 'Details cannot be set for a metadata without a value', - ], - }) - @Post(':type/:id/set') - public async setMetadata( - @Param() params: MetadataParamsDto, - @Body() body: SetMetadataDto - ): Promise { - const serviceResult = await this.metadataService.setValue(params, body); - - return handleServiceResult(serviceResult); - } -} diff --git a/src/metadata/metadata.module.ts b/src/metadata/metadata.module.ts deleted file mode 100644 index 7197ffd2..00000000 --- a/src/metadata/metadata.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { MetadataController } from '~/metadata/metadata.controller'; -import { MetadataService } from '~/metadata/metadata.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => AssetsModule)], - controllers: [MetadataController], - providers: [MetadataService], - exports: [MetadataService], -}) -export class MetadataModule {} diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts deleted file mode 100644 index 567655db..00000000 --- a/src/metadata/metadata.service.spec.ts +++ /dev/null @@ -1,228 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataLockStatus, MetadataType, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AssetsService } from '~/assets/assets.service'; -import { MetadataService } from '~/metadata/metadata.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockMetadataEntry, - MockAsset, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAssetService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('MetadataService', () => { - let service: MetadataService; - let mockAssetsService: MockAssetService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - let ticker: string; - let type: MetadataType; - let id: BigNumber; - let signer: string; - - beforeEach(async () => { - ticker = 'TICKER'; - type = MetadataType.Local; - id = new BigNumber(1); - signer = testValues.signer; - mockPolymeshApi = new MockPolymesh(); - - mockTransactionsService = mockTransactionsProvider.useValue; - mockAssetsService = new MockAssetService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [MetadataService, AssetsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(MetadataService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findGlobalKeys', () => { - it('should return a list of global metadata keys', async () => { - const globalKeys = [ - { - name: 'Global Metadata', - id: new BigNumber(1), - specs: { description: 'Some description' }, - }, - ]; - - mockPolymeshApi.assets.getGlobalMetadataKeys.mockResolvedValue(globalKeys); - - const result = await service.findGlobalKeys(); - - expect(result).toEqual(globalKeys); - }); - }); - - describe('findAll', () => { - it('should return all Metadata entries for a given ticker', async () => { - const metadata = [createMockMetadataEntry()]; - const mockAsset = new MockAsset(); - mockAsset.metadata.get.mockResolvedValue(metadata); - - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - - const result = await service.findAll(ticker); - - expect(result).toEqual(metadata); - }); - }); - - describe('findOne', () => { - let mockAsset: MockAsset; - - beforeEach(() => { - mockAsset = new MockAsset(); - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - }); - - it('should return the Metadata entry', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - mockAsset.metadata.getOne.mockResolvedValue(mockMetadataEntry); - - const result = await service.findOne({ ticker, type, id }); - - expect(result).toEqual(mockMetadataEntry); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockAsset.metadata.getOne.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(service.findOne({ ticker, type, id })).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('create', () => { - it('should run a register procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterAssetMetadataLocalType, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const mockAsset = new MockAsset(); - when(mockAssetsService.findOne).calledWith(ticker).mockResolvedValue(mockAsset); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - - const body = { - signer, - name: 'Some Metadata', - specs: { - description: 'Some description', - }, - }; - - const result = await service.create(ticker, body); - expect(result).toEqual({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockAsset.metadata.register, - { name: body.name, specs: body.specs }, - { signer } - ); - }); - }); - - describe('setValue', () => { - it('should run a set procedure and return the queue results', async () => { - const mockMetadataEntry = createMockMetadataEntry(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.SetAssetMetadata, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy) - .calledWith({ ticker, type, id }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(mockMetadataEntry as any); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - - const body = { - signer, - value: 'some value', - details: { - expiry: new Date('2099/01/01'), - lockStatus: MetadataLockStatus.Locked, - }, - }; - - const result = await service.setValue({ ticker, type, id }, body); - expect(result).toEqual({ - result: mockMetadataEntry, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockMetadataEntry.set, - { value: body.value, details: body.details }, - { signer } - ); - }); - }); -}); diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts deleted file mode 100644 index a82ea544..00000000 --- a/src/metadata/metadata.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - GlobalMetadataKey, - MetadataEntry, - SetMetadataParams, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { ServiceReturn } from '~/common/utils'; -import { CreateMetadataDto } from '~/metadata/dto/create-metadata.dto'; -import { MetadataParamsDto } from '~/metadata/dto/metadata-params.dto'; -import { SetMetadataDto } from '~/metadata/dto/set-metadata.dto'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class MetadataService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly assetsService: AssetsService - ) {} - - public async findGlobalKeys(): Promise { - return this.polymeshService.polymeshApi.assets.getGlobalMetadataKeys(); - } - - public async findAll(ticker: string): Promise { - const { metadata } = await this.assetsService.findOne(ticker); - - return metadata.get(); - } - - public async findOne({ ticker, type, id }: MetadataParamsDto): Promise { - const { metadata } = await this.assetsService.findOne(ticker); - - return await metadata.getOne({ type, id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async create(ticker: string, params: CreateMetadataDto): ServiceReturn { - const { signer, webhookUrl, ...rest } = params; - - const { - metadata: { register }, - } = await this.assetsService.findOne(ticker); - - return this.transactionsService.submit(register, rest, { - signer, - webhookUrl, - }); - } - - public async setValue( - params: MetadataParamsDto, - body: SetMetadataDto - ): ServiceReturn { - const { signer, webhookUrl, ...rest } = body; - - const { set } = await this.findOne(params); - - return this.transactionsService.submit(set, rest as SetMetadataParams, { - signer, - webhookUrl, - }); - } -} diff --git a/src/metadata/metadata.util.ts b/src/metadata/metadata.util.ts deleted file mode 100644 index 53d6ab6e..00000000 --- a/src/metadata/metadata.util.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MetadataEntry, MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { MetadataDetailsModel } from '~/metadata/models/metadata-details.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; - -export async function createMetadataDetailsModel( - metadataEntry: MetadataEntry -): Promise { - const { - id, - type, - asset: { ticker }, - } = metadataEntry; - - const [{ name, specs }, valueDetails] = await Promise.all([ - metadataEntry.details(), - metadataEntry.value(), - ]); - - let metadataValue = null; - if (valueDetails) { - const { expiry, lockStatus, value } = valueDetails; - - let lockedUntil; - if (lockStatus === MetadataLockStatus.LockedUntil) { - lockedUntil = valueDetails.lockedUntil; - } - - metadataValue = new MetadataValueModel({ value, expiry, lockStatus, lockedUntil }); - } - - return new MetadataDetailsModel({ id, type, asset: ticker, name, specs, value: metadataValue }); -} diff --git a/src/metadata/models/created-metadata-entry.model.ts b/src/metadata/models/created-metadata-entry.model.ts deleted file mode 100644 index fbf296fd..00000000 --- a/src/metadata/models/created-metadata-entry.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; - -export class CreatedMetadataEntryModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Asset Metadata', - type: () => MetadataEntryModel, - }) - @Type(() => MetadataEntryModel) - readonly metadata: MetadataEntryModel; - - constructor(model: CreatedMetadataEntryModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/metadata/models/global-metadata.model.ts b/src/metadata/models/global-metadata.model.ts deleted file mode 100644 index dff98f68..00000000 --- a/src/metadata/models/global-metadata.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; - -export class GlobalMetadataModel { - @ApiProperty({ - description: 'ID of the Global Asset Metadata', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'Name of the Global Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly name: string; - - @ApiProperty({ - description: 'Specs describing the Asset Metadata', - type: MetadataSpecModel, - }) - @Type(() => MetadataSpecModel) - readonly specs: MetadataSpecModel; - - constructor(model: GlobalMetadataModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-details.model.ts b/src/metadata/models/metadata-details.model.ts deleted file mode 100644 index b844a94b..00000000 --- a/src/metadata/models/metadata-details.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { MetadataEntryModel } from '~/metadata/models/metadata-entry.model'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; -import { MetadataValueModel } from '~/metadata/models/metadata-value.model'; - -export class MetadataDetailsModel extends MetadataEntryModel { - @ApiProperty({ - description: 'Name of the Global Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly name: string; - - @ApiProperty({ - description: 'Specs describing the Asset Metadata', - type: MetadataSpecModel, - }) - @Type(() => MetadataSpecModel) - readonly specs: MetadataSpecModel; - - @ApiProperty({ - description: - 'Asset Metadata value and its details (expiry + lock status). `null` means that value is not yet set', - type: MetadataValueModel, - nullable: true, - }) - @Type(() => MetadataValueModel) - readonly value: MetadataValueModel | null; - - constructor(model: MetadataDetailsModel) { - const { id, type, asset, ...rest } = model; - super({ id, type, asset }); - - Object.assign(this, rest); - } -} diff --git a/src/metadata/models/metadata-entry.model.ts b/src/metadata/models/metadata-entry.model.ts deleted file mode 100644 index c10ebd9b..00000000 --- a/src/metadata/models/metadata-entry.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class MetadataEntryModel { - @ApiProperty({ - description: 'The ticker of the Asset for which this is the Metadata for', - type: 'string', - example: 'TICKER', - }) - readonly asset: string; - - @ApiProperty({ - description: 'The type of metadata represented by this instance', - type: 'string', - enum: MetadataType, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'ID corresponding to defined `type` of Metadata', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: MetadataEntryModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-spec.model.ts b/src/metadata/models/metadata-spec.model.ts deleted file mode 100644 index 62dfff8f..00000000 --- a/src/metadata/models/metadata-spec.model.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class MetadataSpecModel { - @ApiPropertyOptional({ - description: 'Off-chain specs or documentation link', - type: 'string', - example: 'https://example.com', - }) - readonly url?: string; - - @ApiPropertyOptional({ - description: 'Description of the Metadata type', - type: 'string', - example: 'Some description', - }) - readonly description?: string; - - @ApiPropertyOptional({ - description: - '[SCALE](https://wiki.polkadot.network/docs/build-tools-index#scale-codec) encoded `AssetMetadataTypeDef`', - type: 'string', - example: 'https://example.com', - }) - readonly typedef?: string; - - constructor(model: MetadataSpecModel) { - Object.assign(this, model); - } -} diff --git a/src/metadata/models/metadata-value.model.ts b/src/metadata/models/metadata-value.model.ts deleted file mode 100644 index 6486a948..00000000 --- a/src/metadata/models/metadata-value.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { MetadataLockStatus } from '@polymeshassociation/polymesh-sdk/types'; - -export class MetadataValueModel { - @ApiProperty({ - description: 'Value of the Asset Metadata', - type: 'string', - example: 'Some metadata', - }) - readonly value: string; - - @ApiProperty({ - description: 'Date at which the Metadata value expires, null if it never expires', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - }) - readonly expiry: Date | null; - - @ApiProperty({ - description: 'Lock status of Metadata value', - enum: MetadataLockStatus, - example: MetadataLockStatus.LockedUntil, - }) - readonly lockStatus: MetadataLockStatus; - - @ApiPropertyOptional({ - description: - 'Date till which the Metadata value will be locked. This only applies when `status` is `LockedUntil`', - type: 'string', - example: new Date('05/23/2021').toISOString(), - }) - readonly lockedUntil?: Date; - - constructor(model: MetadataValueModel) { - Object.assign(this, model); - } -} diff --git a/src/network/mocks/network-properties.mock.ts b/src/network/mocks/network-properties.mock.ts deleted file mode 100644 index ff0a7a5a..00000000 --- a/src/network/mocks/network-properties.mock.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -export class MockNetworkProperties { - name = 'Development'; - - version = new BigNumber(1); -} diff --git a/src/network/models/network-block.model.ts b/src/network/models/network-block.model.ts deleted file mode 100644 index ee81215f..00000000 --- a/src/network/models/network-block.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NetworkBlockModel { - @ApiProperty({ - description: 'Latest Block Id', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: NetworkBlockModel) { - Object.assign(this, model); - } -} diff --git a/src/network/models/network-properties.model.ts b/src/network/models/network-properties.model.ts deleted file mode 100644 index e05a56fa..00000000 --- a/src/network/models/network-properties.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NetworkPropertiesModel { - @ApiProperty({ - description: 'Network name', - type: 'string', - example: 'Development', - }) - readonly name: string; - - @ApiProperty({ - description: 'Network version number', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly version: BigNumber; - - constructor(model: NetworkPropertiesModel) { - Object.assign(this, model); - } -} diff --git a/src/network/network.controller.spec.ts b/src/network/network.controller.spec.ts deleted file mode 100644 index d261c690..00000000 --- a/src/network/network.controller.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockNetworkProperties } from '~/network/mocks/network-properties.mock'; -import { NetworkBlockModel } from '~/network/models/network-block.model'; -import { NetworkController } from '~/network/network.controller'; -import { NetworkService } from '~/network/network.service'; -import { mockNetworkServiceProvider } from '~/test-utils/service-mocks'; - -describe('NetworkController', () => { - let controller: NetworkController; - let mockNetworkService: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [NetworkController], - providers: [mockNetworkServiceProvider], - }).compile(); - - mockNetworkService = mockNetworkServiceProvider.useValue as DeepMocked; - controller = module.get(NetworkController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getNetworkProperties', () => { - it('should return network properties', async () => { - const mockResult = new MockNetworkProperties(); - - mockNetworkService.getNetworkProperties.mockResolvedValue(mockResult); - - const result = await controller.getNetworkProperties(); - - expect(result).toEqual(mockResult); - }); - }); - - describe('getLatestBlock', () => { - it('should return latest block number as NetworkBlockModel', async () => { - const mockLatestBlock = new BigNumber(1); - const mockResult = new NetworkBlockModel({ id: mockLatestBlock }); - mockNetworkService.getLatestBlock.mockResolvedValue(mockLatestBlock); - - const result = await controller.getLatestBlock(); - - expect(result).toEqual(mockResult); - }); - }); -}); diff --git a/src/network/network.controller.ts b/src/network/network.controller.ts deleted file mode 100644 index 9fc45cb6..00000000 --- a/src/network/network.controller.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { NetworkBlockModel } from '~/network/models/network-block.model'; -import { NetworkPropertiesModel } from '~/network/models/network-properties.model'; -import { NetworkService } from '~/network/network.service'; - -@ApiTags('network') -@Controller('network') -export class NetworkController { - constructor(private readonly networkService: NetworkService) {} - - @ApiOperation({ - summary: 'Fetch network properties', - description: 'This endpoint will provide the network name and version', - }) - @ApiOkResponse({ - description: 'Network properties response', - type: NetworkPropertiesModel, - }) - @Get() - public async getNetworkProperties(): Promise { - const networkProperties = await this.networkService.getNetworkProperties(); - - return new NetworkPropertiesModel(networkProperties); - } - - @ApiOperation({ - summary: 'Get the latest block', - description: 'This endpoint will provide the latest block number', - }) - @ApiOkResponse({ - description: 'Latest block number that has been added to the chain', - type: NetworkBlockModel, - }) - @Get('latest-block') - public async getLatestBlock(): Promise { - const latestBlock = await this.networkService.getLatestBlock(); - - return new NetworkBlockModel({ id: latestBlock }); - } -} diff --git a/src/network/network.module.ts b/src/network/network.module.ts deleted file mode 100644 index 6033db08..00000000 --- a/src/network/network.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { NetworkController } from '~/network/network.controller'; -import { NetworkService } from '~/network/network.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; - -/** - * NetworkModule responsible for the exposing the SDK network namespace - */ -@Module({ - imports: [PolymeshModule], - providers: [NetworkService], - exports: [NetworkService], - controllers: [NetworkController], -}) -export class NetworkModule {} diff --git a/src/network/network.service.spec.ts b/src/network/network.service.spec.ts deleted file mode 100644 index a8b3312a..00000000 --- a/src/network/network.service.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable import/first */ -const mockHexStripPrefix = jest.fn().mockImplementation(params => params); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { MockNetworkProperties } from '~/network/mocks/network-properties.mock'; -import { NetworkService } from '~/network/network.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { extrinsicWithFees, testValues } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; - -jest.mock('@polkadot/util', () => ({ - ...jest.requireActual('@polkadot/util'), - hexStripPrefix: mockHexStripPrefix, -})); - -describe('NetworkService', () => { - let networkService: NetworkService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - - const { testAccount } = testValues; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [NetworkService], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - networkService = module.get(NetworkService); - polymeshService = module.get(PolymeshService); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(networkService).toBeDefined(); - }); - - describe('getNetworkProperties', () => { - it('should return network properties', async () => { - const networkProperties = new MockNetworkProperties(); - - mockPolymeshApi.network.getNetworkProperties.mockReturnValue(networkProperties); - - const result = await networkService.getNetworkProperties(); - - expect(result).toBe(networkProperties); - }); - }); - - describe('getLatestBlock', () => { - it('should latest block ID', async () => { - const mockResult = new BigNumber(1); - - mockPolymeshApi.network.getLatestBlock.mockReturnValue(mockResult); - - const result = await networkService.getLatestBlock(); - - expect(result).toBe(mockResult); - }); - }); - - describe('getTreasuryAccount', () => { - it("should return the chain's treasury Account", async () => { - mockPolymeshApi.network.getTreasuryAccount.mockReturnValue(testAccount); - - const result = networkService.getTreasuryAccount(); - - expect(result).toBe(testAccount); - }); - }); - - describe('getTransactionByHash', () => { - it('should return the extrinsic details', async () => { - mockPolymeshApi.network.getTransactionByHash.mockReturnValue(extrinsicWithFees); - - const result = await networkService.getTransactionByHash('someHash'); - - expect(result).toEqual(extrinsicWithFees); - }); - }); -}); diff --git a/src/network/network.service.ts b/src/network/network.service.ts deleted file mode 100644 index 6d7dbc55..00000000 --- a/src/network/network.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { hexStripPrefix } from '@polkadot/util'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Account, ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-sdk/types'; - -import { NetworkPropertiesModel } from '~/network/models/network-properties.model'; -import { PolymeshService } from '~/polymesh/polymesh.service'; - -@Injectable() -export class NetworkService { - constructor(private readonly polymeshService: PolymeshService) {} - - public async getNetworkProperties(): Promise { - return this.polymeshService.polymeshApi.network.getNetworkProperties(); - } - - public async getLatestBlock(): Promise { - return this.polymeshService.polymeshApi.network.getLatestBlock(); - } - - public getTreasuryAccount(): Account { - return this.polymeshService.polymeshApi.network.getTreasuryAccount(); - } - - public getTransactionByHash(hash: string): Promise { - return this.polymeshService.polymeshApi.network.getTransactionByHash({ - txHash: hexStripPrefix(hash), - }); - } -} diff --git a/src/nfts/dto/collection-key.dto.ts b/src/nfts/dto/collection-key.dto.ts deleted file mode 100644 index 6820bbdb..00000000 --- a/src/nfts/dto/collection-key.dto.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsEnum, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { MetadataSpecDto } from '~/metadata/dto/metadata-spec.dto'; - -export class CollectionKeyDto { - @ApiProperty({ - description: - 'Whether the metadata key is local or global. Local values will be created with the collection, while global values must already exist', - example: MetadataType.Local, - }) - @IsEnum(MetadataType) - readonly type: MetadataType; - - @ApiPropertyOptional({ - description: 'The ID of the global metadata. Required when type is Global', - type: 'string', - example: '1', - }) - @ValidateIf(({ type }) => type === MetadataType.Global) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - @ApiPropertyOptional({ - description: 'The specification for a local metadata value. Required when type is Local', - type: MetadataSpecDto, - }) - @ValidateIf(({ type }) => type === MetadataType.Local) - @Type(() => MetadataSpecDto) - readonly spec: MetadataSpecDto; - - @ApiPropertyOptional({ - description: 'The name of the local metadata value. Required when type is Local', - example: 'Info', - type: 'string', - }) - @ValidateIf(({ type }) => type === MetadataType.Local) - @IsString() - readonly name?: string; -} diff --git a/src/nfts/dto/create-nft-collection.dto.ts b/src/nfts/dto/create-nft-collection.dto.ts deleted file mode 100644 index 7d06f52f..00000000 --- a/src/nfts/dto/create-nft-collection.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { KnownNftType } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; -import { IsEnum, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { AssetDocumentDto } from '~/assets/dto/asset-document.dto'; -import { SecurityIdentifierDto } from '~/assets/dto/security-identifier.dto'; -import { IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { CollectionKeyDto } from '~/nfts/dto/collection-key.dto'; - -export class CreateNftCollectionDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Nft Collection', - example: 'Ticker Collection', - }) - @IsString() - readonly name: string; - - @ApiProperty({ - description: - 'The ticker of the NFT Collection. This must either be free or reserved by the Signer', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiProperty({ - description: 'The type of Asset', - example: KnownNftType.Derivative, - }) - @IsEnum(KnownNftType) - readonly nftType: KnownNftType; - - @ApiPropertyOptional({ - description: "List of the NFT Collection's Security Identifiers", - isArray: true, - type: SecurityIdentifierDto, - }) - @ValidateNested({ each: true }) - @Type(() => SecurityIdentifierDto) - readonly securityIdentifiers?: SecurityIdentifierDto[]; - - @ApiPropertyOptional({ - description: 'Documents related to the NFT Collection', - isArray: true, - type: AssetDocumentDto, - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => AssetDocumentDto) - readonly documents?: AssetDocumentDto[]; - - @ApiProperty({ - description: - 'The metadata keys that define the collection. Every token issued for the collection must have a value for each key specified', - isArray: true, - type: CollectionKeyDto, - }) - @ValidateNested({ each: true }) - @Type(() => CollectionKeyDto) - readonly collectionKeys: CollectionKeyDto[]; -} diff --git a/src/nfts/dto/issue-nft.dto.ts b/src/nfts/dto/issue-nft.dto.ts deleted file mode 100644 index 361729f7..00000000 --- a/src/nfts/dto/issue-nft.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { MetadataValueDto } from '~/nfts/dto/metadata-value.dto'; - -export class IssueNftDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The metadata values for the token', - type: MetadataValueDto, - isArray: true, - }) - @Type(() => MetadataValueDto) - @ValidateNested({ each: true }) - readonly metadata: MetadataValueDto[]; -} diff --git a/src/nfts/dto/metadata-value.dto.ts b/src/nfts/dto/metadata-value.dto.ts deleted file mode 100644 index 4510375e..00000000 --- a/src/nfts/dto/metadata-value.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; - -export class MetadataValueDto { - @ApiProperty({ - description: 'Whether the value if for a local or global metadata entry', - enum: MetadataType, - example: MetadataType.Local, - }) - @IsEnum(MetadataType) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry the value is for', - type: 'string', - example: '1', - }) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The value for the metadata entry', - example: 'https://example.com/nfts/1', - }) - @IsString() - readonly value: string; -} diff --git a/src/nfts/dto/nft-params.dto.ts b/src/nfts/dto/nft-params.dto.ts deleted file mode 100644 index 8ae0f076..00000000 --- a/src/nfts/dto/nft-params.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; - -export class NftParamsDto { - @IsTicker() - readonly ticker: string; - - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; -} diff --git a/src/nfts/dto/redeem-nft.dto.ts b/src/nfts/dto/redeem-nft.dto.ts deleted file mode 100644 index 867f474e..00000000 --- a/src/nfts/dto/redeem-nft.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class RedeemNftDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'The portfolio number from which the Nft must be redeemed from. Use 0 for the default portfolio', - example: '1', - type: 'string', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; -} diff --git a/src/nfts/models/collection-key.model.ts b/src/nfts/models/collection-key.model.ts deleted file mode 100644 index b1544c33..00000000 --- a/src/nfts/models/collection-key.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { MetadataSpecModel } from '~/metadata/models/metadata-spec.model'; - -export class CollectionKeyModel { - @ApiProperty({ - description: 'Whether the metadata entry is Global or Local', - example: MetadataType.Local, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The name of the metadata entry', - example: 'Token info', - }) - readonly name: string; - - @ApiPropertyOptional({ - description: 'The specifications of the metadata entry', - }) - readonly specs: MetadataSpecModel; - - constructor(args: CollectionKeyModel) { - Object.assign(this, args); - } -} diff --git a/src/nfts/models/nft-metadata-key.model.ts b/src/nfts/models/nft-metadata-key.model.ts deleted file mode 100644 index 4de50173..00000000 --- a/src/nfts/models/nft-metadata-key.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { MetadataType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class NftMetadataKeyModel { - @ApiProperty({ - description: 'Whether the metadata is Local or Global', - type: 'string', - example: MetadataType.Local, - }) - readonly type: MetadataType; - - @ApiProperty({ - description: 'The ID of the metadata entry', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - constructor(model: NftMetadataKeyModel) { - Object.assign(this, model); - } -} diff --git a/src/nfts/models/nft-metadata.model.ts b/src/nfts/models/nft-metadata.model.ts deleted file mode 100644 index c0568a68..00000000 --- a/src/nfts/models/nft-metadata.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { NftMetadataKeyModel } from '~/nfts/models/nft-metadata-key.model'; - -export class NftMetadataModel { - @ApiProperty({ - description: 'Value of the tokens metadata', - type: 'string', - example: 'https://example.com', - }) - readonly value: string; - - @ApiProperty({ - description: 'The metadata entry ID', - type: NftMetadataKeyModel, - }) - key: NftMetadataKeyModel; - - constructor(model: NftMetadataModel) { - const key = new NftMetadataKeyModel(model.key); - Object.assign(this, { ...model, key }); - } -} diff --git a/src/nfts/models/nft.model.ts b/src/nfts/models/nft.model.ts deleted file mode 100644 index f4664238..00000000 --- a/src/nfts/models/nft.model.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { NftMetadataModel } from '~/nfts/models/nft-metadata.model'; - -export class NftModel { - @ApiProperty({ - type: 'string', - description: 'The NFT ID', - example: '1', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The collection ticker of which the NFT belongs to', - }) - readonly ticker: string; - - @ApiProperty({ - description: 'The metadata associated to the NFT', - }) - readonly metadata: NftMetadataModel[]; - - @ApiProperty({ - description: - 'The conventional NFT URI based on global metadata. Will be set if the token has a value for `imageUri` or the collection has a value for `baseImageUri`', - }) - readonly imageUri: string | null; - - @ApiProperty({ - description: - 'The conventional NFT URI based on global metadata. Will be set if the token has a value for `tokenUri` or the collection has a value for `baseTokenUri`', - }) - readonly tokenUri: string | null; - - constructor(model: NftModel) { - const metadata = model.metadata.map(value => new NftMetadataModel(value)); - Object.assign(this, { ...model, metadata }); - } -} diff --git a/src/nfts/nfts.controller.spec.ts b/src/nfts/nfts.controller.spec.ts deleted file mode 100644 index 22cf3714..00000000 --- a/src/nfts/nfts.controller.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { KnownNftType, Nft, NftCollection } from '@polymeshassociation/polymesh-sdk/types'; - -import { ServiceReturn } from '~/common/utils'; -import { NftsController } from '~/nfts//nfts.controller'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { NftsService } from '~/nfts/nfts.service'; -import { testValues } from '~/test-utils/consts'; -import { mockNftsServiceProvider } from '~/test-utils/service-mocks'; - -const { signer, ticker, txResult } = testValues; - -describe('NftController', () => { - let controller: NftsController; - let mockNftsService: DeepMocked; - const id = new BigNumber(1); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [NftsController], - providers: [mockNftsServiceProvider], - }).compile(); - - mockNftsService = module.get(NftsService); - controller = module.get(NftsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getCollectionKeys', () => { - it('should call the service and return the result', async () => { - const fakeResult = ['fakeResult'] as unknown as CollectionKeyModel[]; - - mockNftsService.getCollectionKeys.mockResolvedValue(fakeResult); - - const result = await controller.getCollectionKeys({ ticker }); - - expect(result).toEqual(fakeResult); - }); - }); - - describe('getNftInfo', () => { - it('should call the service and return the result', async () => { - const fakeResult = 'fakeNftModel' as unknown as NftModel; - - mockNftsService.nftDetails.mockResolvedValue(fakeResult); - - const result = await controller.getNftDetails({ ticker, id }); - - expect(result).toEqual(fakeResult); - }); - }); - - describe('createNftCollection', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - name: 'Ticker Collection', - ticker, - nftType: KnownNftType.Derivative, - collectionKeys: [], - }; - mockNftsService.createNftCollection.mockResolvedValue( - txResult as unknown as ServiceReturn - ); - - const result = await controller.createNftCollection(input); - expect(result).toEqual(txResult); - }); - }); - - describe('issueNft', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - metadata: [], - }; - const fakeResult = txResult as unknown as ServiceReturn; - mockNftsService.issueNft.mockResolvedValue(fakeResult); - - const result = await controller.issueNft({ ticker }, input); - expect(result).toEqual(fakeResult); - }); - }); - - describe('redeemNft', () => { - it('should call the service and return the results', async () => { - const input = { - signer, - from: new BigNumber(0), - }; - const fakeResult = txResult as unknown as ServiceReturn; - mockNftsService.redeemNft.mockResolvedValue(fakeResult); - - const result = await controller.redeem({ ticker, id }, input); - expect(result).toEqual(fakeResult); - }); - }); -}); diff --git a/src/nfts/nfts.controller.ts b/src/nfts/nfts.controller.ts deleted file mode 100644 index fec9db63..00000000 --- a/src/nfts/nfts.controller.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiGoneResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { CreateNftCollectionDto } from '~/nfts/dto/create-nft-collection.dto'; -import { IssueNftDto } from '~/nfts/dto/issue-nft.dto'; -import { NftParamsDto } from '~/nfts/dto/nft-params.dto'; -import { RedeemNftDto } from '~/nfts/dto/redeem-nft.dto'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { NftsService } from '~/nfts/nfts.service'; - -@ApiTags('nfts') -@Controller('nfts') -export class NftsController { - constructor(private readonly nftService: NftsService) {} - - @ApiOperation({ - summary: 'Fetch the required metadata keys for an NFT Collection', - description: 'This endpoint will provide the NFT collection keys for an NFT Collection', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection whose collection keys are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiArrayResponse(CollectionKeyModel, { - description: 'List of required metadata values for each NFT in the collection', - paginated: true, - }) - @Get(':ticker/collection-keys') - public async getCollectionKeys( - @Param() { ticker }: TickerParamsDto - ): Promise { - return this.nftService.getCollectionKeys(ticker); - } - - @ApiOperation({ - summary: 'Fetch the details of an NFT', - description: 'This endpoint will return the metadata details of an NFT', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the NFT', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - type: NftModel, - description: 'List of required metadata values for each NFT in the collection', - }) - @Get(':ticker/:id') - public async getNftDetails(@Param() { ticker, id }: NftParamsDto): Promise { - return this.nftService.nftDetails(ticker, id); - } - - @ApiOperation({ - summary: 'Create an NFT collection', - description: 'This endpoint allows for the creation of NFT collections', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiGoneResponse({ - description: 'The ticker has already been used to create an asset', - }) - @Post('/create') - public async createNftCollection( - @Body() params: CreateNftCollectionDto - ): Promise { - const result = await this.nftService.createNftCollection(params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Issue an NFT for a collection', - description: 'This endpoint allows for the issuance of NFTs', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection to issue an NFT for', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post(':ticker/issue') - public async issueNft( - @Param() { ticker }: TickerParamsDto, - @Body() params: IssueNftDto - ): Promise { - const result = await this.nftService.issueNft(ticker, params); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Redeem an NFT, this removes it from circulation', - description: 'This endpoint allows for the redemption (aka burning) of NFTs', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the NFT Collection to redeem an NFT from', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the NFT', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @Post(':ticker/:id/redeem') - public async redeem( - @Param() { ticker, id }: NftParamsDto, - @Body() params: RedeemNftDto - ): Promise { - const result = await this.nftService.redeemNft(ticker, id, params); - - return handleServiceResult(result); - } -} diff --git a/src/nfts/nfts.module.ts b/src/nfts/nfts.module.ts deleted file mode 100644 index b2d21b55..00000000 --- a/src/nfts/nfts.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { NftsController } from '~/nfts/nfts.controller'; -import { NftsService } from '~/nfts/nfts.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule], - controllers: [NftsController], - providers: [NftsService], -}) -export class NftsModule {} diff --git a/src/nfts/nfts.service.spec.ts b/src/nfts/nfts.service.spec.ts deleted file mode 100644 index 2277ebe1..00000000 --- a/src/nfts/nfts.service.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - KnownNftType, - MetadataType, - Nft, - NftCollection, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { NftsService } from '~/nfts/nfts.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { MockPolymesh, MockTransaction } from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import { TransactionsService } from '~/transactions/transactions.service'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { ticker, signer } = testValues; - -describe('NftService', () => { - let service: NftsService; - let mockPolymeshApi: MockPolymesh; - let polymeshService: PolymeshService; - let mockTransactionsService: MockTransactionsService; - const id = new BigNumber(1); - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [NftsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - mockPolymeshApi = module.get(POLYMESH_API); - polymeshService = module.get(PolymeshService); - mockTransactionsService = module.get(TransactionsService); - service = module.get(NftsService); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findCollection', () => { - it('should return the collection for a valid ticker', async () => { - const collection = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - - const result = await service.findCollection(ticker); - - expect(result).toEqual(collection); - }); - }); - - describe('findCollection', () => { - it('should return the collection for a valid ticker', async () => { - const collection = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - - const result = await service.findCollection(ticker); - - expect(result).toEqual(collection); - }); - - it('should call handleSdkError and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.assets.getNftCollection.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - const address = 'address'; - - await expect(() => service.findCollection(address)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - describe('findNft', () => { - it('should return the NFT for a valid ticker and id', async () => { - const collection = createMock(); - const nft = createMock(); - mockPolymeshApi.assets.getNftCollection.mockResolvedValue(collection); - collection.getNft.mockResolvedValue(nft); - - const result = await service.findNft(ticker, id); - - expect(result).toEqual(nft); - }); - - it('should call handleSdkError and throw an error', async () => { - const collection = createMock(); - const nft = createMock(); - const findCollectionSpy = jest.spyOn(service, 'findCollection'); - findCollectionSpy.mockResolvedValue(collection); - - const mockError = new Error('Some Error'); - collection.getNft.mockRejectedValue(nft); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findNft(ticker, id)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - - describe('nftDetails', () => { - it('should return the details', async () => { - const nft = createMock({ id }); - const imageUri = 'https://example.com/nfts/1/image'; - const tokenUri = null; - nft.getMetadata.mockResolvedValue([]); - nft.getImageUri.mockResolvedValue(imageUri); - nft.getTokenUri.mockResolvedValue(tokenUri); - - const findNftSpy = jest.spyOn(service, 'findNft'); - findNftSpy.mockResolvedValue(nft); - - const result = await service.nftDetails(ticker, id); - expect(result).toEqual({ - id, - ticker, - imageUri, - tokenUri, - metadata: [], - }); - }); - }); - - describe('getCollectionKeys', () => { - it('should return the collection keys', async () => { - const collection = createMock(); - const findCollectionSpy = jest.spyOn(service, 'findCollection'); - findCollectionSpy.mockResolvedValue(collection); - - const mockMetadata = [ - { - type: MetadataType.Local, - id: new BigNumber(1), - value: 'someValue', - name: 'some name', - specs: {}, - ticker, - }, - ]; - - collection.collectionKeys.mockResolvedValue(mockMetadata); - - const result = await service.getCollectionKeys(ticker); - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: new BigNumber(1), type: MetadataType.Local }), - ]) - ); - }); - }); - - describe('createNftCollection', () => { - it('should create the collection', async () => { - const input = { - ticker, - name: 'Collection Name', - nftType: KnownNftType.Derivative, - collectionKeys: [], - signer, - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.CreateNftCollection, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockCollection, - transactions: [mockTransaction], - }); - - const result = await service.createNftCollection(input); - - expect(result).toEqual({ - result: mockCollection, - transactions: [mockTransaction], - }); - }); - }); - - describe('issueNft', () => { - it('should issue an NFT', async () => { - const input = { - signer, - metadata: [], - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.IssueNft, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - const mockNft = createMock(); - - jest.spyOn(service, 'findCollection').mockResolvedValue(mockCollection); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockNft, - transactions: [mockTransaction], - }); - - const result = await service.issueNft(ticker, input); - - expect(result).toEqual({ - result: mockNft, - transactions: [mockTransaction], - }); - }); - }); - - describe('redeemNft', () => { - it('should redeem an NFT', async () => { - const input = { - signer, - from: new BigNumber(1), - }; - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.nft.RedeemNft, - }; - const mockTransaction = new MockTransaction(mockTransactions); - const mockCollection = createMock(); - - jest.spyOn(service, 'findCollection').mockResolvedValue(mockCollection); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.redeemNft(ticker, id, input); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); -}); diff --git a/src/nfts/nfts.service.ts b/src/nfts/nfts.service.ts deleted file mode 100644 index 70bd7d45..00000000 --- a/src/nfts/nfts.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - CreateNftCollectionParams, - Nft, - NftCollection, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { CreateNftCollectionDto } from '~/nfts/dto/create-nft-collection.dto'; -import { IssueNftDto } from '~/nfts/dto/issue-nft.dto'; -import { RedeemNftDto } from '~/nfts/dto/redeem-nft.dto'; -import { CollectionKeyModel } from '~/nfts/models/collection-key.model'; -import { NftModel } from '~/nfts/models/nft.model'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class NftsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findCollection(ticker: string): Promise { - return this.polymeshService.polymeshApi.assets.getNftCollection({ ticker }).catch(error => { - throw handleSdkError(error); - }); - } - - public async findNft(ticker: string, id: BigNumber): Promise { - const collection = await this.findCollection(ticker); - - return collection.getNft({ id }).catch(error => { - throw handleSdkError(error); - }); - } - - public async nftDetails(ticker: string, id: BigNumber): Promise { - const nft = await this.findNft(ticker, id); - const [metadata, imageUri, tokenUri] = await Promise.all([ - nft.getMetadata(), - nft.getImageUri(), - nft.getTokenUri(), - ]); - - return new NftModel({ - id: nft.id, - ticker, - metadata, - imageUri, - tokenUri, - }); - } - - public async getCollectionKeys(ticker: string): Promise { - const collection = await this.findCollection(ticker); - - const keys = await collection.collectionKeys(); - - return keys.map(key => new CollectionKeyModel(key)); - } - - public async createNftCollection(params: CreateNftCollectionDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const createCollection = this.polymeshService.polymeshApi.assets.createNftCollection; - return this.transactionsService.submit( - createCollection, - args as CreateNftCollectionParams, - base - ); - } - - public async issueNft(ticker: string, params: IssueNftDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { issue } = await this.findCollection(ticker); - - return this.transactionsService.submit(issue, args, base); - } - - public async redeemNft(ticker: string, id: BigNumber, params: RedeemNftDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const nft = await this.findNft(ticker, id); - - return this.transactionsService.submit(nft.redeem, { from: toPortfolioId(args.from) }, base); - } -} diff --git a/src/notifications/config/notifications.config.ts b/src/notifications/config/notifications.config.ts deleted file mode 100644 index 5ee07d84..00000000 --- a/src/notifications/config/notifications.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; - -export default registerAs('notifications', () => { - const { NOTIFICATIONS_MAX_TRIES, NOTIFICATIONS_RETRY_INTERVAL } = process.env; - - return { - maxTries: Number(NOTIFICATIONS_MAX_TRIES), - retryInterval: Number(NOTIFICATIONS_RETRY_INTERVAL), - }; -}); diff --git a/src/notifications/entities/notification.entity.ts b/src/notifications/entities/notification.entity.ts deleted file mode 100644 index 68e50d36..00000000 --- a/src/notifications/entities/notification.entity.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { NotificationStatus } from '~/notifications/types'; - -export class NotificationEntity { - public id: number; - - public subscriptionId: number; - - public eventId: number; - - public triesLeft: number; - - public status: NotificationStatus; - - public createdAt: Date; - - public nonce: number; - - constructor(entity: NotificationEntity) { - Object.assign(this, entity); - } -} diff --git a/src/notifications/notifications.consts.ts b/src/notifications/notifications.consts.ts deleted file mode 100644 index 744bab45..00000000 --- a/src/notifications/notifications.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const SIGNATURE_HEADER_KEY = 'x-hook-signature'; diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts deleted file mode 100644 index 7f8339a2..00000000 --- a/src/notifications/notifications.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { forwardRef, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { EventsModule } from '~/events/events.module'; -import { LoggerModule } from '~/logger/logger.module'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; - -@Module({ - imports: [ - ConfigModule.forFeature(notificationsConfig), - forwardRef(() => EventsModule), - SubscriptionsModule, - HttpModule, - ScheduleModule, - LoggerModule, - ], - providers: [NotificationsService], - exports: [NotificationsService], -}) -export class NotificationsModule {} diff --git a/src/notifications/notifications.service.spec.ts b/src/notifications/notifications.service.spec.ts deleted file mode 100644 index e5907e59..00000000 --- a/src/notifications/notifications.service.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* eslint-disable import/first */ -const mockLastValueFrom = jest.fn(); - -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppNotFoundError } from '~/common/errors'; -import { TransactionType } from '~/common/types'; -import { EventsService } from '~/events/events.service'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationEntity } from '~/notifications/entities/notification.entity'; -import { NotificationsService } from '~/notifications/notifications.service'; -import { NotificationStatus } from '~/notifications/types'; -import { ScheduleService } from '~/schedule/schedule.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { - MockEventsService, - MockHttpService, - MockScheduleService, - MockSubscriptionsService, -} from '~/test-utils/service-mocks'; - -jest.mock('rxjs', () => ({ - ...jest.requireActual('rxjs'), - lastValueFrom: mockLastValueFrom, -})); - -describe('NotificationsService', () => { - let service: NotificationsService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let unsafeService: any; - - let mockScheduleService: MockScheduleService; - let mockSubscriptionsService: MockSubscriptionsService; - let mockEventsService: MockEventsService; - let mockHttpService: MockHttpService; - - const maxTries = 5; - const retryInterval = 5000; - - beforeEach(async () => { - mockScheduleService = new MockScheduleService(); - mockSubscriptionsService = new MockSubscriptionsService(); - mockEventsService = new MockEventsService(); - mockHttpService = new MockHttpService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - NotificationsService, - ScheduleService, - SubscriptionsService, - EventsService, - HttpService, - mockPolymeshLoggerProvider, - { - provide: notificationsConfig.KEY, - useValue: { maxTries, retryInterval }, - }, - ], - }) - .overrideProvider(ScheduleService) - .useValue(mockScheduleService) - .overrideProvider(SubscriptionsService) - .useValue(mockSubscriptionsService) - .overrideProvider(EventsService) - .useValue(mockEventsService) - .overrideProvider(HttpService) - .useValue(mockHttpService) - .compile(); - - service = module.get(NotificationsService); - unsafeService = service; - - unsafeService.notifications = { - 1: new NotificationEntity({ - id: 1, - subscriptionId: 1, - eventId: 1, - triesLeft: maxTries, - status: NotificationStatus.Acknowledged, - createdAt: new Date('10/14/1987'), - nonce: 0, - }), - }; - unsafeService.currentId = 1; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return a notification by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual( - new NotificationEntity({ - id: 1, - subscriptionId: 1, - eventId: 1, - triesLeft: maxTries, - status: NotificationStatus.Acknowledged, - createdAt: new Date('10/14/1987'), - nonce: 0, - }) - ); - }); - - it('should throw an error if there is no notification with the passed ID', () => { - return expect(service.findOne(10)).rejects.toThrow(AppNotFoundError); - }); - }); - - describe('createNotifications', () => { - it('should create a group of notifications, return their IDs, and schedule them to be sent, retrying if something goes wrong', async () => { - const subscriptionId = 1; - const result = await service.createNotifications([ - { - eventId: 2, - subscriptionId, - nonce: 0, - }, - ]); - - expect(result).toEqual([2]); - - const webhookUrl = 'https://www.example.com'; - const legitimacySecret = 'someSecret'; - const type = EventType.TransactionUpdate; - const scope = '0x01'; - const payload = { - type: TransactionType.Single, - transactionTag: TxTags.asset.RegisterTicker, - }; - const mockIsExpired = jest.fn(); - mockSubscriptionsService.findOne.mockReturnValue({ - webhookUrl, - id: subscriptionId, - isExpired: mockIsExpired, - legitimacySecret, - }); - mockEventsService.findOne.mockReturnValue({ - payload, - type, - scope, - subscriptionId, - }); - mockLastValueFrom.mockReturnValue({ - status: 200, - }); - - // notifications for expired subscriptions should be marked as orphaned - mockIsExpired.mockReturnValue(true); - await unsafeService.sendNotification(1); - - let notification = await service.findOne(1); - - expect(notification.status).toBe(NotificationStatus.Orphaned); - expect(mockHttpService.post).not.toHaveBeenCalled(); - - mockIsExpired.mockReturnValue(false); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.status).toBe(NotificationStatus.Acknowledged); - - await service.updateNotification(2, { - status: NotificationStatus.Active, - }); - - mockLastValueFrom.mockReturnValue({ - status: 500, - }); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.triesLeft).toBe(maxTries - 1); - expect(notification.status).toBe(NotificationStatus.Active); - - await service.updateNotification(2, { - triesLeft: 1, - }); - - await unsafeService.sendNotification(2); - - notification = await service.findOne(2); - - expect(notification.status).toBe(NotificationStatus.Failed); - }); - }); - - describe('updateSubscription', () => { - it('should update a notification and return it, ignoring fields other than status or triesLeft', async () => { - const status = NotificationStatus.Active; - const triesLeft = 1; - const result = await service.updateNotification(1, { - status, - triesLeft, - id: 4, - }); - - expect(result.status).toBe(status); - expect(result.triesLeft).toBe(triesLeft); - expect(result.id).toBe(1); - }); - }); -}); diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts deleted file mode 100644 index 717389cb..00000000 --- a/src/notifications/notifications.service.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; -import { AxiosResponse } from 'axios'; -import { pick } from 'lodash'; -import { lastValueFrom } from 'rxjs'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventsService } from '~/events/events.service'; -import { EventType, GetPayload } from '~/events/types'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import notificationsConfig from '~/notifications/config/notifications.config'; -import { NotificationEntity } from '~/notifications/entities/notification.entity'; -import { SIGNATURE_HEADER_KEY } from '~/notifications/notifications.consts'; -import { signPayload } from '~/notifications/notifications.util'; -import { NotificationPayload, NotificationStatus } from '~/notifications/types'; -import { ScheduleService } from '~/schedule/schedule.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; - -@Injectable() -export class NotificationsService { - private notifications: Record; - private currentId: number; - - private maxTries: number; - private retryInterval: number; - - constructor( - @Inject(notificationsConfig.KEY) config: ConfigType, - private readonly scheduleService: ScheduleService, - private readonly subscriptionsService: SubscriptionsService, - @Inject(forwardRef(() => EventsService)) private readonly eventsService: EventsService, - private readonly httpService: HttpService, - // TODO @polymath-eric: handle errors with specialized service - private readonly logger: PolymeshLogger - ) { - const { maxTries, retryInterval } = config; - - this.maxTries = maxTries; - this.retryInterval = retryInterval; - - this.notifications = {}; - this.currentId = 0; - - logger.setContext(NotificationsService.name); - } - - public async findOne(id: number): Promise { - const notification = this.notifications[id]; - - if (!notification) { - throw new AppNotFoundError(id.toString(), 'notification'); - } - - return notification; - } - - public async createNotifications( - newNotifications: Pick[] - ): Promise { - const { notifications, maxTries: triesLeft } = this; - const newIds: number[] = []; - - newNotifications.forEach(notification => { - this.currentId += 1; // auto-increment - const id = this.currentId; - - newIds.push(id); - notifications[id] = new NotificationEntity({ - id, - ...notification, - triesLeft, - status: NotificationStatus.Active, - createdAt: new Date(), - }); - - /** - * we add the notification to the scheduler cycle - */ - this.scheduleSendNotification(id, 0); - }); - - return newIds; - } - - /** - * @note ignores any properties other than `status` and `triesLeft` - */ - public async updateNotification( - id: number, - data: Partial - ): Promise { - const { notifications } = this; - - const updater = pick(data, 'status', 'triesLeft'); - - const current = await this.findOne(id); - - const updated = new NotificationEntity({ - ...current, - ...updater, - }); - - notifications[id] = updated; - - return updated; - } - - /** - * Schedule a notification to be sent after a certain time has elapsed - * - * @param id - notification ID - * @param ms - amount of milliseconds to wait before sending the notification - */ - private scheduleSendNotification(id: number, ms: number = this.retryInterval): void { - this.scheduleService.addTimeout( - this.getTimeoutId(id), - /* istanbul ignore next */ - () => this.sendNotification(id), - ms - ); - } - - /** - * Generate an identifier for a "send notification" scheduled task. This is used - * to track scheduled timeouts internally - * - * @param id - notification ID - */ - private getTimeoutId(id: number): string { - return `sendNotification_${id}`; - } - - /** - * Attempt to send a notification to the corresponding subscription URL. Any response other than - * 200 will cause a retry to be scheduled - */ - private async sendNotification(id: number): Promise { - const notification = await this.findOne(id); - - const { subscriptionsService, eventsService, logger } = this; - const { subscriptionId, eventId, triesLeft, nonce } = notification; - - try { - const [subscription, { payload, type, scope }] = await Promise.all([ - subscriptionsService.findOne(subscriptionId), - eventsService.findOne(eventId), - ]); - - const { webhookUrl, legitimacySecret } = subscription; - - if (subscription.isExpired()) { - await this.updateNotification(id, { - status: NotificationStatus.Orphaned, - }); - - return; - } - - const notificationPayload = this.assembleNotificationPayload( - subscriptionId, - type, - scope, - payload, - nonce - ); - const signature = signPayload(notificationPayload, legitimacySecret); - const response = await lastValueFrom( - this.httpService.post(webhookUrl, notificationPayload, { - headers: { - [SIGNATURE_HEADER_KEY]: signature, - }, - timeout: 10000, - }) - ); - - await this.handleWebhookResponse(id, response); - } catch (err) { - logger.error(`Error while sending notification "${id}":`, err); - - await this.retry(id, triesLeft - 1); - } - } - - private assembleNotificationPayload( - subscriptionId: number, - type: T, - scope: string, - payload: GetPayload, - nonce: number - ): NotificationPayload { - return { - type, - scope, - subscriptionId, - payload, - nonce, - }; - } - - /** - * Mark the notification as acknowledged if the response status is OK. Otherwise, throw an error - * - * @param id - notification IID - */ - private async handleWebhookResponse(id: number, response: AxiosResponse): Promise { - const { status } = response; - if (status === HttpStatus.OK) { - await this.updateNotification(id, { - status: NotificationStatus.Acknowledged, - }); - - return; - } - - throw new Error(`Webhook responded with non-OK status: ${status}`); - } - - /** - * Reschedule a notification to be sent later - * - * @param id - notification ID - * @param triesLeft - amount of retries left for the notification. If none are left, - * the notification is marked as "timed out" and no retry is scheduled - */ - private async retry(id: number, triesLeft: number): Promise { - if (triesLeft === 0) { - await this.updateNotification(id, { - triesLeft, - status: NotificationStatus.Failed, - }); - - return; - } - - await this.updateNotification(id, { - triesLeft, - }); - - this.scheduleSendNotification(id); - } -} diff --git a/src/notifications/notifications.util.spec.ts b/src/notifications/notifications.util.spec.ts deleted file mode 100644 index eedf1e91..00000000 --- a/src/notifications/notifications.util.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { EventType } from '~/events/types'; -import { signPayload } from '~/notifications/notifications.util'; - -describe('signPayload', () => { - it('should create an HMAC of the passed payload, using the passed secret', () => { - const result = signPayload( - { - scope: 'someScope', - subscriptionId: 1, - nonce: 1, - type: EventType.TransactionUpdate, - payload: { - status: TransactionStatus.Running, - transactionHash: '0x01', - transactionTag: TxTags.asset.RegisterTicker, - type: TransactionType.Single, - }, - }, - 'someSecret' - ); - - expect(result).toBe('iYFr08wYKxLP8eiFT7tOfkvid+0f3FT3h7wH81ELNsQ='); - }); -}); diff --git a/src/notifications/notifications.util.ts b/src/notifications/notifications.util.ts deleted file mode 100644 index 5ce90f49..00000000 --- a/src/notifications/notifications.util.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createHmac } from 'crypto'; -import stringify from 'json-stable-stringify'; - -import { NotificationPayload } from '~/notifications/types'; - -/** - * Compute an HMAC of the payload for legitimacy validation - */ -export function signPayload(payload: NotificationPayload, secret: string): string { - return createHmac('SHA256', secret).update(stringify(payload)).digest('base64'); -} diff --git a/src/notifications/types.ts b/src/notifications/types.ts deleted file mode 100644 index fc0b9197..00000000 --- a/src/notifications/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { EventType, GetPayload } from '~/events/types'; - -export enum NotificationStatus { - /** - * waiting to be received and acknowledged by consumer - */ - Active = 'active', - /** - * properly received by consumer - */ - Acknowledged = 'acknowledged', - /** - * couldn't be delivered after max retries - */ - Failed = 'failed', - /** - * subscription expired before the notification was acknowledged - */ - Orphaned = 'orphaned', -} - -export type NotificationPayload = { - subscriptionId: number; - type: T; - scope: string; - nonce: number; - payload: GetPayload; -}; diff --git a/src/offerings/dto/offering-status-filter.dto.ts b/src/offerings/dto/offering-status-filter.dto.ts deleted file mode 100644 index 0bdc5357..00000000 --- a/src/offerings/dto/offering-status-filter.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ - -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional } from 'class-validator'; - -export class OfferingStatusFilterDto { - @IsEnum(OfferingTimingStatus) - @IsOptional() - readonly timing?: OfferingTimingStatus; - - @IsEnum(OfferingBalanceStatus) - @IsOptional() - readonly balance?: OfferingBalanceStatus; - - @IsEnum(OfferingSaleStatus) - @IsOptional() - readonly sale?: OfferingSaleStatus; -} diff --git a/src/offerings/mocks/offering-with-details.mock.ts b/src/offerings/mocks/offering-with-details.mock.ts deleted file mode 100644 index 05fa68a9..00000000 --- a/src/offerings/mocks/offering-with-details.mock.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { MockOffering, MockPortfolio, MockVenue } from '~/test-utils/mocks'; - -export class MockOfferingWithDetails { - offering = new MockOffering(); - - details = { - tiers: [ - { - amount: new BigNumber(1000), - price: new BigNumber(1), - remaining: new BigNumber(1000), - }, - ], - creator: { - did: 'Ox6'.padEnd(66, '0'), - }, - name: 'SERIES A', - offeringPortfolio: new MockPortfolio(), - raisingPortfolio: new MockPortfolio(), - raisingCurrency: 'CURRENCY', - venue: new MockVenue(), - start: new Date(), - end: null, - status: { - timing: OfferingTimingStatus.Started, - balance: OfferingBalanceStatus.Available, - sale: OfferingSaleStatus.Live, - }, - minInvestment: new BigNumber(1), - totalAmount: new BigNumber(1000), - totalRemaining: new BigNumber(1000), - }; -} diff --git a/src/offerings/models/investment.model.ts b/src/offerings/models/investment.model.ts deleted file mode 100644 index 30eab779..00000000 --- a/src/offerings/models/investment.model.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; - -export class InvestmentModel { - @ApiProperty({ - description: 'The DID of the Investor', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly investor: Identity; - - @ApiProperty({ - description: 'The amount sold', - example: '100', - type: 'string', - }) - @FromBigNumber() - readonly soldAmount: BigNumber; - - @ApiProperty({ - description: 'The amount invested', - example: '10', - type: 'string', - }) - @FromBigNumber() - readonly investedAmount: BigNumber; - - constructor(model: InvestmentModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/models/offering-details.model.ts b/src/offerings/models/offering-details.model.ts deleted file mode 100644 index 5af466dd..00000000 --- a/src/offerings/models/offering-details.model.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Identity, - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingStatus, - OfferingTimingStatus, - Venue, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { TierModel } from '~/offerings/models/tier.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class OfferingDetailsModel { - @ApiProperty({ - description: 'ID of the Offering', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly id: BigNumber; - - @ApiProperty({ - description: 'The DID of the creator Identity', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly creator: Identity; - - @ApiProperty({ - description: 'Name of the Offering', - type: 'string', - example: 'SERIES A', - }) - readonly name: string; - - @ApiProperty({ - description: 'Portfolio containing the Asset being offered', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly offeringPortfolio: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Portfolio receiving the Asset being raised', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly raisingPortfolio: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Currency denomination of the investment', - type: 'string', - example: 'CURR', - }) - readonly raisingCurrency: string; - - @ApiProperty({ - description: 'The Tiers of the Offerings', - type: TierModel, - isArray: true, - }) - @Type(() => TierModel) - readonly tiers: TierModel[]; - - @ApiProperty({ - description: 'The Venue used for the Offering', - type: 'string', - example: '1', - }) - @FromEntity() - readonly venue: Venue; - - @ApiProperty({ - description: 'Start time of the Offering', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly start: Date; - - @ApiProperty({ - description: "End time of the Offering. A null value means the Offering doesn't end", - nullable: true, - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly end: Date | null; - - @ApiProperty({ - description: 'Status of the Offering', - example: { - timing: OfferingTimingStatus.Started, - balance: OfferingBalanceStatus.Available, - sale: OfferingSaleStatus.Live, - }, - }) - readonly status: OfferingStatus; - - @ApiProperty({ - description: 'Minimum raising amount per transaction', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly minInvestment: BigNumber; - - @ApiProperty({ - description: 'Total amount to be raised', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalAmount: BigNumber; - - @ApiProperty({ - description: 'Total amount remaining for purchase', - type: 'string', - example: '10000', - }) - @FromBigNumber() - readonly totalRemaining: BigNumber; - - constructor(model: OfferingDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/models/tier.model.ts b/src/offerings/models/tier.model.ts deleted file mode 100644 index b3dc1ea2..00000000 --- a/src/offerings/models/tier.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { FromBigNumber } from '~/common/decorators/transformation'; - -export class TierModel { - @ApiProperty({ - description: 'Total amount available in the Tier', - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly amount: BigNumber; - - @ApiProperty({ - description: 'Price per unit', - type: 'string', - example: '1', - }) - @FromBigNumber() - readonly price: BigNumber; - - @ApiProperty({ - description: 'Total amount remaining for purchase in the Tier', - type: 'string', - example: '100', - }) - @FromBigNumber() - readonly remaining: BigNumber; - - constructor(model: TierModel) { - Object.assign(this, model); - } -} diff --git a/src/offerings/offerings.controller.spec.ts b/src/offerings/offerings.controller.spec.ts deleted file mode 100644 index 1ae7bf5d..00000000 --- a/src/offerings/offerings.controller.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { OfferingTimingStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { MockOfferingWithDetails } from '~/offerings/mocks/offering-with-details.mock'; -import { OfferingsController } from '~/offerings/offerings.controller'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { createOfferingDetailsModel } from '~/offerings/offerings.util'; -import { MockOfferingsService } from '~/test-utils/service-mocks'; - -describe('OfferingsController', () => { - let controller: OfferingsController; - const mockOfferingsService = new MockOfferingsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [OfferingsController], - providers: [OfferingsService], - }) - .overrideProvider(OfferingsService) - .useValue(mockOfferingsService) - .compile(); - - controller = module.get(OfferingsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getOfferings', () => { - it('should return the list of Offerings for an Asset', async () => { - const mockOfferings = [new MockOfferingWithDetails()]; - - mockOfferingsService.findAllByTicker.mockResolvedValue(mockOfferings); - - const result = await controller.getOfferings( - { ticker: 'TICKER' }, - { timing: OfferingTimingStatus.Started } - ); - - const mockResult = new ResultsModel({ - results: mockOfferings.map(offering => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createOfferingDetailsModel(offering as any) - ), - }); - expect(result).toEqual(mockResult); - }); - }); - - describe('getInvestments', () => { - const mockInvestments = { - data: [ - { - investor: '0x6000', - soldAmount: '100', - investedAmount: '200', - }, - ], - next: '10', - count: new BigNumber(2), - }; - it('should return a paginated list of Investments made in an Offering', async () => { - mockOfferingsService.findInvestmentsByTicker.mockResolvedValue(mockInvestments); - - const result = await controller.getInvestments( - { ticker: 'TICKER', id: new BigNumber(1) }, - { start: new BigNumber(0), size: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockInvestments.data, - total: new BigNumber(mockInvestments.count), - next: mockInvestments.next, - }) - ); - }); - }); -}); diff --git a/src/offerings/offerings.controller.ts b/src/offerings/offerings.controller.ts deleted file mode 100644 index a2cc1931..00000000 --- a/src/offerings/offerings.controller.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingBalanceStatus, - OfferingSaleStatus, - OfferingTimingStatus, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { ApiArrayResponse } from '~/common/decorators/swagger'; -import { IsTicker } from '~/common/decorators/validation'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { OfferingStatusFilterDto } from '~/offerings/dto/offering-status-filter.dto'; -import { InvestmentModel } from '~/offerings/models/investment.model'; -import { OfferingDetailsModel } from '~/offerings/models/offering-details.model'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { createOfferingDetailsModel } from '~/offerings/offerings.util'; - -class OfferingParams extends IdParamsDto { - @IsTicker() - readonly ticker: string; -} - -@ApiTags('offerings') -@Controller('assets/:ticker/offerings') -export class OfferingsController { - constructor(private readonly offeringsService: OfferingsService) {} - - @ApiTags('assets') - @ApiOperation({ - summary: 'Fetch Asset Offerings for an Asset', - description: 'This endpoint will provide the list of all Asset Offerings for an Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset whose Offerings are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiQuery({ - name: 'timing', - description: 'Timing status by which to filter Offerings', - enum: OfferingTimingStatus, - required: false, - }) - @ApiQuery({ - name: 'balance', - description: 'Balance status by which to filter Offerings', - enum: OfferingBalanceStatus, - required: false, - }) - @ApiQuery({ - name: 'sale', - description: 'Sale status by which to filter Offerings', - enum: OfferingSaleStatus, - required: false, - }) - @ApiArrayResponse(OfferingDetailsModel, { - description: 'List of Offerings for this Asset', - paginated: false, - }) - @Get() - public async getOfferings( - @Param() { ticker }: TickerParamsDto, - @Query() { timing, balance, sale }: OfferingStatusFilterDto - ): Promise> { - const offerings = await this.offeringsService.findAllByTicker(ticker, { - timing, - balance, - sale, - }); - return new ResultsModel({ - results: offerings.map(offering => createOfferingDetailsModel(offering)), - }); - } - - @ApiOperation({ - summary: 'List Investments made in an Offering', - description: - 'This endpoint will return a list of Investments made in an Offering for a given Asset', - }) - @ApiParam({ - name: 'ticker', - description: 'The ticker of the Asset', - type: 'string', - example: 'TICKER', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Offering', - type: 'string', - example: '1', - }) - @ApiQuery({ - name: 'size', - description: 'The number of Investments to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Starting offset for pagination', - type: 'string', - required: false, - example: '0', - }) - @ApiArrayResponse(InvestmentModel, { - description: 'A List of Investments', - paginated: true, - }) - @Get(':id/investments') - public async getInvestments( - @Param() { ticker, id }: OfferingParams, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.offeringsService.findInvestmentsByTicker( - ticker, - id, - size, - new BigNumber(start || 0) - ); - return new PaginatedResultsModel({ - results: data.map(({ investor, soldAmount, investedAmount }) => { - return new InvestmentModel({ - investor, - soldAmount, - investedAmount, - }); - }), - total, - next, - }); - } -} diff --git a/src/offerings/offerings.module.ts b/src/offerings/offerings.module.ts deleted file mode 100644 index 25d8a53a..00000000 --- a/src/offerings/offerings.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { OfferingsController } from '~/offerings/offerings.controller'; -import { OfferingsService } from '~/offerings/offerings.service'; - -@Module({ - imports: [forwardRef(() => AssetsModule)], - providers: [OfferingsService], - exports: [OfferingsService], - controllers: [OfferingsController], -}) -export class OfferingsModule {} diff --git a/src/offerings/offerings.service.spec.ts b/src/offerings/offerings.service.spec.ts deleted file mode 100644 index a75e2397..00000000 --- a/src/offerings/offerings.service.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { OfferingTimingStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AppNotFoundError } from '~/common/errors'; -import { MockOfferingWithDetails } from '~/offerings/mocks/offering-with-details.mock'; -import { OfferingsService } from '~/offerings/offerings.service'; -import { MockAsset } from '~/test-utils/mocks'; -import { MockAssetService } from '~/test-utils/service-mocks'; - -describe('OfferingsService', () => { - let service: OfferingsService; - const mockAssetsService = new MockAssetService(); - - let mockOfferingWithDetails: MockOfferingWithDetails; - const mockInvestments = { - data: [ - { - investor: '0x6000', - soldAmount: '100', - investedAmount: '200', - }, - ], - next: '10', - count: new BigNumber(2), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OfferingsService, AssetsService], - }) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(OfferingsService); - - mockOfferingWithDetails = new MockOfferingWithDetails(); - mockOfferingWithDetails.offering.getInvestments.mockReturnValue(mockInvestments); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByTicker', () => { - it('should return the list of Offerings for an Asset', async () => { - const mockOfferings = [new MockOfferingWithDetails()]; - - const mockAsset = new MockAsset(); - mockAsset.offerings.get.mockResolvedValue(mockOfferings); - mockAssetsService.findFungible.mockResolvedValue(mockAsset); - - const result = await service.findAllByTicker('TICKER', { - timing: OfferingTimingStatus.Started, - }); - - expect(result).toEqual(mockOfferings); - }); - }); - - describe('findInvestmentsByTicker', () => { - it('should return a list of investments', async () => { - const findSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue(mockOfferingWithDetails as any); - - const result = await service.findInvestmentsByTicker( - 'TICKER', - new BigNumber(1), - new BigNumber(0) - ); - - expect(result).toEqual({ - data: mockInvestments.data, - count: mockInvestments.count, - next: mockInvestments.next, - }); - }); - }); - - describe('findOne', () => { - describe('if the offering is not found', () => { - it('should throw a AppNotFoundError', async () => { - const findSpy = jest.spyOn(service, 'findAllByTicker'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue([mockOfferingWithDetails] as any); - - let error; - try { - await service.findInvestmentsByTicker('TICKER', new BigNumber(99), new BigNumber(0)); - } catch (err) { - error = err; - } - expect(error).toBeInstanceOf(AppNotFoundError); - }); - }); - describe('otherwise', () => { - it('should return the offering', async () => { - const findSpy = jest.spyOn(service, 'findAllByTicker'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findSpy.mockResolvedValue([mockOfferingWithDetails] as any); - - const result = await service.findOne('TICKER', new BigNumber(1)); - expect(result).toEqual(mockOfferingWithDetails); - }); - }); - }); -}); diff --git a/src/offerings/offerings.service.ts b/src/offerings/offerings.service.ts deleted file mode 100644 index 6facb873..00000000 --- a/src/offerings/offerings.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - OfferingStatus, - OfferingWithDetails, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { AppNotFoundError } from '~/common/errors'; -import { InvestmentModel } from '~/offerings/models/investment.model'; - -@Injectable() -export class OfferingsService { - constructor(private readonly assetsService: AssetsService) {} - - public async findAllByTicker( - ticker: string, - stoStatus?: Partial - ): Promise { - const asset = await this.assetsService.findFungible(ticker); - return asset.offerings.get({ status: stoStatus }); - } - - public async findOne(ticker: string, id: BigNumber): Promise { - const offerings = await this.findAllByTicker(ticker); - const offering = offerings.find(({ offering: { id: offeringId } }) => offeringId.eq(id)); - if (!offering) { - throw new AppNotFoundError(id.toString(), `Asset "${ticker}" Offering`); - } - return offering; - } - - public async findInvestmentsByTicker( - ticker: string, - id: BigNumber, - size: BigNumber, - start?: BigNumber - ): Promise> { - const { offering } = await this.findOne(ticker, id); - return offering.getInvestments({ size, start }); - } -} diff --git a/src/offerings/offerings.util.ts b/src/offerings/offerings.util.ts deleted file mode 100644 index 0923adf5..00000000 --- a/src/offerings/offerings.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { OfferingWithDetails } from '@polymeshassociation/polymesh-sdk/types'; - -import { OfferingDetailsModel } from '~/offerings/models/offering-details.model'; -import { TierModel } from '~/offerings/models/tier.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; - -export function createOfferingDetailsModel( - offeringWithDetails: OfferingWithDetails -): OfferingDetailsModel { - const { - offering: { id }, - details: { tiers, raisingPortfolio, offeringPortfolio, ...rest }, - } = offeringWithDetails; - return new OfferingDetailsModel({ - id, - tiers: tiers.map(tier => new TierModel(tier)), - offeringPortfolio: createPortfolioIdentifierModel(offeringPortfolio), - raisingPortfolio: createPortfolioIdentifierModel(raisingPortfolio), - ...rest, - }); -} diff --git a/src/polymesh-rest-api b/src/polymesh-rest-api new file mode 160000 index 00000000..99e6e85b --- /dev/null +++ b/src/polymesh-rest-api @@ -0,0 +1 @@ +Subproject commit 99e6e85b6e1976b8940934522e20fb97e3cad8c6 diff --git a/src/polymesh/config/polymesh.config.ts b/src/polymesh/config/polymesh.config.ts index 83d35b34..262a066a 100644 --- a/src/polymesh/config/polymesh.config.ts +++ b/src/polymesh/config/polymesh.config.ts @@ -14,20 +14,11 @@ interface Config { } export default registerAs('polymesh', () => { - const { - POLYMESH_NODE_URL, - POLYMESH_MIDDLEWARE_URL, - POLYMESH_MIDDLEWARE_API_KEY, - POLYMESH_MIDDLEWARE_V2_URL, - } = process.env; + const { POLYMESH_NODE_URL, POLYMESH_MIDDLEWARE_V2_URL } = process.env; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const config: Config = { nodeUrl: POLYMESH_NODE_URL || '' }; - if (POLYMESH_MIDDLEWARE_URL && POLYMESH_MIDDLEWARE_API_KEY) { - config.middleware = { link: POLYMESH_MIDDLEWARE_URL, key: POLYMESH_MIDDLEWARE_API_KEY }; - } - if (POLYMESH_MIDDLEWARE_V2_URL) { config.middlewareV2 = { link: POLYMESH_MIDDLEWARE_V2_URL, key: '' }; } diff --git a/src/polymesh/polymesh.module.ts b/src/polymesh/polymesh.module.ts index b9061213..35b19ded 100644 --- a/src/polymesh/polymesh.module.ts +++ b/src/polymesh/polymesh.module.ts @@ -2,12 +2,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigType } from '@nestjs/config'; -import { Polymesh } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialPolymesh as Polymesh } from '@polymeshassociation/polymesh-private-sdk'; import polymeshConfig from '~/polymesh/config/polymesh.config'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ScheduleModule } from '~/schedule/schedule.module'; +import { ScheduleModule } from '~/polymesh-rest-api/src/schedule/schedule.module'; @Module({ imports: [ConfigModule.forFeature(polymeshConfig), ScheduleModule], diff --git a/src/polymesh/polymesh.service.spec.ts b/src/polymesh/polymesh.service.spec.ts index da68ab41..acdfc07b 100644 --- a/src/polymesh/polymesh.service.spec.ts +++ b/src/polymesh/polymesh.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ScheduleService } from '~/schedule/schedule.service'; +import { ScheduleService } from '~/polymesh-rest-api/src/schedule/schedule.service'; import { MockPolymesh } from '~/test-utils/mocks'; import { MockScheduleService } from '~/test-utils/service-mocks'; diff --git a/src/polymesh/polymesh.service.ts b/src/polymesh/polymesh.service.ts index 25231b69..45440f1e 100644 --- a/src/polymesh/polymesh.service.ts +++ b/src/polymesh/polymesh.service.ts @@ -1,11 +1,15 @@ import { Inject, Injectable } from '@nestjs/common'; import { AddressOrPair, AugmentedSubmittable, SubmittableExtrinsic } from '@polkadot/api/types'; import { ISubmittableResult } from '@polkadot/types/types'; -import { Polymesh } from '@polymeshassociation/polymesh-sdk'; +import { ConfidentialPolymesh as Polymesh } from '@polymeshassociation/polymesh-private-sdk'; -import { AppError, AppInternalError, AppValidationError } from '~/common/errors'; import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { ScheduleService } from '~/schedule/schedule.service'; +import { + AppError, + AppInternalError, + AppValidationError, +} from '~/polymesh-rest-api/src/common/errors'; +import { ScheduleService } from '~/polymesh-rest-api/src/schedule/schedule.service'; @Injectable() export class PolymeshService { diff --git a/src/portfolios/decorators/transformation.ts b/src/portfolios/decorators/transformation.ts deleted file mode 100644 index 69f54a1e..00000000 --- a/src/portfolios/decorators/transformation.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { applyDecorators } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Transform } from 'class-transformer'; - -/** - * Transforms a null value default Portfolio id to 0 - */ -export function FromPortfolioId() { - return applyDecorators( - Transform(({ value }: { value?: BigNumber | string }) => (value || new BigNumber(0)).toString()) - ); -} diff --git a/src/portfolios/dto/asset-movement.dto.ts b/src/portfolios/dto/asset-movement.dto.ts deleted file mode 100644 index 22dec80e..00000000 --- a/src/portfolios/dto/asset-movement.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PortfolioMovementDto } from '~/portfolios/dto/portfolio-movement.dto'; - -export class AssetMovementDto extends TransactionBaseDto { - @ApiProperty({ - example: '2', - description: 'ID of the Portfolio to move the Asset from. Use 0 for default Portfolio', - }) - @IsBigNumber() - @ToBigNumber() - readonly from: BigNumber; - - @ApiProperty({ - example: '1', - description: 'ID of the Portfolio to move the Asset to. Use 0 for default Portfolio', - }) - @IsBigNumber() - @ToBigNumber() - readonly to: BigNumber; - - @ApiProperty({ - description: 'List of Assets and amounts to be moved', - isArray: true, - type: PortfolioMovementDto, - }) - @Type(() => PortfolioMovementDto) - @ValidateNested({ each: true }) - readonly items: PortfolioMovementDto[]; -} diff --git a/src/portfolios/dto/create-portfolio.dto.ts b/src/portfolios/dto/create-portfolio.dto.ts deleted file mode 100644 index 3800620c..00000000 --- a/src/portfolios/dto/create-portfolio.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreatePortfolioDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The name of the Portfolio to be created', - example: 'FOLIO-1', - }) - @IsString() - readonly name: string; -} diff --git a/src/portfolios/dto/get-transactions.dto.ts b/src/portfolios/dto/get-transactions.dto.ts deleted file mode 100644 index 2d386fa6..00000000 --- a/src/portfolios/dto/get-transactions.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; - -import { IsTicker } from '~/common/decorators/validation'; - -export class GetTransactionsDto { - @ApiPropertyOptional({ - description: 'Account address involved in transactions', - example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx', - }) - @IsOptional() - @IsString() - readonly account?: string; - - @ApiPropertyOptional({ - description: 'Asset ticker for which the transactions were made', - example: '123', - }) - @IsOptional() - @IsTicker() - readonly ticker?: string; -} diff --git a/src/portfolios/dto/modify-portfolio.dto.ts b/src/portfolios/dto/modify-portfolio.dto.ts deleted file mode 100644 index 7f40cf24..00000000 --- a/src/portfolios/dto/modify-portfolio.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyPortfolioDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The new name of the Portfolio', - example: 'FOLIO-1', - }) - @IsString() - readonly name: string; -} diff --git a/src/portfolios/dto/portfolio-movement.dto.ts b/src/portfolios/dto/portfolio-movement.dto.ts deleted file mode 100644 index a9641154..00000000 --- a/src/portfolios/dto/portfolio-movement.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsByteLength, IsOptional, IsString, ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; - -export class PortfolioMovementDto { - @ApiProperty({ - description: 'Ticker of Asset to move', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; - - @ApiPropertyOptional({ - description: 'Amount of a Fungible Asset to move', - example: '1234', - type: 'string', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'NFT IDs to move from a collection', - example: ['1'], - isArray: true, - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiPropertyOptional({ - description: 'Memo to help identify the transfer. Maximum 32 bytes', - example: 'Transfer to growth portfolio', - }) - @IsOptional() - @IsString() - @IsByteLength(0, 32) - readonly memo?: string; -} diff --git a/src/portfolios/dto/portfolio.dto.ts b/src/portfolios/dto/portfolio.dto.ts deleted file mode 100644 index 2993cbbc..00000000 --- a/src/portfolios/dto/portfolio.dto.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { PortfolioLike } from '@polymeshassociation/polymesh-sdk/types'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid } from '~/common/decorators/validation'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; - -export class PortfolioDto { - @ApiProperty({ - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly did: string; - - @ApiProperty({ - description: 'Portfolio number. Use 0 for the Default Portfolio', - example: '123', - }) - @IsBigNumber() - @ToBigNumber() - readonly id: BigNumber; - - public toPortfolioLike(): PortfolioLike { - const { did, id } = this; - const portfolioId = toPortfolioId(id); - - if (portfolioId) { - return { - identity: did, - id: portfolioId, - }; - } - - return did; - } - - constructor(dto: Omit) { - Object.assign(this, dto); - } -} diff --git a/src/portfolios/dto/set-custodian.dto.ts b/src/portfolios/dto/set-custodian.dto.ts deleted file mode 100644 index 355d0ed9..00000000 --- a/src/portfolios/dto/set-custodian.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDate, IsOptional } from 'class-validator'; - -import { IsDid } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class SetCustodianDto extends TransactionBaseDto { - @ApiProperty({ - description: 'The DID of identity to be set as custodian', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly target: string; - - @ApiPropertyOptional({ - description: 'Expiry date for the custody over Portfolio', - example: new Date('05/23/2021').toISOString(), - }) - @IsOptional() - @IsDate() - readonly expiry?: Date; -} diff --git a/src/portfolios/models/created-portfolio.model.ts b/src/portfolios/models/created-portfolio.model.ts deleted file mode 100644 index 600f45f2..00000000 --- a/src/portfolios/models/created-portfolio.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class CreatedPortfolioModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the newly created Portfolio', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly portfolio: PortfolioIdentifierModel; - - constructor(model: CreatedPortfolioModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/portfolios/models/historic-settlement-leg.model.ts b/src/portfolios/models/historic-settlement-leg.model.ts deleted file mode 100644 index c6620e58..00000000 --- a/src/portfolios/models/historic-settlement-leg.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { SettlementDirectionEnum } from '@polymeshassociation/polymesh-sdk/types'; - -export class HistoricSettlementLegModel { - @ApiProperty({ - description: 'The direction of the settlement leg', - example: SettlementDirectionEnum.Incoming, - enum: SettlementDirectionEnum, - }) - readonly direction: string; - - constructor(model: HistoricSettlementLegModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/historic-settlement.model.ts b/src/portfolios/models/historic-settlement.model.ts deleted file mode 100644 index e5a45155..00000000 --- a/src/portfolios/models/historic-settlement.model.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { SettlementResultEnum } from '@polymeshassociation/polymesh-sdk/middleware/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { AccountDataModel } from '~/identities/models/account-data.model'; -import { HistoricSettlementLegModel } from '~/portfolios/models/historic-settlement-leg.model'; - -export class HistoricSettlementModel { - @ApiProperty({ - description: 'Block number of the settlement transaction', - example: new BigNumber(1), - }) - @FromBigNumber() - readonly blockNumber: BigNumber; - - @ApiProperty({ - description: 'Block hash of the settlement transaction', - example: '0x01', - }) - readonly blockHash: string; - - @ApiProperty({ - description: 'Transaction status', - enum: SettlementResultEnum, - example: SettlementResultEnum.Executed, - }) - readonly status: string; - - @ApiProperty({ - description: 'Array of account addresses involved in the settlement', - example: [ - { address: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx' }, - { address: '5graXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx' }, - ], - type: AccountDataModel, - isArray: true, - }) - @Type(() => AccountDataModel) - readonly accounts: AccountDataModel[]; - - @ApiProperty({ - description: 'Transaction settlement legs', - type: HistoricSettlementLegModel, - isArray: true, - }) - @Type(() => HistoricSettlementLegModel) - readonly legs: HistoricSettlementLegModel[]; - - constructor(model: HistoricSettlementModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/portfolio-identifier.model.ts b/src/portfolios/models/portfolio-identifier.model.ts deleted file mode 100644 index 050f4e98..00000000 --- a/src/portfolios/models/portfolio-identifier.model.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { FromPortfolioId } from '~/portfolios/decorators/transformation'; - -export class PortfolioIdentifierModel { - @ApiProperty({ - description: 'The DID of the Portfolio owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - readonly did: string; - - @ApiProperty({ - description: 'Portfolio number. 0 represents the Default Portfolio', - type: 'string', - example: '123', - }) - @FromPortfolioId() - readonly id?: string; - - constructor(model: PortfolioIdentifierModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/models/portfolio.model.ts b/src/portfolios/models/portfolio.model.ts deleted file mode 100644 index c520d4a8..00000000 --- a/src/portfolios/models/portfolio.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Identity } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { AssetBalanceModel } from '~/assets/models/asset-balance.model'; -import { FromEntity } from '~/common/decorators/transformation'; -import { FromPortfolioId } from '~/portfolios/decorators/transformation'; - -export class PortfolioModel { - @ApiProperty({ - description: 'Portfolio number. 0 represents the Default Portfolio', - type: 'string', - example: '123', - }) - @FromPortfolioId() - readonly id?: BigNumber; - - @ApiProperty({ - description: 'Name of the Portfolio', - type: 'string', - example: 'ABC', - }) - readonly name: string; - - @ApiProperty({ - description: 'List of balances for each Asset in the Portfolio', - type: () => AssetBalanceModel, - isArray: true, - }) - @Type(() => AssetBalanceModel) - readonly assetBalances: AssetBalanceModel[]; - - @ApiPropertyOptional({ - description: 'Identity who custodies the Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly custodian?: Identity; - - @ApiProperty({ - description: 'Identity who owns the Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - constructor(model: PortfolioModel) { - Object.assign(this, model); - } -} diff --git a/src/portfolios/porfolios.controller.spec.ts b/src/portfolios/porfolios.controller.spec.ts deleted file mode 100644 index bcb21e9a..00000000 --- a/src/portfolios/porfolios.controller.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { HistoricSettlementModel } from '~/portfolios/models/historic-settlement.model'; -import { PortfoliosController } from '~/portfolios/portfolios.controller'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { createPortfolioIdentifierModel, createPortfolioModel } from '~/portfolios/portfolios.util'; -import { testValues } from '~/test-utils/consts'; -import { createMockResultSet, MockHistoricSettlement, MockPortfolio } from '~/test-utils/mocks'; -import { MockPortfoliosService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('PortfoliosController', () => { - let controller: PortfoliosController; - const mockPortfoliosService = new MockPortfoliosService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [PortfoliosController], - providers: [PortfoliosService, mockPolymeshLoggerProvider], - }) - .overrideProvider(PortfoliosService) - .useValue(mockPortfoliosService) - .compile(); - - controller = module.get(PortfoliosController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getPortfolios', () => { - it('should return list of all portfolios of an identity', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - mockPortfoliosService.findAllByOwner.mockResolvedValue([mockPortfolio]); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = await createPortfolioModel(mockPortfolio as any, did); - - const result = await controller.getPortfolios({ did }); - - expect(result).toEqual(new ResultsModel({ results: [mockDetails] })); - }); - }); - - describe('moveAssets', () => { - it('should return the transaction details', async () => { - mockPortfoliosService.moveAssets.mockResolvedValue(txResult); - const params = { - signer: '0x6000', - to: new BigNumber(2), - from: new BigNumber(0), - items: [{ to: '3', ticker: 'TICKER', amount: new BigNumber(100) }], - }; - - const result = await controller.moveAssets({ did: '0x6000' }, params); - - expect(result).toEqual(txResult); - }); - }); - - describe('createPortfolio', () => { - it('should return the transaction details', async () => { - const mockPortfolio = new MockPortfolio(); - const response = { - ...txResult, - result: mockPortfolio, - }; - mockPortfoliosService.createPortfolio.mockResolvedValue(response); - const params = { - signer, - name: 'FOLIO-1', - }; - - const result = await controller.createPortfolio(params); - - expect(result).toEqual({ - ...txResult, - portfolio: { - id: '1', - did, - }, - }); - }); - }); - - describe('deletePortfolio', () => { - it('should return the transaction details', async () => { - mockPortfoliosService.deletePortfolio.mockResolvedValue(txResult); - - const result = await controller.deletePortfolio( - new PortfolioDto({ id: new BigNumber(1), did }), - { signer } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('modifyPortfolioName', () => { - it('should return the transaction details', async () => { - const mockPortfolio = new MockPortfolio(); - const response = { - ...txResult, - result: mockPortfolio, - }; - mockPortfoliosService.updatePortfolioName.mockResolvedValue(response); - - const modifyPortfolioArgs = { - signer, - name: 'FOLIO-1', - }; - - const result = await controller.modifyPortfolioName( - new PortfolioDto({ id: new BigNumber(1), did }), - modifyPortfolioArgs - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('getCustodiedPortfolios', () => { - it('should return list of all custodied portfolios of an identity', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - - mockPortfoliosService.getCustodiedPortfolios.mockResolvedValue( - createMockResultSet([mockPortfolio]) - ); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = createPortfolioIdentifierModel(mockPortfolio as any); - - const result = await controller.getCustodiedPortfolios( - { did }, - { size: new BigNumber(1), start: '0' } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ results: [mockDetails], next: '0', total: new BigNumber(1) }) - ); - }); - }); - - describe('getPortfolio', () => { - it('should get the portfolio details', async () => { - const mockPortfolio = new MockPortfolio(); - mockPortfolio.getAssetBalances.mockResolvedValue([]); - mockPortfolio.getCustodian.mockResolvedValue({ did }); - mockPortfolio.getName.mockResolvedValue('P-1'); - mockPortfoliosService.findOne.mockResolvedValue(mockPortfolio); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockDetails = await createPortfolioModel(mockPortfolio as any, did); - - const result = await controller.getPortfolio( - new PortfolioDto({ id: new BigNumber(mockPortfolio.id), did }) - ); - - expect(result).toEqual(mockDetails); - }); - }); - - describe('setCustodian', () => { - it('should return the transaction details', async () => { - const response = { - ...txResult, - }; - mockPortfoliosService.setCustodian.mockResolvedValue(response); - const params: SetCustodianDto = { - target: did, - signer, - }; - - const result = await controller.setCustodian( - new PortfolioDto({ id: new BigNumber(1), did }), - params - ); - - expect(result).toEqual({ - ...txResult, - }); - }); - }); - - describe('getTransactionHistory', () => { - it('should return transaction result model', async () => { - const mockHistoricSettlement = new MockHistoricSettlement(); - const resultSet = createMockResultSet([mockHistoricSettlement]); - mockPortfoliosService.getTransactions.mockResolvedValue([mockHistoricSettlement]); - - const result = await controller.getTransactionHistory( - new PortfolioDto({ id: new BigNumber(1), did }), - {} - ); - - const settlementModelResult = resultSet.data.map( - settlement => new HistoricSettlementModel(settlement as unknown as HistoricSettlementModel) - ); - - expect(result).toEqual({ results: settlementModelResult }); - }); - }); - - describe('quitCustody', () => { - it('should return the transaction details', async () => { - const response = { - ...txResult, - }; - mockPortfoliosService.quitCustody.mockResolvedValue(response); - const params = { - signer, - }; - - const result = await controller.quitCustody( - new PortfolioDto({ id: new BigNumber(1), did }), - params - ); - - expect(result).toEqual({ - ...txResult, - }); - }); - }); - - describe('createdAt', () => { - it('should throw AppNotFoundError if the event details are not yet ready', () => { - mockPortfoliosService.createdAt.mockResolvedValue(null); - - return expect(() => - controller.createdAt(new PortfolioDto({ id: new BigNumber(1), did })) - ).rejects.toBeInstanceOf(NotFoundException); - }); - - describe('otherwise', () => { - it('should return the Portfolio creation event details', async () => { - const eventIdentifier = { - blockNumber: new BigNumber('2719172'), - blockHash: 'someHash', - blockDate: new Date('2021-06-26T01:47:45.000Z'), - eventIndex: new BigNumber(1), - }; - mockPortfoliosService.createdAt.mockResolvedValue(eventIdentifier); - - const result = await controller.createdAt(new PortfolioDto({ id: new BigNumber(1), did })); - - expect(result).toEqual(new EventIdentifierModel(eventIdentifier)); - }); - }); - }); -}); diff --git a/src/portfolios/portfolios.controller.ts b/src/portfolios/portfolios.controller.ts deleted file mode 100644 index 448ada24..00000000 --- a/src/portfolios/portfolios.controller.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { - Body, - Controller, - Get, - HttpStatus, - NotFoundException, - Param, - Post, - Query, -} from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { NumberedPortfolio } from '@polymeshassociation/polymesh-sdk/types'; - -import { - ApiArrayResponse, - ApiTransactionFailedResponse, - ApiTransactionResponse, -} from '~/common/decorators/swagger'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { DidDto } from '~/common/dto/params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { ResultsModel } from '~/common/models/results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; -import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; -import { GetTransactionsDto } from '~/portfolios/dto/get-transactions.dto'; -import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { CreatedPortfolioModel } from '~/portfolios/models/created-portfolio.model'; -import { HistoricSettlementModel } from '~/portfolios/models/historic-settlement.model'; -import { PortfolioModel } from '~/portfolios/models/portfolio.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { createPortfolioIdentifierModel, createPortfolioModel } from '~/portfolios/portfolios.util'; - -@ApiTags('portfolios') -@Controller() -export class PortfoliosController { - constructor( - private readonly portfoliosService: PortfoliosService, - private logger: PolymeshLogger - ) { - logger.setContext(PortfoliosService.name); - } - - @ApiOperation({ - summary: 'Get all Portfolios of an Identity', - description: 'This endpoint will provide list of all the Portfolios of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolios are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(PortfolioModel, { - description: 'Return the list of all Portfolios of the given Identity', - paginated: false, - }) - @Get('/identities/:did/portfolios') - async getPortfolios(@Param() { did }: DidDto): Promise> { - this.logger.debug(`Fetching portfolios for ${did}`); - - const portfolios = await this.portfoliosService.findAllByOwner(did); - - const results = await Promise.all( - portfolios.map(portfolio => createPortfolioModel(portfolio, did)) - ); - - this.logger.debug(`Returning details of ${portfolios.length} portfolios for did ${did}`); - - return new ResultsModel({ results }); - } - - @ApiOperation({ - summary: 'Move Assets between portfolios', - description: 'This endpoint moves Assets between Portfolios', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the owner of the Portfolios to move assets between.', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @Post('/identities/:did/portfolios/move-assets') - public async moveAssets( - @Param() { did }: DidDto, - @Body() transferParams: AssetMovementDto - ): Promise { - const result = await this.portfoliosService.moveAssets(did, transferParams); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Create a Portfolio', - description: 'This endpoint creates a Portfolio', - }) - @ApiTransactionResponse({ - description: 'Details of the newly created Portfolio', - type: CreatedPortfolioModel, - }) - @Post('/portfolios/create') - public async createPortfolio( - @Body() createPortfolioParams: CreatePortfolioDto - ): Promise { - const serviceResult = await this.portfoliosService.createPortfolio(createPortfolioParams); - const resolver: TransactionResolver = ({ transactions, details, result }) => - new CreatedPortfolioModel({ - portfolio: createPortfolioIdentifierModel(result), - details, - transactions, - }); - return handleServiceResult(serviceResult, resolver); - } - - // TODO @prashantasdeveloper: Update error responses post handling error codes - // TODO @prashantasdeveloper: Move the signer to headers - @ApiOperation({ - summary: 'Delete a Portfolio', - description: 'This endpoint deletes a Portfolio', - }) - @ApiParam({ - name: 'id', - description: 'Portfolio number to be deleted', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiOkResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiBadRequestResponse({ - description: "Either the Portfolio doesn't exist or contains assets", - }) - @ApiNotFoundResponse({ - description: 'The Portfolio was removed and no longer exists', - }) - @Post('/identities/:did/portfolios/:id/delete') - public async deletePortfolio( - @Param() portfolio: PortfolioDto, - @Query() transactionBaseDto: TransactionBaseDto - ): Promise { - const result = await this.portfoliosService.deletePortfolio(portfolio, transactionBaseDto); - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Modify Portfolio name', - description: 'This endpoint modifies Portfolio name for a numbered portfolio', - }) - @ApiParam({ - name: 'id', - description: 'Portfolio number for which name is to be modified', - type: 'string', - example: '1', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Portfolio owner', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Portfolio was not found'], - }) - @Post('/identities/:did/portfolios/:id/modify-name') - public async modifyPortfolioName( - @Param() portfolioParams: PortfolioDto, - @Body() modifyPortfolioParams: ModifyPortfolioDto - ): Promise { - const serviceResult = await this.portfoliosService.updatePortfolioName( - portfolioParams, - modifyPortfolioParams - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Get all custodied Portfolios of an Identity', - description: 'This endpoint will provide list of all the custodied Portfolios of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose custodied Portfolios are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiArrayResponse(PortfolioIdentifierModel, { - description: 'Returns the list of all custodied Portfolios of the given Identity', - paginated: true, - }) - @Get('/identities/:did/custodied-portfolios') - async getCustodiedPortfolios( - @Param() { did }: DidDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { - data, - count: total, - next, - } = await this.portfoliosService.getCustodiedPortfolios(did, { - size, - start: start?.toString(), - }); - - const results = data.map(portfolio => createPortfolioIdentifierModel(portfolio)); - - return new PaginatedResultsModel({ - results, - total, - next, - }); - } - - @ApiOperation({ - summary: 'Get details of a Portfolio for an Identity', - description: 'This endpoint will provide details for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio details are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which details are to be fetched. Use 0 for default Portfolio', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Portfolio details', - type: PortfolioModel, - }) - @Get('/identities/:did/portfolios/:id') - async getPortfolio(@Param() { did, id }: PortfolioDto): Promise { - const portfolio = await this.portfoliosService.findOne(did, id); - - return createPortfolioModel(portfolio, did); - } - - @ApiOperation({ - summary: 'Set Portfolio Custodian', - description: 'This endpoint will set Custodian for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity who owns the Portfolio for which Custodian is to be set', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which to set the Custodian. Use 0 for default Portfolio', - type: 'string', - example: '1', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to set Custodian for the Portfolio'], - }) - @Post('/identities/:did/portfolios/:id/custodian') - async setCustodian( - @Param() { did, id }: PortfolioDto, - @Body() setCustodianParams: SetCustodianDto - ): Promise { - const result = await this.portfoliosService.setCustodian(did, id, setCustodianParams); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get list of transactions for a Portfolio', - description: - 'This endpoint will provide list of transaction for the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio transactions are to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which transactions are to be fetched. Use 0 for the default Portfolio', - type: 'string', - example: '0', - }) - @ApiOkResponse({ - description: 'Portfolio transactions', - type: HistoricSettlementModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - }) - @Get('/identities/:did/portfolios/:id/transactions') - async getTransactionHistory( - @Param() { did, id }: PortfolioDto, - @Query() { account, ticker }: GetTransactionsDto - ): Promise> { - const data = await this.portfoliosService.getTransactions(did, id, account, ticker); - - const results = data.map(settlement => new HistoricSettlementModel(settlement)); - - return new ResultsModel({ results }); - } - - @ApiOperation({ - summary: 'Quit Custody of a Portfolio', - description: - 'This endpoint will quit signers Custody over the provided Portfolio of an Identity', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity who owns the Portfolio for which Custody is to be quit', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which to quit Custody. Use 0 for the default Portfolio', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiTransactionResponse({ - description: 'Information about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - 'The Portfolio with provided ID was not found', - 'The Identity with provided DID was not found', - ], - [HttpStatus.UNPROCESSABLE_ENTITY]: ['Insufficient balance to quit Custody for the Portfolio'], - }) - @Post('/identities/:did/portfolios/:id/quit-custody') - async quitCustody( - @Param() { did, id }: PortfolioDto, - @Body() txBase: TransactionBaseDto - ): Promise { - const result = await this.portfoliosService.quitCustody(did, id, txBase); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get Portfolio creation event data', - description: - 'The endpoint retrieves the identifier data (block number, date and event index) of the event that was emitted when the given Numbered Portfolio was created. This requires Polymesh GraphQL Middleware Service', - }) - @ApiParam({ - name: 'did', - description: 'The DID of the Identity whose Portfolio creation event is to be fetched', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @ApiParam({ - name: 'id', - description: - 'The ID of the portfolio for which Portfolio creation event is to be fetched. Throws an error if default Portfolio (0) details are requested', - type: 'string', - example: '1', - }) - @ApiOkResponse({ - description: 'Details of event where the Numbered Portfolio was created', - type: EventIdentifierModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: [ - "The Portfolio doesn't exist", - "The Portfolio hasn't yet been processed by the Middleware", - ], - [HttpStatus.BAD_REQUEST]: ['Event details for default Portfolio are requested'], - }) - @Get('/identities/:did/portfolios/:id/created-at') - async createdAt(@Param() { did, id }: PortfolioDto): Promise { - const result = await this.portfoliosService.createdAt(did, id); - - if (!result) { - throw new NotFoundException("Portfolio data hasn't yet been processed by the middleware"); - } - - return new EventIdentifierModel(result); - } -} diff --git a/src/portfolios/portfolios.module.ts b/src/portfolios/portfolios.module.ts deleted file mode 100644 index 96920265..00000000 --- a/src/portfolios/portfolios.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { IdentitiesModule } from '~/identities/identities.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PortfoliosController } from '~/portfolios/portfolios.controller'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; -@Module({ - imports: [PolymeshModule, LoggerModule, TransactionsModule, forwardRef(() => IdentitiesModule)], - providers: [PortfoliosService], - exports: [PortfoliosService], - controllers: [PortfoliosController], -}) -export class PortfoliosModule {} diff --git a/src/portfolios/portfolios.service.spec.ts b/src/portfolios/portfolios.service.spec.ts deleted file mode 100644 index fdf9bf72..00000000 --- a/src/portfolios/portfolios.service.spec.ts +++ /dev/null @@ -1,465 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { AppValidationError } from '~/common/errors'; -import { IdentitiesService } from '~/identities/identities.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { PortfoliosService } from '~/portfolios/portfolios.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockResultSet, - MockHistoricSettlement, - MockIdentity, - MockPolymesh, - MockPortfolio, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockIdentitiesService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -const { signer, did } = testValues; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('PortfoliosService', () => { - let service: PortfoliosService; - - const mockIdentitiesService = new MockIdentitiesService(); - - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [PortfoliosService, IdentitiesService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .compile(); - - service = module.get(PortfoliosService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAllByOwner', () => { - it('should return a list of Portfolios for a given DID', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolios = [ - { - name: 'Default', - assetBalances: [ - { - ticker: 'TICKER', - }, - ], - }, - { - id: new BigNumber(1), - name: 'TEST', - assetBalances: [], - }, - ]; - mockIdentity.portfolios.getPortfolios.mockResolvedValue(mockPortfolios); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findAllByOwner(did); - expect(result).toEqual(mockPortfolios); - }); - }); - - describe('findOne', () => { - it('should return the Portfolio if it exists', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolio = { - name: 'Growth', - id: new BigNumber(1), - assetBalances: [], - }; - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.findOne(owner, new BigNumber(1)); - expect(result).toEqual({ - id: new BigNumber(1), - name: 'Growth', - assetBalances: [], - }); - }); - - it('should return the default portfolio when given id of 0', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolio = { - id: new BigNumber(0), - assetBalances: [], - }; - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const result = await service.findOne(owner, new BigNumber(0)); - expect(result).toEqual({ - id: new BigNumber(0), - assetBalances: [], - }); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('foo'); - const mockIdentity = new MockIdentity(); - const owner = '0x6000'; - mockIdentity.portfolios.getPortfolio.mockRejectedValue(mockError); - - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findOne(owner, new BigNumber(2))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('moveAssets', () => { - it('should run a moveFunds procedure and return the queue results', async () => { - const findOneSpy = jest.spyOn(service, 'findOne'); - const mockPortfolio = new MockPortfolio(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockPortfolio as any); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.MovePortfolioFunds, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer: '0x6000', - to: new BigNumber(2), - from: new BigNumber(0), - items: [ - { - ticker: 'TICKER', - amount: new BigNumber(123), - }, - ], - }; - - const result = await service.moveAssets('0x6000', body); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPortfolio.moveFunds, - { - to: new BigNumber(2), - items: [ - { - amount: new BigNumber(123), - asset: 'TICKER', - memo: undefined, - }, - ], - }, - { signer: '0x6000' } - ); - }); - }); - - describe('createPortfolio', () => { - it('should create a Portfolio and return the queue results', async () => { - const mockPortfolio = new MockPortfolio(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.CreatePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransaction.run.mockResolvedValue(mockPortfolio); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - - const body = { - signer: '0x6000', - name: 'FOLIO-1', - }; - - const result = await service.createPortfolio(body); - expect(result).toEqual({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - }); - }); - - describe('deletePortfolio', () => { - describe('otherwise', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.DeletePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.delete.mockResolvedValue(mockTransaction); - - const portfolio = new PortfolioDto({ - id: new BigNumber(1), - did, - }); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.deletePortfolio(portfolio, { signer }); - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - }); - - describe('updatePortfolioName', () => { - it('should rename a Portfolio and return the queue results', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.portfolio.RenamePortfolio, - }; - const mockTransaction = new MockTransaction(transaction); - - const mockIdentity = new MockIdentity(); - const modifyName = jest.fn(); - - modifyName.mockReturnValue(mockTransaction); - const mockPortfolio = new MockPortfolio(); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - - const portfolio = new PortfolioDto({ - id: new BigNumber(1), - did, - }); - - const body = { - signer, - name: 'FOLIO-1', - }; - - const result = await service.updatePortfolioName(portfolio, body); - expect(result).toEqual({ - result: mockPortfolio, - transactions: [mockTransaction], - }); - }); - - it('should throw an error on Default portfolio', async () => { - const portfolio = new PortfolioDto({ - id: new BigNumber(0), - did, - }); - - const body = { - signer, - name: 'FOLIO-1', - }; - - const result = service.updatePortfolioName(portfolio, body); - - await expect(result).rejects.toBeInstanceOf(AppValidationError); - }); - }); - - describe('getCustodiedPortfolios', () => { - it('should return a paginated list of custodied Portfolios for a given DID', async () => { - const mockIdentity = new MockIdentity(); - const mockPortfolios = [ - { - name: 'Default', - assetBalances: [ - { - ticker: 'TICKER', - }, - ], - }, - { - id: new BigNumber(1), - name: 'TEST', - assetBalances: [], - }, - ]; - const resultSet = createMockResultSet(mockPortfolios); - - mockIdentity.portfolios.getCustodiedPortfolios.mockResolvedValue(resultSet); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - const result = await service.getCustodiedPortfolios(did, { - size: new BigNumber(10), - start: '0', - }); - expect(result).toEqual(resultSet); - }); - }); - - describe('setCustodian', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const mockTransaction = new MockTransaction(transaction); - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.setCustodian.mockResolvedValue(mockTransaction); - - const custodianParams: SetCustodianDto = { - target: did, - signer, - }; - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.setCustodian(did, mockPortfolio.id, custodianParams); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('getTransactions', () => { - it('should return the transaction result set', async () => { - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - const mockHistoricSettlement = new MockHistoricSettlement(); - - const mockResultSet = createMockResultSet([mockHistoricSettlement]); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.getTransactionHistory.mockResolvedValue(mockResultSet); - - const result = await service.getTransactions(did, mockPortfolio.id); - - expect(result).toEqual(mockResultSet); - }); - }); - - describe('quitCustody', () => { - it('should return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.RemoveAuthorization, - }; - const mockTransaction = new MockTransaction(transaction); - const mockPortfolio = new MockPortfolio(); - const mockIdentity = new MockIdentity(); - - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - mockIdentity.portfolios.getPortfolio.mockResolvedValue(mockPortfolio); - mockPortfolio.quitCustody.mockResolvedValue(mockTransaction); - - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - - const result = await service.quitCustody(did, mockPortfolio.id, { signer }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - }); - }); - - describe('createdAt', () => { - it('should throw an error if default Portfolio details are requested', () => { - return expect(() => service.createdAt(did, new BigNumber(0))).rejects.toThrowError(); - }); - - describe('otherwise', () => { - it('should return the EventIdentifier details for a Portfolio', async () => { - const mockResult = { - blockNumber: new BigNumber('2719172'), - blockHash: 'someHash', - blockDate: new Date('2021-06-26T01:47:45.000Z'), - eventIndex: new BigNumber(1), - }; - const mockPortfolio = new MockPortfolio(); - mockPortfolio.createdAt.mockResolvedValue(mockResult); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jest.spyOn(service, 'findOne').mockResolvedValue(mockPortfolio as any); - const result = await service.createdAt(did, new BigNumber(1)); - expect(result).toEqual(mockResult); - }); - }); - }); -}); diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts deleted file mode 100644 index a17db118..00000000 --- a/src/portfolios/portfolios.service.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AuthorizationRequest, - DefaultPortfolio, - EventIdentifier, - HistoricSettlement, - NumberedPortfolio, - PaginationOptions, - PortfolioMovement, - ResultSet, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { AppValidationError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { AssetMovementDto } from '~/portfolios/dto/asset-movement.dto'; -import { CreatePortfolioDto } from '~/portfolios/dto/create-portfolio.dto'; -import { ModifyPortfolioDto } from '~/portfolios/dto/modify-portfolio.dto'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SetCustodianDto } from '~/portfolios/dto/set-custodian.dto'; -import { toPortfolioId } from '~/portfolios/portfolios.util'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class PortfoliosService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly identitiesService: IdentitiesService, - private readonly transactionsService: TransactionsService - ) {} - - public async findAllByOwner(did: string): Promise<[DefaultPortfolio, ...NumberedPortfolio[]]> { - const identity = await this.identitiesService.findOne(did); - return identity.portfolios.getPortfolios(); - } - - public async findOne(did: string): Promise; - public async findOne(did: string, portfolioId: BigNumber): Promise; - public async findOne( - did: string, - portfolioId?: BigNumber - ): Promise { - const identity = await this.identitiesService.findOne(did); - if (portfolioId?.gt(0)) { - return await identity.portfolios.getPortfolio({ portfolioId }).catch(error => { - throw handleSdkError(error); - }); - } - return await identity.portfolios.getPortfolio().catch(error => { - throw handleSdkError(error); - }); - } - - public async moveAssets(owner: string, params: AssetMovementDto): ServiceReturn { - const { - base, - args: { to, items, from }, - } = extractTxBase(params); - - const fromId = toPortfolioId(from); - const fromPortfolio = fromId ? await this.findOne(owner, fromId) : await this.findOne(owner); - - const formattedArgs = { - to: toPortfolioId(to), - items: items.map(({ ticker: asset, amount, memo, nfts }) => { - return { - asset, - amount, - memo, - nfts, - } as PortfolioMovement; - }), - }; - - return this.transactionsService.submit(fromPortfolio.moveFunds, formattedArgs, base); - } - - public async createPortfolio(params: CreatePortfolioDto): ServiceReturn { - const { - polymeshService: { polymeshApi }, - } = this; - const { base, args } = extractTxBase(params); - - return this.transactionsService.submit(polymeshApi.identities.createPortfolio, args, base); - } - - public async deletePortfolio( - portfolio: PortfolioDto, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const identity = await this.identitiesService.findOne(portfolio.did); - return this.transactionsService.submit( - identity.portfolios.delete, - { portfolio: portfolio.id }, - transactionBaseDto - ); - } - - public async getCustodiedPortfolios( - did: string, - paginationOptions: PaginationOptions - ): Promise> { - const identity = await this.identitiesService.findOne(did); - - return identity.portfolios.getCustodiedPortfolios(paginationOptions); - } - - public async updatePortfolioName( - portfolioParams: PortfolioDto, - params: ModifyPortfolioDto - ): ServiceReturn { - const { did, id } = portfolioParams; - - if (id.lte(0)) { - throw new AppValidationError('Default portfolio name cannot be modified'); - } - - const { base, args } = extractTxBase(params); - const portfolio = await this.findOne(did, id); - - return this.transactionsService.submit(portfolio.modifyName, args, base); - } - - public async setCustodian( - did: string, - portfolioId: BigNumber, - params: SetCustodianDto - ): ServiceReturn { - const portfolio = await this.findOne(did, portfolioId); - const { - base, - args: { target: targetIdentity, expiry }, - } = extractTxBase(params); - - return this.transactionsService.submit( - portfolio.setCustodian, - { targetIdentity, expiry }, - base - ); - } - - public async getTransactions( - did: string, - portfolioId: BigNumber, - account?: string, - ticker?: string - ): Promise { - const portfolio = await this.findOne(did, portfolioId); - - return portfolio.getTransactionHistory({ account, ticker }); - } - - public async quitCustody( - did: string, - id: BigNumber, - args: TransactionBaseDto - ): ServiceReturn { - const portfolio = await this.findOne(did, id); - - return this.transactionsService.submit(portfolio.quitCustody, {}, args); - } - - public async createdAt(did: string, portfolioId: BigNumber): Promise { - if (portfolioId.lte(0)) { - throw new AppValidationError('Cannot get event details for Default Portfolio'); - } - const portfolio = await this.findOne(did, portfolioId); - return portfolio.createdAt(); - } -} diff --git a/src/portfolios/portfolios.util.ts b/src/portfolios/portfolios.util.ts deleted file mode 100644 index 7a43f1e8..00000000 --- a/src/portfolios/portfolios.util.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* istanbul ignore file */ - -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - DefaultPortfolio, - Identity, - NumberedPortfolio, - PortfolioBalance, -} from '@polymeshassociation/polymesh-sdk/types'; -import { isNumberedPortfolio } from '@polymeshassociation/polymesh-sdk/utils'; - -import { AssetBalanceModel } from '~/assets/models/asset-balance.model'; -import { PortfolioModel } from '~/portfolios/models/portfolio.model'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export async function createPortfolioModel( - portfolio: DefaultPortfolio | NumberedPortfolio, - did: string -): Promise { - let custodian: Identity; - let assetBalances: PortfolioBalance[]; - let name = 'default'; - - let portfolioId; - if (isNumberedPortfolio(portfolio)) { - const numberedPortfolio = portfolio; - portfolioId = numberedPortfolio.id; - [assetBalances, custodian, name] = await Promise.all([ - portfolio.getAssetBalances(), - portfolio.getCustodian(), - numberedPortfolio.getName(), - ]); - } else { - [assetBalances, custodian] = await Promise.all([ - portfolio.getAssetBalances(), - portfolio.getCustodian(), - ]); - } - - let portfolioModelParams: ConstructorParameters[0] = { - id: portfolioId, - name, - assetBalances: assetBalances.map( - ({ asset, total, free, locked }) => - new AssetBalanceModel({ - asset, - total, - free, - locked, - }) - ), - owner: portfolio.owner, - }; - if (custodian.did !== did) { - portfolioModelParams = { ...portfolioModelParams, custodian }; - } - return new PortfolioModel(portfolioModelParams); -} - -export function createPortfolioIdentifierModel( - portfolio: DefaultPortfolio | NumberedPortfolio -): PortfolioIdentifierModel { - return new PortfolioIdentifierModel(portfolio.toHuman()); -} - -export function toPortfolioId(id: BigNumber): BigNumber | undefined { - if (id.eq(0)) { - return undefined; - } - return id; -} diff --git a/src/schedule/schedule.module.ts b/src/schedule/schedule.module.ts deleted file mode 100644 index a966e02b..00000000 --- a/src/schedule/schedule.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; - -import { LoggerModule } from '~/logger/logger.module'; -import { ScheduleService } from '~/schedule/schedule.service'; - -/** - * Scheduler module to allow better control over errors in scheduled tasks - */ -@Module({ - imports: [NestScheduleModule.forRoot(), LoggerModule], - providers: [ScheduleService], - exports: [ScheduleService], -}) -export class ScheduleModule {} diff --git a/src/schedule/schedule.service.spec.ts b/src/schedule/schedule.service.spec.ts deleted file mode 100644 index 6dd60764..00000000 --- a/src/schedule/schedule.service.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SchedulerRegistry } from '@nestjs/schedule'; -import { Test, TestingModule } from '@nestjs/testing'; - -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { ScheduleService } from '~/schedule/schedule.service'; - -describe('ScheduleService', () => { - let service: ScheduleService; - let registry: SchedulerRegistry; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ScheduleService, mockPolymeshLoggerProvider, SchedulerRegistry], - }).compile(); - - registry = module.get(SchedulerRegistry); - - service = module.get(ScheduleService); - - jest.useFakeTimers(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('addInterval', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - afterEach(() => { - cb.mockReset(); - }); - - it('should add an interval function to the scheduler registry', () => { - service.addInterval(id, cb, time); - - expect(registry.getInterval(id)).toBeDefined(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(time * 3); - - expect(cb).toHaveBeenCalledTimes(3); - }); - - it('should handle any errors thrown by the callback', () => { - const message = 'foo'; - cb.mockImplementation(() => { - throw new Error(message); - }); - - service.addInterval(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - }); - }); - - describe('deleteInterval', () => { - it('should remove an interval added to the scheduler registry', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - service.addInterval(id, cb, time); - - expect(registry.getInterval(id)).toBeDefined(); - - service.deleteInterval(id); - - expect(() => registry.getInterval(id)).toThrow( - `No Interval was found with the given name (${id}). Check that you created one with a decorator or with the create API.` - ); - }); - }); - - describe('addTimeout', () => { - const id = 'someId'; - const cb = jest.fn(); - const time = 5000; - - afterEach(() => { - cb.mockReset(); - }); - - it('should add a timeout function to the scheduler registry, and remove it when it has run', () => { - service.addTimeout(id, cb, time); - - expect(registry.getTimeout(id)).toBeDefined(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(time * 3); - - expect(cb).toHaveBeenCalledTimes(1); - - expect(() => registry.getTimeout(id)).toThrow( - `No Timeout was found with the given name (${id}). Check that you created one with a decorator or with the create API.` - ); - }); - - it('should handle any errors thrown by the callback', () => { - const message = 'foo'; - cb.mockImplementation(() => { - throw new Error(message); - }); - - service.addTimeout(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - - cb.mockImplementation(() => { - throw message; - }); - - service.addTimeout(id, cb, time); - - jest.advanceTimersByTime(time); - - expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalledWith( - `Error on scheduled task "${id}": ${message}` - ); - }); - }); -}); diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts deleted file mode 100644 index 54e95311..00000000 --- a/src/schedule/schedule.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SchedulerRegistry } from '@nestjs/schedule'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { ScheduledTaskType } from '~/schedule/types'; - -@Injectable() -export class ScheduleService { - constructor( - private readonly schedulerRegistry: SchedulerRegistry, - private readonly logger: PolymeshLogger - ) { - logger.setContext(ScheduleService.name); - } - - public addInterval(id: string, cb: () => void | Promise, ms: number): void { - const { schedulerRegistry } = this; - - const interval = setInterval(this.wrapCallback(id, ScheduledTaskType.Interval, cb), ms); - - schedulerRegistry.addInterval(id, interval); - } - - public deleteInterval(id: string): void { - this.schedulerRegistry.deleteInterval(id); - } - - public addTimeout(id: string, cb: () => void | Promise, ms: number): void { - const { schedulerRegistry } = this; - - const timeout = setTimeout(this.wrapCallback(id, ScheduledTaskType.Timeout, cb), ms); - - schedulerRegistry.addTimeout(id, timeout); - } - - public deleteTimeout(id: string): void { - this.schedulerRegistry.deleteTimeout(id); - } - - /** - * Wrap a task callback in a function that handles any errors - * - * @param id - task identifier (i.e. "sendNotification_1") - * @param cb - task callback to be wrapped - * - * @returns a wrapped version of the callback that gracefully handles errors - */ - private wrapCallback( - id: string, - type: ScheduledTaskType, - cb: () => void | Promise - ): () => Promise { - const { logger } = this; - - return async (): Promise => { - // the scheduler registry keeps timeout ids forever. This allows us to reuse them - if (type === ScheduledTaskType.Timeout) { - this.deleteTimeout(id); - } - - try { - await cb(); - } catch (err) { - logger.error( - `Error on scheduled task "${id}": ${(err as Error).message || JSON.stringify(err)}` - ); - } - }; - } -} diff --git a/src/schedule/types.ts b/src/schedule/types.ts deleted file mode 100644 index eb4b0b1f..00000000 --- a/src/schedule/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ScheduledTaskType { - Timeout = 'timeout', - Interval = 'interval', -} diff --git a/src/settlements/dto/create-instruction.dto.ts b/src/settlements/dto/create-instruction.dto.ts deleted file mode 100644 index b6d2ad9f..00000000 --- a/src/settlements/dto/create-instruction.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* istanbul ignore file */ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { IsByteLength, IsDate, IsOptional, IsString, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { LegDto } from '~/settlements/dto/leg.dto'; - -export class CreateInstructionDto extends TransactionBaseDto { - @ValidateNested({ each: true }) - @Type(() => LegDto) - readonly legs: LegDto[]; - - @ApiPropertyOptional({ - description: 'Date at which the trade was agreed upon (optional, for offchain trades)', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly tradeDate?: Date; - - @ApiPropertyOptional({ - description: 'Date at which the trade was executed (optional, for offchain trades)', - example: new Date('10/14/1987').toISOString(), - }) - @IsOptional() - @IsDate() - readonly valueDate?: Date; - - @ApiPropertyOptional({ - type: 'string', - description: - 'Block at which the Instruction will be executed. If not passed, the Instruction will be executed when all parties affirm or as soon as one party rejects', - example: '123', - }) - @IsOptional() - @IsBigNumber() - @ToBigNumber() - readonly endBlock?: BigNumber; - - @ApiPropertyOptional({ - description: 'Identifier string to help differentiate instructions. Maximum 32 bytes', - example: 'Transfer of GROWTH Asset', - }) - @IsOptional() - @IsString() - @IsByteLength(0, 32) - readonly memo?: string; -} diff --git a/src/settlements/dto/create-venue.dto.ts b/src/settlements/dto/create-venue.dto.ts deleted file mode 100644 index 4cff20ae..00000000 --- a/src/settlements/dto/create-venue.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { VenueType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateVenueDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Description of the Venue', - example: 'A place to exchange Assets', - }) - @IsString() - readonly description: string; - - @ApiProperty({ - description: 'The type of Venue', - enum: VenueType, - example: VenueType.Exchange, - }) - @IsEnum(VenueType) - readonly type: VenueType; -} diff --git a/src/settlements/dto/leg-validation-params.dto.ts b/src/settlements/dto/leg-validation-params.dto.ts deleted file mode 100644 index e7e4f2c4..00000000 --- a/src/settlements/dto/leg-validation-params.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ValidateIf } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsDid, IsTicker } from '~/common/decorators/validation'; - -export class LegValidationParamsDto { - @ApiPropertyOptional({ - description: 'Amount of the Asset to be transferred', - type: 'string', - example: '1000', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFT IDs to be transferred for the collection', - type: 'string', - isArray: true, - example: ['1'], - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'DID of the sender', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly fromDid: string; - - @ApiProperty({ - description: - 'Portfolio ID of the sender from which Asset is to be transferred. Use 0 for the Default Portfolio', - type: 'string', - example: '1', - }) - @IsBigNumber() - @ToBigNumber() - readonly fromPortfolio: BigNumber; - - @ApiProperty({ - description: 'DID of the receiver', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @IsDid() - readonly toDid: string; - - @ApiProperty({ - description: - 'Portfolio ID of the receiver to which Asset is to be transferred. Use 0 for Default Portfolio', - type: 'string', - example: '2', - }) - @IsBigNumber() - @ToBigNumber() - readonly toPortfolio: BigNumber; - - @ApiProperty({ - description: 'Ticker of the Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @IsTicker() - readonly asset: string; -} diff --git a/src/settlements/dto/leg.dto.ts b/src/settlements/dto/leg.dto.ts deleted file mode 100644 index ae0c9873..00000000 --- a/src/settlements/dto/leg.dto.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; -import { ValidateIf, ValidateNested } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber, IsTicker } from '~/common/decorators/validation'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; - -export class LegDto { - @ApiPropertyOptional({ - description: 'Amount of the fungible Asset to be transferred', - type: 'string', - example: '1000', - }) - @ValidateIf(({ nfts }) => !nfts) - @IsBigNumber() - @ToBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFT IDs of a collection to be transferred', - type: 'string', - example: ['1'], - }) - @ValidateIf(({ amount }) => !amount) - @IsBigNumber() - @ToBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'Portfolio of the sender', - type: () => PortfolioDto, - example: { - did: '0x0600000000000000000000000000000000000000000000000000000000000000', - id: 1, - }, - }) - @ValidateNested() - @Type(() => PortfolioDto) - readonly from: PortfolioDto; - - @ApiProperty({ - description: 'Portfolio of the receiver', - type: () => PortfolioDto, - example: { - did: '0x0111111111111111111111111111111111111111111111111111111111111111', - id: 0, - }, - }) - @ValidateNested() - @Type(() => PortfolioDto) - readonly to: PortfolioDto; - - @ApiProperty({ - description: 'Asset ticker', - example: 'TICKER', - }) - @IsTicker() - readonly asset: string; -} diff --git a/src/settlements/dto/modify-venue.dto.ts b/src/settlements/dto/modify-venue.dto.ts deleted file mode 100644 index dd864f84..00000000 --- a/src/settlements/dto/modify-venue.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { VenueType } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsString, ValidateIf } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyVenueDto extends TransactionBaseDto { - @ApiPropertyOptional({ - description: 'Details about the Venue', - example: 'The TSX is an exchange located in Toronto, Ontario', - }) - @ValidateIf(({ type, description }: ModifyVenueDto) => !type || !!description) - @IsString() - readonly description?: string; - - @ApiPropertyOptional({ - description: 'The type of Venue', - enum: VenueType, - example: VenueType.Exchange, - }) - @ValidateIf(({ type, description }: ModifyVenueDto) => !!type || !description) - @IsEnum(VenueType) - readonly type?: VenueType; -} diff --git a/src/settlements/models/created-instruction.model.ts b/src/settlements/models/created-instruction.model.ts deleted file mode 100644 index 54e57a42..00000000 --- a/src/settlements/models/created-instruction.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Instruction } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedInstructionModel extends TransactionQueueModel { - @ApiProperty({ - type: 'string', - description: 'ID of the newly created settlement Instruction', - example: '123', - }) - @FromEntity() - readonly instruction: Instruction; - - constructor(model: CreatedInstructionModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/settlements/models/created-venue.model.ts b/src/settlements/models/created-venue.model.ts deleted file mode 100644 index b446b7c2..00000000 --- a/src/settlements/models/created-venue.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Venue } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; - -export class CreatedVenueModel extends TransactionQueueModel { - @ApiProperty({ - type: 'string', - description: 'ID of the newly created Venue', - example: '123', - }) - @FromEntity() - readonly venue: Venue; - - constructor(model: CreatedVenueModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/settlements/models/grouped-instructions.model.ts b/src/settlements/models/grouped-instructions.model.ts deleted file mode 100644 index 9dd5465a..00000000 --- a/src/settlements/models/grouped-instructions.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { GroupedInstructions } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class GroupedInstructionModel { - @ApiProperty({ - description: 'List of affirmed Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly affirmed: BigNumber[]; - - @ApiProperty({ - description: 'List of pending Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly pending: BigNumber[]; - - @ApiProperty({ - description: 'List of failed Instruction ids', - isArray: true, - type: 'number', - example: [123], - }) - @FromEntityObject() - readonly failed: BigNumber[]; - - constructor(instructions: GroupedInstructions) { - Object.assign(this, instructions); - } -} diff --git a/src/settlements/models/instruction-affirmation.model.ts b/src/settlements/models/instruction-affirmation.model.ts deleted file mode 100644 index 7a474d7f..00000000 --- a/src/settlements/models/instruction-affirmation.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { AffirmationStatus, Identity } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class InstructionAffirmationModel { - @ApiProperty({ - description: 'The DID of the identity affirming the Instruction', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - identity: Identity; - - @ApiProperty({ - description: 'The current status of the Instruction', - type: 'string', - enum: AffirmationStatus, - example: AffirmationStatus.Pending, - }) - status: AffirmationStatus; - - constructor(model: InstructionAffirmationModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/instruction.model.ts b/src/settlements/models/instruction.model.ts deleted file mode 100644 index 0ae52367..00000000 --- a/src/settlements/models/instruction.model.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { InstructionStatus, InstructionType, Venue } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { LegModel } from '~/settlements/models/leg.model'; - -export class InstructionModel { - @ApiProperty({ - description: 'ID of the Venue through which the settlement is handled', - type: 'string', - example: '123', - }) - @FromEntity() - readonly venue: Venue; - - @ApiProperty({ - description: 'Date when the Instruction was created', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly createdAt: Date; - - @ApiProperty({ - description: 'The current status of the Instruction', - type: 'string', - enum: InstructionStatus, - example: InstructionStatus.Pending, - }) - readonly status: InstructionStatus; - - @ApiPropertyOptional({ - description: 'Date at which the trade was agreed upon (optional, for offchain trades)', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly tradeDate?: Date; - - @ApiPropertyOptional({ - description: 'Date at which the trade was executed (optional, for offchain trades)', - type: 'string', - example: new Date('10/14/1987').toISOString(), - }) - readonly valueDate?: Date; - - @ApiProperty({ - description: 'Type of the Instruction', - type: 'string', - enum: InstructionType, - example: InstructionType.SettleOnBlock, - }) - readonly type: InstructionType; - - @ApiPropertyOptional({ - description: - 'Block at which the Instruction is executed. This value will only be present for "SettleOnBlock" type Instruction', - type: 'string', - example: '1000000', - }) - @FromBigNumber() - readonly endBlock?: BigNumber; - - @ApiPropertyOptional({ - description: - 'Identifies the event where the Instruction execution was attempted. This value will not be present for a "Pending" Instruction', - type: EventIdentifierModel, - }) - @Type(() => EventIdentifierModel) - readonly eventIdentifier?: EventIdentifierModel; - - @ApiPropertyOptional({ - description: 'Identifier string provided while creating the Instruction', - example: 'Transfer of GROWTH Asset', - }) - readonly memo?: string; - - @ApiProperty({ - description: 'List of Legs in the Instruction', - type: LegModel, - isArray: true, - }) - @Type(() => LegModel) - readonly legs: LegModel[]; - - constructor(model: InstructionModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/leg.model.ts b/src/settlements/models/leg.model.ts deleted file mode 100644 index feaf1128..00000000 --- a/src/settlements/models/leg.model.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { FungibleAsset, NftCollection } from '@polymeshassociation/polymesh-sdk/types'; -import { Type } from 'class-transformer'; - -import { FromBigNumber, FromEntity } from '~/common/decorators/transformation'; -import { PortfolioIdentifierModel } from '~/portfolios/models/portfolio-identifier.model'; - -export class LegModel { - @ApiProperty({ - description: 'Portfolio from which the transfer is to be made', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly from: PortfolioIdentifierModel; - - @ApiProperty({ - description: 'Portfolio to which the transfer is to be made', - type: PortfolioIdentifierModel, - }) - @Type(() => PortfolioIdentifierModel) - readonly to: PortfolioIdentifierModel; - - @ApiPropertyOptional({ - description: 'Amount of fungible tokens to be transferred', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly amount?: BigNumber; - - @ApiPropertyOptional({ - description: 'The NFTs from the collection to be transferred', - type: 'string', - example: '123', - }) - @FromBigNumber() - readonly nfts?: BigNumber[]; - - @ApiProperty({ - description: 'Asset to be transferred', - type: 'string', - example: 'TICKER', - }) - @FromEntity() - readonly asset: FungibleAsset | NftCollection; - - constructor(model: LegModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/transfer-breakdown.model.ts b/src/settlements/models/transfer-breakdown.model.ts deleted file mode 100644 index 31605f50..00000000 --- a/src/settlements/models/transfer-breakdown.model.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Compliance, - TransferError, - TransferRestrictionResult, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntityObject } from '~/common/decorators/transformation'; - -export class TransferBreakdownModel { - @ApiProperty({ - description: 'List of general transfer errors', - type: 'string', - enum: TransferError, - example: [TransferError.InvalidSenderPortfolio, TransferError.InvalidSenderCdd], - }) - readonly general: TransferError[]; - - @ApiProperty({ - description: 'Compliance rules for the Asset, and whether the Asset transfer adheres to them', - }) - @FromEntityObject() - readonly compliance: Compliance; - - @ApiProperty({ - description: 'List of transfer restrictions and whether the transfer satisfies each one', - }) - @FromEntityObject() - readonly restrictions: TransferRestrictionResult[]; - - @ApiProperty({ - description: 'Indicator to know if the transfer is possible.', - type: 'boolean', - example: true, - }) - readonly result: boolean; - - constructor(model: TransferBreakdownModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/models/venue-details.model.ts b/src/settlements/models/venue-details.model.ts deleted file mode 100644 index 536be4b2..00000000 --- a/src/settlements/models/venue-details.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, VenueType } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class VenueDetailsModel { - @ApiProperty({ - description: 'The DID of the Venue owner', - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - }) - @FromEntity() - readonly owner: Identity; - - @ApiProperty({ - description: 'Description of the Venue', - type: 'string', - example: 'VENUE-DESC', - }) - readonly description: string; - - @ApiProperty({ - description: 'Type of the Venue', - type: 'string', - enum: VenueType, - example: VenueType.Distribution, - }) - readonly type: VenueType; - - constructor(model: VenueDetailsModel) { - Object.assign(this, model); - } -} diff --git a/src/settlements/settlements.controller.spec.ts b/src/settlements/settlements.controller.spec.ts deleted file mode 100644 index 88e747b8..00000000 --- a/src/settlements/settlements.controller.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AffirmationStatus, - InstructionStatus, - InstructionType, - Nft, - TransferError, - VenueType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; -import { SettlementsController } from '~/settlements/settlements.controller'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { MockInstruction, MockPortfolio, MockVenue } from '~/test-utils/mocks'; -import { MockSettlementsService } from '~/test-utils/service-mocks'; - -const { did, signer, txResult } = testValues; - -describe('SettlementsController', () => { - let controller: SettlementsController; - const mockSettlementsService = new MockSettlementsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SettlementsController], - providers: [SettlementsService], - }) - .overrideProvider(SettlementsService) - .useValue(mockSettlementsService) - .compile(); - - controller = module.get(SettlementsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getInstruction', () => { - it('should return the Instruction details', async () => { - const date = new Date(); - - const mockInstruction = new MockInstruction(); - const mockInstructionDetails = { - venue: { - id: new BigNumber(123), - }, - status: InstructionStatus.Pending, - createdAt: date, - type: InstructionType.SettleOnBlock, - endBlock: new BigNumber(1000000), - }; - const mockLegs = { - data: [ - { - from: new MockPortfolio(), - to: new MockPortfolio(), - amount: new BigNumber(100), - asset: { - ticker: 'TICKER', - }, - }, - { - from: new MockPortfolio(), - to: new MockPortfolio(), - nfts: [createMock({ id: new BigNumber(1) })], - asset: { - ticker: 'TICKER', - }, - }, - ], - next: null, - }; - mockInstruction.details.mockResolvedValue(mockInstructionDetails); - mockInstruction.getStatus.mockResolvedValue({ status: InstructionStatus.Pending }); - mockInstruction.getLegs.mockResolvedValue(mockLegs); - mockSettlementsService.findInstruction.mockResolvedValue(mockInstruction); - const result = await controller.getInstruction({ id: new BigNumber(3) }); - - expect(result).toEqual({ - ...mockInstructionDetails, - legs: - mockLegs.data.map(({ from, to, amount, nfts, asset }) => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - from: createPortfolioIdentifierModel(from as any), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - to: createPortfolioIdentifierModel(to as any), - amount, - nfts, - asset, - })) || [], - }); - }); - }); - - describe('createInstruction', () => { - it('should create an instruction and return the data returned by the service', async () => { - const mockData = { - ...txResult, - result: 'fakeInstruction', - }; - mockSettlementsService.createInstruction.mockResolvedValue(mockData); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await controller.createInstruction({ id: new BigNumber(3) }, {} as any); - - expect(result).toEqual({ - ...txResult, - instruction: 'fakeInstruction', - }); - }); - }); - - describe('affirmInstruction', () => { - it('should affirm an instruction and return the data returned by the service', async () => { - mockSettlementsService.affirmInstruction.mockResolvedValue(txResult); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await controller.affirmInstruction({ id: new BigNumber(3) }, {} as any); - - expect(result).toEqual(txResult); - }); - }); - - describe('rejectInstruction', () => { - it('should reject an instruction and return the data returned by the service', async () => { - mockSettlementsService.rejectInstruction.mockResolvedValue(txResult); - - const result = await controller.affirmInstruction( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('withdrawAffirmation', () => { - it('should withdraw affirmation from an instruction and return the data returned by the service', async () => { - mockSettlementsService.withdrawAffirmation.mockResolvedValue(txResult); - - const result = await controller.withdrawAffirmation( - { id: new BigNumber(3) }, - { signer: 'signer' } - ); - - expect(result).toEqual(txResult); - }); - }); - - describe('getAffirmations', () => { - it('should return the list of affirmations generated for a Instruction', async () => { - const mockAffirmations = { - data: [ - { - identity: { - did, - }, - status: AffirmationStatus.Pending, - }, - ], - next: null, - }; - mockSettlementsService.findAffirmations.mockResolvedValue(mockAffirmations); - - const result = await controller.getAffirmations( - { id: new BigNumber(3) }, - { size: new BigNumber(10) } - ); - - expect(result).toEqual( - new PaginatedResultsModel({ - results: mockAffirmations.data, - next: null, - }) - ); - }); - }); - - describe('getVenueDetails', () => { - it('should return the details of the Venue', async () => { - const mockVenueDetails = { - owner: { - did, - }, - description: 'Venue desc', - type: VenueType.Distribution, - }; - mockSettlementsService.findVenueDetails.mockResolvedValue(mockVenueDetails); - - const result = await controller.getVenueDetails({ id: new BigNumber(3) }); - - expect(result).toEqual(mockVenueDetails); - }); - }); - - describe('createVenue', () => { - it('should create a Venue and return the data returned by the service', async () => { - const body = { - signer, - description: 'Generic Exchange', - type: VenueType.Exchange, - }; - const mockVenue = new MockVenue(); - const mockData = { - ...txResult, - result: mockVenue, - }; - mockSettlementsService.createVenue.mockResolvedValue(mockData); - - const result = await controller.createVenue(body); - - expect(result).toEqual({ - ...txResult, - venue: mockVenue, - }); - }); - }); - - describe('modifyVenue', () => { - it('should modify a venue and return the data returned by the service', async () => { - mockSettlementsService.modifyVenue.mockResolvedValue(txResult); - - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await controller.modifyVenue({ id: new BigNumber(3) }, body); - - expect(result).toEqual(txResult); - }); - }); - - describe('validateLeg', () => { - it('should call the service and return the Leg validations', async () => { - const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], - compliance: { - requirements: [], - complies: false, - }, - restrictions: [], - result: false, - }; - - mockSettlementsService.canTransfer.mockResolvedValue(mockTransferBreakdown); - - const result = await controller.validateLeg({ - fromDid: 'fromDid', - fromPortfolio: new BigNumber(1), - toDid: 'toDid', - toPortfolio: new BigNumber(1), - asset: 'TICKER', - amount: new BigNumber(123), - }); - - expect(result).toEqual(mockTransferBreakdown); - }); - }); -}); diff --git a/src/settlements/settlements.controller.ts b/src/settlements/settlements.controller.ts deleted file mode 100644 index 549d793a..00000000 --- a/src/settlements/settlements.controller.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; -import { Instruction, Venue } from '@polymeshassociation/polymesh-sdk/types'; - -import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { IdParamsDto } from '~/common/dto/id-params.dto'; -import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { PaginatedResultsModel } from '~/common/models/paginated-results.model'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; -import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; -import { LegValidationParamsDto } from '~/settlements/dto/leg-validation-params.dto'; -import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; -import { CreatedInstructionModel } from '~/settlements/models/created-instruction.model'; -import { CreatedVenueModel } from '~/settlements/models/created-venue.model'; -import { InstructionModel } from '~/settlements/models/instruction.model'; -import { InstructionAffirmationModel } from '~/settlements/models/instruction-affirmation.model'; -import { TransferBreakdownModel } from '~/settlements/models/transfer-breakdown.model'; -import { VenueDetailsModel } from '~/settlements/models/venue-details.model'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { createInstructionModel } from '~/settlements/settlements.util'; - -@ApiTags('settlements') -@Controller() -export class SettlementsController { - constructor(private readonly settlementsService: SettlementsService) {} - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Fetch Instruction details', - description: 'This endpoint will provide the details of the Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the Instruction', - type: InstructionModel, - }) - @ApiNotFoundResponse({ - description: 'The Instruction with the given ID was not found', - }) - @Get('instructions/:id') - public async getInstruction(@Param() { id }: IdParamsDto): Promise { - const instruction = await this.settlementsService.findInstruction(id); - return createInstructionModel(instruction); - } - - @ApiTags('venues', 'instructions') - @ApiOperation({ - summary: 'Create a new Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Venue through which Settlement will be handled', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'The ID of the newly created Instruction', - type: CreatedInstructionModel, - }) - @Post('venues/:id/instructions/create') - public async createInstruction( - @Param() { id }: IdParamsDto, - @Body() createInstructionDto: CreateInstructionDto - ): Promise { - const serviceResult = await this.settlementsService.createInstruction(id, createInstructionDto); - - const resolver: TransactionResolver = ({ - result: instruction, - transactions, - details, - }) => - new CreatedInstructionModel({ - instruction, - details, - transactions, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Affirm an existing Instruction', - description: - 'This endpoint will affirm a pending Instruction. All owners of involved portfolios must affirm for the Instruction to be executed', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be affirmed', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/affirm') - public async affirmInstruction( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.affirmInstruction(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Reject an existing Instruction', - description: 'This endpoint will reject a pending Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction to be rejected', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @Post('instructions/:id/reject') - public async rejectInstruction( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.rejectInstruction(id, signerDto); - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'Withdraw affirmation from an existing Instruction', - description: 'This endpoint will withdraw an affirmation from an Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction from which to withdraw the affirmation', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the transaction', - type: TransactionQueueModel, - }) - @ApiNotFoundResponse({ - description: 'The requested Instruction was not found', - }) - @Post('instructions/:id/withdraw') - public async withdrawAffirmation( - @Param() { id }: IdParamsDto, - @Body() signerDto: TransactionBaseDto - ): Promise { - const result = await this.settlementsService.withdrawAffirmation(id, signerDto); - - return handleServiceResult(result); - } - - @ApiTags('instructions') - @ApiOperation({ - summary: 'List of affirmations', - description: - 'This endpoint will provide the list of all affirmations generated by a Instruction', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Instruction whose affirmations are to be fetched', - type: 'string', - example: '123', - }) - @ApiQuery({ - name: 'size', - description: 'The number of affirmations to be fetched', - type: 'string', - required: false, - example: '10', - }) - @ApiQuery({ - name: 'start', - description: 'Start index from which affirmations are to be fetched', - type: 'string', - required: false, - }) - @ApiArrayResponse(InstructionAffirmationModel, { - description: 'List of all affirmations related to the target Identity and their current status', - paginated: true, - }) - @Get('instructions/:id/affirmations') - public async getAffirmations( - @Param() { id }: IdParamsDto, - @Query() { size, start }: PaginatedParamsDto - ): Promise> { - const { data, count, next } = await this.settlementsService.findAffirmations( - id, - size, - start?.toString() - ); - return new PaginatedResultsModel({ - results: data?.map( - ({ identity, status }) => - new InstructionAffirmationModel({ - identity, - status, - }) - ), - total: count, - next, - }); - } - - @ApiTags('venues') - @ApiOperation({ - summary: 'Fetch details of a Venue', - description: 'This endpoint will provide the basic details of a Venue', - }) - @ApiParam({ - name: 'id', - description: 'The ID of the Venue whose details are to be fetched', - type: 'string', - example: '123', - }) - @ApiOkResponse({ - description: 'Details of the Venue', - type: VenueDetailsModel, - }) - @Get('venues/:id') - public async getVenueDetails(@Param() { id }: IdParamsDto): Promise { - const venueDetails = await this.settlementsService.findVenueDetails(id); - return new VenueDetailsModel(venueDetails); - } - - @ApiTags('venues') - @ApiOperation({ - summary: 'Create a Venue', - description: 'This endpoint creates a new Venue', - }) - @ApiTransactionResponse({ - description: 'Details about the newly created Venue', - type: CreatedVenueModel, - }) - @Post('/venues/create') - public async createVenue( - @Body() createVenueDto: CreateVenueDto - ): Promise { - const serviceResult = await this.settlementsService.createVenue(createVenueDto); - - const resolver: TransactionResolver = ({ result: venue, transactions, details }) => - new CreatedVenueModel({ - venue, - details, - transactions, - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiTags('venues') - @ApiParam({ - type: 'string', - name: 'id', - }) - @ApiOperation({ - summary: "Modify a venue's details", - }) - @Post('venues/:id/modify') - public async modifyVenue( - @Param() { id }: IdParamsDto, - @Body() modifyVenueDto: ModifyVenueDto - ): Promise { - const serviceResult = await this.settlementsService.modifyVenue(id, modifyVenueDto); - return handleServiceResult(serviceResult); - } - - @ApiTags('assets') - @ApiOperation({ - summary: 'Check if a Leg meets the transfer requirements', - description: 'This endpoint will provide transfer breakdown of an Asset transfer', - }) - @ApiOkResponse({ - description: - 'Breakdown of every requirement that must be fulfilled for an Asset transfer to be executed successfully, and whether said requirement is met or not', - type: TransferBreakdownModel, - }) - @Get('leg-validations') - public async validateLeg( - @Query() - { asset, amount, nfts, fromDid, fromPortfolio, toDid, toPortfolio }: LegValidationParamsDto - ): Promise { - const fromPortfolioLike = new PortfolioDto({ - did: fromDid, - id: fromPortfolio, - }).toPortfolioLike(); - const toPortfolioLike = new PortfolioDto({ did: toDid, id: toPortfolio }).toPortfolioLike(); - - const transferBreakdown = await this.settlementsService.canTransfer( - fromPortfolioLike, - toPortfolioLike, - asset, - amount, - nfts - ); - - return new TransferBreakdownModel(transferBreakdown); - } -} diff --git a/src/settlements/settlements.module.ts b/src/settlements/settlements.module.ts deleted file mode 100644 index 1c666b41..00000000 --- a/src/settlements/settlements.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { forwardRef, Module } from '@nestjs/common'; - -import { AssetsModule } from '~/assets/assets.module'; -import { IdentitiesModule } from '~/identities/identities.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SettlementsController } from '~/settlements/settlements.controller'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [ - TransactionsModule, - forwardRef(() => IdentitiesModule), - PolymeshModule, - forwardRef(() => AssetsModule), - ], - providers: [SettlementsService], - exports: [SettlementsService], - controllers: [SettlementsController], -}) -export class SettlementsModule {} diff --git a/src/settlements/settlements.service.spec.ts b/src/settlements/settlements.service.spec.ts deleted file mode 100644 index e54cf591..00000000 --- a/src/settlements/settlements.service.spec.ts +++ /dev/null @@ -1,457 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AffirmationStatus, - TransferError, - TxTags, - VenueType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { IdentitiesService } from '~/identities/identities.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { PortfolioDto } from '~/portfolios/dto/portfolio.dto'; -import { SettlementsService } from '~/settlements/settlements.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAsset, - MockIdentity, - MockInstruction, - MockPolymesh, - MockTransaction, - MockVenue, -} from '~/test-utils/mocks'; -import { - MockAssetService, - MockIdentitiesService, - mockTransactionsProvider, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -const { signer, did } = testValues; - -describe('SettlementsService', () => { - let service: SettlementsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - - const mockIdentitiesService = new MockIdentitiesService(); - - const mockAssetsService = new MockAssetService(); - - const mockTransactionsService = mockTransactionsProvider.useValue; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [SettlementsService, AssetsService, IdentitiesService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(IdentitiesService) - .useValue(mockIdentitiesService) - .overrideProvider(AssetsService) - .useValue(mockAssetsService) - .compile(); - - service = module.get(SettlementsService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findPendingInstructionsByDid', () => { - it('should return a list of pending instructions', async () => { - const mockIdentity = new MockIdentity(); - mockIdentitiesService.findOne.mockReturnValue(mockIdentity); - - const mockInstructions = { - pending: [{ id: new BigNumber(1) }, { id: new BigNumber(2) }, { id: new BigNumber(3) }], - }; - - mockIdentity.getInstructions.mockResolvedValue(mockInstructions); - - const result = await service.findGroupedInstructionsByDid('0x01'); - - expect(result).toEqual(mockInstructions); - }); - }); - - describe('findInstruction', () => { - it('should return the Instruction entity for a given ID', async () => { - const mockInstruction = new MockInstruction(); - mockPolymeshApi.settlements.getInstruction.mockResolvedValue(mockInstruction); - const result = await service.findInstruction(new BigNumber(123)); - expect(result).toEqual(mockInstruction); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.settlements.getInstruction.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findInstruction(new BigNumber(123))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('findVenue', () => { - it('should return the Venue entity for a given ID', async () => { - const mockVenue = new MockVenue(); - mockPolymeshApi.settlements.getVenue.mockResolvedValue(mockVenue); - const result = await service.findVenue(new BigNumber(123)); - expect(result).toEqual(mockVenue); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockPolymeshApi.settlements.getVenue.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.findVenue(new BigNumber(123))).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); - - describe('createInstruction', () => { - it('should run an addInstruction procedure and return the queue data', async () => { - const mockVenue = new MockVenue(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.AddInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - const mockInstruction = 'instruction'; - mockTransactionsService.submit.mockResolvedValue({ - result: mockInstruction, - transactions: [mockTransaction], - }); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const params = { - legs: [ - { - from: new PortfolioDto({ did: 'fromDid', id: new BigNumber(0) }), - to: new PortfolioDto({ did: 'toDid', id: new BigNumber(1) }), - amount: new BigNumber(100), - asset: 'FAKE_TICKER', - }, - ], - }; - - const body = { - signer, - ...params, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await service.createInstruction(new BigNumber(123), body as any); - - expect(result).toEqual({ - result: mockInstruction, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockVenue.addInstruction, - { - legs: [ - { - from: 'fromDid', - to: { identity: 'toDid', id: new BigNumber(1) }, - amount: new BigNumber(100), - asset: 'FAKE_TICKER', - }, - ], - }, - { signer } - ); - }); - }); - - describe('createVenue', () => { - it('should run a createVenue procedure and return the queue data', async () => { - const mockIdentity = new MockIdentity(); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.CreateVenue, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: undefined, - transactions: [mockTransaction], - }); - mockPolymeshApi.settlements.createVenue.mockResolvedValue(mockTransaction); - mockIdentitiesService.findOne.mockResolvedValue(mockIdentity); - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await service.createVenue(body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.settlements.createVenue, - { description: body.description, type: body.type }, - { signer } - ); - }); - }); - - describe('modifyVenue', () => { - it('should run a modify procedure and return the queue data', async () => { - const mockVenue = new MockVenue(); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.UpdateVenueType, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const body = { - signer, - description: 'A generic exchange', - type: VenueType.Exchange, - }; - - const result = await service.modifyVenue(new BigNumber(123), body); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockVenue.modify, - { description: body.description, type: body.type }, - { signer } - ); - }); - }); - - describe('affirmInstruction', () => { - it('should run an affirm procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.AffirmInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const body = { - signer, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await service.affirmInstruction(new BigNumber(123), body as any); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.affirm, - {}, - { signer } - ); - }); - }); - - describe('rejectInstruction', () => { - it('should run a reject procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.RejectInstruction, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.rejectInstruction(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.reject, - {}, - { signer } - ); - }); - }); - - describe('findVenueDetails', () => { - it('should return the Venue details', async () => { - const mockDetails = { - owner: { - did, - }, - description: 'Venue desc', - type: VenueType.Distribution, - }; - const mockVenue = new MockVenue(); - mockVenue.details.mockResolvedValue(mockDetails); - - const findVenueSpy = jest.spyOn(service, 'findVenue'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findVenueSpy.mockResolvedValue(mockVenue as any); - - const result = await service.findVenueDetails(new BigNumber(123)); - - expect(result).toEqual(mockDetails); - }); - }); - - describe('findAffirmations', () => { - it('should return a list of affirmations for an Instruction', async () => { - const mockAffirmations = { - data: [ - { - identity: { - did, - }, - status: AffirmationStatus.Pending, - }, - ], - next: null, - }; - - const mockInstruction = new MockInstruction(); - mockInstruction.getAffirmations.mockResolvedValue(mockAffirmations); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.findAffirmations(new BigNumber(123), new BigNumber(10)); - - expect(result).toEqual(mockAffirmations); - }); - }); - - describe('canTransfer', () => { - it('should return if Asset transfer is possible ', async () => { - const mockTransferBreakdown = { - general: [TransferError.SelfTransfer, TransferError.ScopeClaimMissing], - compliance: { - requirements: [], - complies: false, - }, - restrictions: [], - result: false, - }; - - const mockAsset = new MockAsset(); - mockAsset.settlements.canTransfer.mockResolvedValue(mockTransferBreakdown); - - mockAssetsService.findOne.mockResolvedValue(mockAsset); - - const result = await service.canTransfer( - new PortfolioDto({ did: 'fromDid', id: new BigNumber(1) }).toPortfolioLike(), - new PortfolioDto({ did: 'toDid', id: new BigNumber(2) }).toPortfolioLike(), - 'TICKER', - new BigNumber(123) - ); - - expect(result).toEqual(mockTransferBreakdown); - }); - }); - - describe('withdrawAffirmation', () => { - it('should run a withdraw affirmation procedure and return the queue data', async () => { - const mockInstruction = new MockInstruction(); - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.settlement.WithdrawAffirmation, - }; - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ transactions: [mockTransaction] }); - - const findInstructionSpy = jest.spyOn(service, 'findInstruction'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findInstructionSpy.mockResolvedValue(mockInstruction as any); - - const result = await service.withdrawAffirmation(new BigNumber(123), { - signer, - }); - - expect(result).toEqual({ - result: undefined, - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockInstruction.withdraw, - {}, - { signer } - ); - }); - }); -}); diff --git a/src/settlements/settlements.service.ts b/src/settlements/settlements.service.ts deleted file mode 100644 index b118bc92..00000000 --- a/src/settlements/settlements.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - GroupedInstructions, - Instruction, - InstructionAffirmation, - InstructionLeg, - PortfolioLike, - ResultSet, - TransferBreakdown, - Venue, - VenueDetails, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AssetsService } from '~/assets/assets.service'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { IdentitiesService } from '~/identities/identities.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { CreateInstructionDto } from '~/settlements/dto/create-instruction.dto'; -import { CreateVenueDto } from '~/settlements/dto/create-venue.dto'; -import { ModifyVenueDto } from '~/settlements/dto/modify-venue.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class SettlementsService { - constructor( - private readonly identitiesService: IdentitiesService, - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly assetsService: AssetsService - ) {} - - public async findGroupedInstructionsByDid(did: string): Promise { - const identity = await this.identitiesService.findOne(did); - - return identity.getInstructions(); - } - - public async findInstruction(id: BigNumber): Promise { - return await this.polymeshService.polymeshApi.settlements - .getInstruction({ - id, - }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async createInstruction( - venueId: BigNumber, - createInstructionDto: CreateInstructionDto - ): ServiceReturn { - const { base, args } = extractTxBase(createInstructionDto); - const venue = await this.findVenue(venueId); - - const params = { - ...args, - legs: args.legs.map( - ({ amount, nfts, asset, from, to }) => - ({ - amount, - nfts, - asset, - from: from.toPortfolioLike(), - to: to.toPortfolioLike(), - } as InstructionLeg) - ), - }; - - return this.transactionsService.submit(venue.addInstruction, params, base); - } - - public async affirmInstruction( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.affirm, {}, signerDto); - } - - public async rejectInstruction( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.reject, {}, signerDto); - } - - public async findVenuesByOwner(did: string): Promise { - const identity = await this.identitiesService.findOne(did); - return identity.getVenues(); - } - - public async findVenue(id: BigNumber): Promise { - return await this.polymeshService.polymeshApi.settlements - .getVenue({ - id, - }) - .catch(error => { - throw handleSdkError(error); - }); - } - - public async findVenueDetails(id: BigNumber): Promise { - const venue = await this.findVenue(id); - - return venue.details(); - } - - public async findAffirmations( - id: BigNumber, - size: BigNumber, - start?: string - ): Promise> { - const instruction = await this.findInstruction(id); - - return instruction.getAffirmations({ size, start }); - } - - public async createVenue(createVenueDto: CreateVenueDto): ServiceReturn { - const { base, args } = extractTxBase(createVenueDto); - - const method = this.polymeshService.polymeshApi.settlements.createVenue; - return this.transactionsService.submit(method, args, base); - } - - public async modifyVenue( - venueId: BigNumber, - modifyVenueDto: ModifyVenueDto - ): ServiceReturn { - const { base, args } = extractTxBase(modifyVenueDto); - const venue = await this.findVenue(venueId); - - return this.transactionsService.submit(venue.modify, args as Required, base); - } - - public async canTransfer( - from: PortfolioLike, - to: PortfolioLike, - ticker: string, - transferAmount?: BigNumber, - transferNfts?: BigNumber[] - ): Promise { - const assetDetails = await this.assetsService.findOne(ticker); - const amount = transferAmount ?? new BigNumber(0); - const nfts = transferNfts ?? []; - return assetDetails.settlements.canTransfer({ from, to, amount, nfts }); - } - - public async withdrawAffirmation( - id: BigNumber, - signerDto: TransactionBaseDto - ): ServiceReturn { - const instruction = await this.findInstruction(id); - - return this.transactionsService.submit(instruction.withdraw, {}, signerDto); - } -} diff --git a/src/settlements/settlements.util.ts b/src/settlements/settlements.util.ts deleted file mode 100644 index 50633ba2..00000000 --- a/src/settlements/settlements.util.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - Instruction, - InstructionStatus, - InstructionType, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { EventIdentifierModel } from '~/common/models/event-identifier.model'; -import { isFungibleLeg, isNftLeg } from '~/common/utils'; -import { createPortfolioIdentifierModel } from '~/portfolios/portfolios.util'; -import { InstructionModel } from '~/settlements/models/instruction.model'; -import { LegModel } from '~/settlements/models/leg.model'; - -export async function createInstructionModel(instruction: Instruction): Promise { - const [details, legsResultSet, instructionStatus] = await Promise.all([ - instruction.details(), - instruction.getLegs(), - instruction.getStatus(), - ]); - - const { status, createdAt, tradeDate, valueDate, venue, type, memo } = details; - - const legs = legsResultSet.data - ?.map(leg => { - const { from: legFrom, to: legTo, asset } = leg; - const from = createPortfolioIdentifierModel(legFrom); - const to = createPortfolioIdentifierModel(legTo); - - if (isFungibleLeg(leg)) { - console.log('is fungible'); - const { amount } = leg; - return new LegModel({ - asset, - from, - to, - amount, - }); - } else if (isNftLeg(leg)) { - console.log('is nft'); - const { nfts } = leg; - - return new LegModel({ - asset, - from, - to, - nfts: nfts.map(({ id }) => id), - }); - } - - return null; - }) - .filter(leg => !!leg) as LegModel[]; // filters out "off chain" legs, in case they were used - - let instructionModelParams: ConstructorParameters[0] = { - status, - createdAt, - venue, - type, - legs: legs || [], - }; - - if (valueDate !== null) { - instructionModelParams = { ...instructionModelParams, valueDate }; - } - - if (tradeDate !== null) { - instructionModelParams = { ...instructionModelParams, tradeDate }; - } - - if (memo !== null) { - instructionModelParams = { ...instructionModelParams, memo }; - } - - if (details.type === InstructionType.SettleOnBlock) { - instructionModelParams = { ...instructionModelParams, endBlock: details.endBlock }; - } - - if (instructionStatus.status !== InstructionStatus.Pending) { - instructionModelParams = { - ...instructionModelParams, - eventIdentifier: new EventIdentifierModel(instructionStatus.eventIdentifier), - }; - } - - return new InstructionModel(instructionModelParams); -} diff --git a/src/signing/config/signers.config.ts b/src/signing/config/signers.config.ts deleted file mode 100644 index 5aec94e9..00000000 --- a/src/signing/config/signers.config.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; -import { readFileSync } from 'fs'; - -export default registerAs('signer-accounts', () => { - const { - LOCAL_SIGNERS, - LOCAL_MNEMONICS, - VAULT_URL, - VAULT_TOKEN, - FIREBLOCKS_URL, - FIREBLOCKS_API_KEY, - FIREBLOCKS_SECRET_PATH, - } = process.env; - - if (VAULT_URL && VAULT_TOKEN) { - const vault = { - url: VAULT_URL, - token: VAULT_TOKEN, - }; - return { vault }; - } - - if (FIREBLOCKS_URL && FIREBLOCKS_API_KEY && FIREBLOCKS_SECRET_PATH) { - const secret = readFileSync(FIREBLOCKS_SECRET_PATH, 'utf8'); - const fireblocks = { - url: FIREBLOCKS_URL, - apiKey: FIREBLOCKS_API_KEY, - secret, - }; - - return { fireblocks }; - } - - const signers = LOCAL_SIGNERS?.split(',').map(d => d.trim()) || []; - const mnemonics = LOCAL_MNEMONICS?.split(',').map(m => m.trim()) || []; - - const accounts: Record = {}; - - signers.forEach((signer, index) => { - accounts[signer] = mnemonics[index]; - }); - - return { - local: accounts, - }; -}); diff --git a/src/signing/dto/signer-details.dto.ts b/src/signing/dto/signer-details.dto.ts deleted file mode 100644 index 78606705..00000000 --- a/src/signing/dto/signer-details.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ - -import { IsString } from 'class-validator'; - -export class SignerDetailsDto { - @IsString() - readonly signer: string; -} diff --git a/src/signing/models/signer.model.ts b/src/signing/models/signer.model.ts deleted file mode 100644 index d72434fd..00000000 --- a/src/signing/models/signer.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class SignerModel { - @ApiProperty({ - type: 'string', - description: 'The address associated to the signer', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - readonly address: string; - - constructor(model: SignerModel) { - Object.assign(this, model); - } -} diff --git a/src/signing/services/fireblocks-signing.service.spec.ts b/src/signing/services/fireblocks-signing.service.spec.ts deleted file mode 100644 index 4650f77d..00000000 --- a/src/signing/services/fireblocks-signing.service.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { DerivationPath } from '@polymeshassociation/fireblocks-signing-manager/lib/fireblocks'; - -import { AppValidationError } from '~/common/errors'; -import { LoggerModule } from '~/logger/logger.module'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { FireblocksSigningService } from '~/signing/services'; -import { MockFireblocksSigningManager } from '~/signing/signing.mock'; -import { SigningModule } from '~/signing/signing.module'; -import { testAccount } from '~/test-utils/consts'; -import { MockPolymesh } from '~/test-utils/mocks'; - -describe('FireblocksSigningService', () => { - let service: FireblocksSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let manager: MockFireblocksSigningManager; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule, LoggerModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - manager = new MockFireblocksSigningManager(); - - service = new FireblocksSigningService( - manager as unknown as FireblocksSigningManager, - polymeshService, - logger - ); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('getAddressByHandle', () => { - const { address } = testAccount; - const mockDeriveResponse = { - publicKey: '01000', - address, - status: 0, - algorithm: 'TEST-ALGO', - derivationPath: [44, 1, 0, 0, 0] as DerivationPath, - }; - - it('should return the address associated to the derivation path', async () => { - const handle = '1-2-3'; - const expectedDerivationPath = [44, 1, 1, 2, 3] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - - const result = await service.getAddressByHandle(handle); - - expect(result).toEqual(address); - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should default non specified sections to 0 for the derivation path', async () => { - const handle = '1'; - const expectedDerivationPath = [44, 1, 1, 0, 0] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - - await service.getAddressByHandle(handle); - - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should infer POLYX BIP-44 path from the ss58Format', async () => { - const handle = '0'; - - const expectedDerivationPath = [44, 595, 0, 0, 0] as DerivationPath; - - manager.deriveAccount.mockResolvedValue(mockDeriveResponse); - manager.ss58Format = 12; - - await service.getAddressByHandle(handle); - - expect(manager.deriveAccount).toHaveBeenCalledWith(expectedDerivationPath); - }); - - it('should error if given an invalid signer', async () => { - const invalidSigners = ['aaa-bbb-ccc', '', '1-2-3-4', '0-a-1', '0--1-2']; - - const expectedError = new AppValidationError( - 'Fireblocks `signer` field should be 3 integers formatted like: `x-y-z`' - ); - - for (const signer of invalidSigners) { - await expect(service.getAddressByHandle(signer)).rejects.toThrow(expectedError); - } - }); - }); -}); diff --git a/src/signing/services/fireblocks-signing.service.ts b/src/signing/services/fireblocks-signing.service.ts deleted file mode 100644 index 3facf420..00000000 --- a/src/signing/services/fireblocks-signing.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { DerivationPath } from '@polymeshassociation/fireblocks-signing-manager/lib/fireblocks'; - -import { AppValidationError } from '~/common/errors'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services'; -import { determineBip44CoinType } from '~/signing/services/util'; - -export class FireblocksSigningService extends SigningService { - constructor( - protected readonly signingManager: FireblocksSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(FireblocksSigningService.name); - } - - public async getAddressByHandle(handle: string): Promise { - const derivePath = this.handleToDerivationPath(handle); - - const key = await this.signingManager.deriveAccount(derivePath); - return key.address; - } - - private handleToDerivationPath(handle: string): DerivationPath { - const sections = handle.split('-').map(Number); - - if (sections.some(isNaN) || sections.length > 3 || handle === '') { - throw new AppValidationError( - 'Fireblocks `signer` field should be 3 integers formatted like: `x-y-z`' - ); - } - - const coinType = determineBip44CoinType(this.signingManager.ss58Format); - - const [accountId, change, accountIndex] = sections; - - return [44, coinType, accountId, change || 0, accountIndex || 0]; - } -} diff --git a/src/signing/services/index.ts b/src/signing/services/index.ts deleted file mode 100644 index 7ae4f287..00000000 --- a/src/signing/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from '~/signing/services/local-signing.service'; -export * from '~/signing/services/signing.service'; -export * from '~/signing/services/fireblocks-signing.service'; -export * from '~/signing/services/vault-signing.service'; diff --git a/src/signing/services/local-signing.service.spec.ts b/src/signing/services/local-signing.service.spec.ts deleted file mode 100644 index 31489e4a..00000000 --- a/src/signing/services/local-signing.service.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; - -import { AppNotFoundError } from '~/common/errors'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { LocalSigningService } from '~/signing/services/local-signing.service'; -import { SigningModule } from '~/signing/signing.module'; -import { MockPolymesh } from '~/test-utils/mocks'; - -describe('LocalSigningService', () => { - let service: LocalSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - const manager = await LocalSigningManager.create({ accounts: [] }); - manager.setSs58Format(0); - - service = new LocalSigningService(manager, polymeshService, logger); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('initialize', () => { - it('should call polymeshApi setSigningManager method', async () => { - await service.initialize(); - expect(mockPolymeshApi.setSigningManager).toHaveBeenCalled(); - }); - - it('should call setAddressByHandle for each account', async () => { - const spy = jest.spyOn(service, 'setAddressByHandle'); - await service.initialize({ Alice: '//Alice', Bob: '//Bob' }); - expect(spy).toHaveBeenCalledWith('Alice', '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5'); - expect(spy).toHaveBeenCalledWith('Bob', '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'); - spy.mockRestore(); - }); - }); - - describe('getAddressByHandle', () => { - it('should get a loaded Account from the address book', () => { - service.setAddressByHandle('humanId', 'someAddress'); - return expect(service.getAddressByHandle('humanId')).resolves.toEqual('someAddress'); - }); - - it('should throw if an Account is not loaded', () => { - expect(() => service.getAddressByHandle('badId')).toThrow(AppNotFoundError); - }); - }); -}); diff --git a/src/signing/services/local-signing.service.ts b/src/signing/services/local-signing.service.ts deleted file mode 100644 index 9e49ccb6..00000000 --- a/src/signing/services/local-signing.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; -import { forEach } from 'lodash'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services/signing.service'; - -export class LocalSigningService extends SigningService { - private addressBook: Record = {}; - - constructor( - protected readonly signingManager: LocalSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(LocalSigningService.name); - } - - public getAddressByHandle(handle: string): Promise { - const address = this.addressBook[handle]; - - if (!address) { - this.throwNoSigner(handle); - } - - return Promise.resolve(address); - } - - public setAddressByHandle(handle: string, address: string): void { - this.addressBook[handle] = address; - } - - public override async initialize(accounts: Record = {}): Promise { - await super.initialize(); - - forEach(accounts, (mnemonic, handle) => { - const address = this.signingManager.addAccount({ mnemonic }); - this.setAddressByHandle(handle, address); - this.logKey(handle, address); - }); - } - - private logKey(handle: string, address: string): void { - this.logger.log(`Key "${handle}" with address "${address}" was loaded`); - } -} diff --git a/src/signing/services/signing.service.ts b/src/signing/services/signing.service.ts deleted file mode 100644 index 51cb43f7..00000000 --- a/src/signing/services/signing.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SigningManager } from '@polymeshassociation/signing-manager-types'; - -import { AppNotFoundError } from '~/common/errors'; -import { PolymeshService } from '~/polymesh/polymesh.service'; - -@Injectable() -export abstract class SigningService { - protected readonly signingManager: SigningManager; - protected readonly polymeshService: PolymeshService; - - public abstract getAddressByHandle(handle: string): Promise; - - public async initialize(): Promise { - return this.polymeshService.polymeshApi.setSigningManager(this.signingManager); - } - - protected throwNoSigner(handle: string): never { - throw new AppNotFoundError(handle, 'signer'); - } -} diff --git a/src/signing/services/util.ts b/src/signing/services/util.ts deleted file mode 100644 index c2a78856..00000000 --- a/src/signing/services/util.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Mainnet should use `595` as the coinType, otherwise it should be `1` to indicate a test net - * reference: https://github.com/satoshilabs/slips/blob/2a2f4c79508749f7e679a127d5a56da079b8d2d8/slip-0044.md?plain=1#L32 - */ -export const determineBip44CoinType = (ss58Format: number): 595 | 1 => { - return ss58Format === 12 ? 595 : 1; -}; diff --git a/src/signing/services/vault-signing.service.spec.ts b/src/signing/services/vault-signing.service.spec.ts deleted file mode 100644 index bfc6fb3f..00000000 --- a/src/signing/services/vault-signing.service.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { AppNotFoundError } from '~/common/errors'; -import { LoggerModule } from '~/logger/logger.module'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { VaultSigningService } from '~/signing/services/vault-signing.service'; -import { MockHashicorpVaultSigningManager } from '~/signing/signing.mock'; -import { SigningModule } from '~/signing/signing.module'; -import { MockPolymesh } from '~/test-utils/mocks'; - -describe('VaultSigningService', () => { - let service: VaultSigningService; - let logger: PolymeshLogger; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let manager: MockHashicorpVaultSigningManager; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule, SigningModule, LoggerModule], - providers: [mockPolymeshLoggerProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - logger = mockPolymeshLoggerProvider.useValue as unknown as PolymeshLogger; - polymeshService = module.get(PolymeshService); - manager = new MockHashicorpVaultSigningManager(); - - manager.setSs58Format(0); - - const castedManager = manager as unknown as HashicorpVaultSigningManager; - - service = new VaultSigningService(castedManager, polymeshService, logger); - - manager.getVaultKeys.mockResolvedValue([ - { - name: 'alice', - address: 'ABC', - publicKey: '0x123', - version: 1, - }, - { - name: 'bob', - address: 'DEF', - publicKey: '0x456', - version: 1, - }, - { - name: 'bob', - address: 'GHI', - publicKey: '0x456', - version: 2, - }, - ]); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - describe('initialize', () => { - it('should call logKey for each account', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const logKeySpy = jest.spyOn(service as any, 'logKey'); // spy on private method - - await service.initialize(); - expect(logKeySpy).toHaveBeenCalledWith('alice-1', 'ABC'); - expect(logKeySpy).toHaveBeenCalledWith('bob-1', 'DEF'); - expect(logKeySpy).toHaveBeenCalledWith('bob-2', 'GHI'); - }); - }); - - describe('getAddressByKey', () => { - it('should check for the key in vault', () => { - return expect(service.getAddressByHandle('alice-1')).resolves.toEqual('ABC'); - }); - - it('should throw if an Account is not found', () => { - return expect(service.getAddressByHandle('badId')).rejects.toBeInstanceOf(AppNotFoundError); - }); - }); -}); diff --git a/src/signing/services/vault-signing.service.ts b/src/signing/services/vault-signing.service.ts deleted file mode 100644 index 07938adb..00000000 --- a/src/signing/services/vault-signing.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { SigningService } from '~/signing/services/signing.service'; - -export class VaultSigningService extends SigningService { - constructor( - protected readonly signingManager: HashicorpVaultSigningManager, - protected readonly polymeshService: PolymeshService, - private readonly logger: PolymeshLogger - ) { - super(); - this.logger.setContext(VaultSigningService.name); - } - - public override async initialize(): Promise { - await super.initialize(); - return this.logKeys(); - } - - public async getAddressByHandle(handle: string): Promise { - const keys = await this.signingManager.getVaultKeys(); - - const key = keys.find(({ name, version }) => `${name}-${version}` === handle); - if (key) { - this.logKey(handle, key.address); - return key.address; - } else { - this.throwNoSigner(handle); - } - } - - public async logKeys(): Promise { - const keys = await this.signingManager.getVaultKeys(); - - keys.forEach(({ name, version, address }) => { - const keyName = `${name}-${version}`; - this.logKey(keyName, address); - }); - } - - private logKey(handle: string, address: string): void { - this.logger.log(`Key "${handle}" with address "${address}" was loaded`); - } -} diff --git a/src/signing/signing.controller.spec.ts b/src/signing/signing.controller.spec.ts deleted file mode 100644 index 4c237510..00000000 --- a/src/signing/signing.controller.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { SignerModel } from '~/signing/models/signer.model'; -import { SigningController } from '~/signing/signing.controller'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { testValues } from '~/test-utils/consts'; - -describe('SigningController', () => { - const signingService = mockSigningProvider.useValue; - const { - testAccount: { address }, - signer, - } = testValues; - let controller: SigningController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SigningController], - providers: [mockSigningProvider], - }).compile(); - controller = module.get(SigningController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getSignerInfo', () => { - it('should call the service and return the result', () => { - const expectedResult = new SignerModel({ address }); - when(signingService.getAddressByHandle).calledWith(signer).mockResolvedValue(address); - return expect(controller.getSignerAddress({ signer })).resolves.toEqual(expectedResult); - }); - }); -}); diff --git a/src/signing/signing.controller.ts b/src/signing/signing.controller.ts deleted file mode 100644 index 0aa3df38..00000000 --- a/src/signing/signing.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; - -import { SignerDetailsDto } from '~/signing/dto/signer-details.dto'; -import { SignerModel } from '~/signing/models/signer.model'; -import { SigningService } from '~/signing/services'; - -@ApiTags('signer') -@Controller('signer') -export class SigningController { - constructor(private readonly signingService: SigningService) {} - - @ApiOperation({ - summary: 'Fetch signer details', - description: 'This endpoint provides information associated with a particular `signer`', - }) - @ApiParam({ - name: 'signer', - description: - 'The value of the `signer` to fetch the address for. Note, the format depends on the signing manager the API is configured with. A Fireblocks signer is up to three numbers like `x-y-z`, Vault will be `{name}-{version}`, while a Local signer can be any string, like `alice`', - type: 'string', - example: 'alice', - }) - @ApiNotFoundResponse({ - description: 'The signer was not found', - }) - @ApiBadRequestResponse({ - description: 'The signer did not have the proper format for the given signing manager', - }) - @ApiOkResponse({ - description: 'Information about the address associated to the signer', - type: SignerModel, - }) - @Get('/:signer') - public async getSignerAddress(@Param() { signer }: SignerDetailsDto): Promise { - const address = await this.signingService.getAddressByHandle(signer); - - return new SignerModel({ address }); - } -} diff --git a/src/signing/signing.mock.ts b/src/signing/signing.mock.ts deleted file mode 100644 index f0e9a493..00000000 --- a/src/signing/signing.mock.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; - -import { SigningService } from '~/signing/services'; -import { MockSigningService } from '~/test-utils/service-mocks'; - -/** - * provides a mock HashicorpVaultSigningManager for testing - */ -export class MockHashicorpVaultSigningManager { - externalSigner = jest.fn(); - getVaultKeys = jest.fn(); - getExternalSigner = jest.fn(); - getSs58Format = jest.fn(); - setSs58Format = jest.fn(); - getAccounts = jest.fn(); - vault = jest.fn(); -} - -Object.setPrototypeOf(MockHashicorpVaultSigningManager, HashicorpVaultSigningManager.prototype); // Lets mock pass `instanceof` checks - -export class MockFireblocksSigningManager { - externalSigner = jest.fn(); - getExternalSigner = jest.fn(); - setSs58Format = jest.fn(); - getAccounts = jest.fn(); - deriveAccount = jest.fn(); - fireblocksClient = jest.fn(); - ss58Format = 42; -} - -Object.setPrototypeOf(MockFireblocksSigningManager, FireblocksSigningManager.prototype); // Lets mock pass `instanceof` checks - -export const mockSigningProvider = { - provide: SigningService, - useValue: new MockSigningService(), -}; diff --git a/src/signing/signing.module.ts b/src/signing/signing.module.ts deleted file mode 100644 index 2ce7e328..00000000 --- a/src/signing/signing.module.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigType } from '@nestjs/config'; -import { FireblocksSigningManager } from '@polymeshassociation/fireblocks-signing-manager'; -import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager'; -import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'; - -import { LoggerModule } from '~/logger/logger.module'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import signersConfig from '~/signing/config/signers.config'; -import { LocalSigningService, SigningService } from '~/signing/services'; -import { FireblocksSigningService } from '~/signing/services/fireblocks-signing.service'; -import { VaultSigningService } from '~/signing/services/vault-signing.service'; -import { SigningController } from '~/signing/signing.controller'; - -@Module({ - imports: [ConfigModule.forFeature(signersConfig), PolymeshModule, LoggerModule], - providers: [ - { - provide: SigningService, - inject: [PolymeshService, signersConfig.KEY, PolymeshLogger], - useFactory: async ( - polymeshService: PolymeshService, - configuration: ConfigType, - logger: PolymeshLogger - ): Promise => { - let service; - const { vault, local, fireblocks } = configuration; - if (vault) { - const manager = new HashicorpVaultSigningManager(vault); - service = new VaultSigningService(manager, polymeshService, logger); - await service.initialize(); - } else if (fireblocks) { - const manager = await FireblocksSigningManager.create(fireblocks); - service = new FireblocksSigningService(manager, polymeshService, logger); - await service.initialize(); - } else { - const manager = await LocalSigningManager.create({ accounts: [] }); - service = new LocalSigningService(manager, polymeshService, logger); - await service.initialize(local); - } - return service; - }, - }, - ], - exports: [SigningService], - controllers: [SigningController], -}) -export class SigningModule {} diff --git a/src/subscriptions/config/subscriptions.config.ts b/src/subscriptions/config/subscriptions.config.ts deleted file mode 100644 index a9f70717..00000000 --- a/src/subscriptions/config/subscriptions.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ - -import { registerAs } from '@nestjs/config'; - -export default registerAs('subscriptions', () => { - const { - SUBSCRIPTIONS_TTL, - SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES, - SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL, - } = process.env; - - return { - ttl: Number(SUBSCRIPTIONS_TTL), - maxTries: Number(SUBSCRIPTIONS_MAX_HANDSHAKE_TRIES), - retryInterval: Number(SUBSCRIPTIONS_HANDSHAKE_RETRY_INTERVAL), - }; -}); diff --git a/src/subscriptions/entities/subscription.entity.ts b/src/subscriptions/entities/subscription.entity.ts deleted file mode 100644 index 8ad7c180..00000000 --- a/src/subscriptions/entities/subscription.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { EventType } from '~/events/types'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -export class SubscriptionEntity { - public id: number; - - public eventType: EventType; - - public eventScope: string; - - public webhookUrl: string; - - public createdAt: Date; - - public ttl: number; - - public status: SubscriptionStatus; - - public triesLeft: number; - - public nextNonce: number; - - /** - * secret for the legitimacy signature. This shared secret is used to - * compute an HMAC of every notification payload being sent to `webhookUrl` and sent - * as part of the request headers. It can be used by the consumer of the subscription - * to verify that messages received by their webhooks are being sent by us - */ - public legitimacySecret: string; - - public isExpired(): boolean { - const { createdAt, ttl } = this; - - return new Date(createdAt.getTime() + ttl) <= new Date(); - } - - constructor(entity: Omit) { - Object.assign(this, entity); - } -} diff --git a/src/subscriptions/subscriptions.consts.ts b/src/subscriptions/subscriptions.consts.ts deleted file mode 100644 index 20e16fa2..00000000 --- a/src/subscriptions/subscriptions.consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const HANDSHAKE_HEADER_KEY = 'x-hook-secret'; diff --git a/src/subscriptions/subscriptions.module.ts b/src/subscriptions/subscriptions.module.ts deleted file mode 100644 index 1aa9ef0e..00000000 --- a/src/subscriptions/subscriptions.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import { LoggerModule } from '~/logger/logger.module'; -import { ScheduleModule } from '~/schedule/schedule.module'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; - -@Module({ - imports: [ConfigModule.forFeature(subscriptionsConfig), ScheduleModule, HttpModule, LoggerModule], - providers: [SubscriptionsService], - exports: [SubscriptionsService], -}) -export class SubscriptionsModule {} diff --git a/src/subscriptions/subscriptions.service.spec.ts b/src/subscriptions/subscriptions.service.spec.ts deleted file mode 100644 index 98873b1a..00000000 --- a/src/subscriptions/subscriptions.service.spec.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* eslint-disable import/first */ -const mockLastValueFrom = jest.fn(); -const mockRandomBytes = jest.fn(); - -import { HttpService } from '@nestjs/axios'; -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppNotFoundError } from '~/common/errors'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { ScheduleService } from '~/schedule/schedule.service'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionEntity } from '~/subscriptions/entities/subscription.entity'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; -import { MockHttpService, MockScheduleService } from '~/test-utils/service-mocks'; - -jest.mock('rxjs', () => ({ - ...jest.requireActual('rxjs'), - lastValueFrom: mockLastValueFrom, -})); -jest.mock('crypto', () => ({ - ...jest.requireActual('crypto'), - randomBytes: mockRandomBytes, -})); - -describe('SubscriptionsService', () => { - let service: SubscriptionsService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let unsafeService: any; - - let mockScheduleService: MockScheduleService; - let mockHttpService: MockHttpService; - - const ttl = 120000; - const maxTries = 5; - const retryInterval = 5000; - const legitimacySecret = 'someSecret'; - - const subs = [ - new SubscriptionEntity({ - id: 1, - eventType: EventType.TransactionUpdate, - eventScope: '0x01', - webhookUrl: 'https://example.com/hook', - createdAt: new Date('10/14/1987'), - ttl, - status: SubscriptionStatus.Done, - triesLeft: maxTries, - nextNonce: 0, - legitimacySecret, - }), - new SubscriptionEntity({ - id: 2, - eventType: EventType.TransactionUpdate, - eventScope: '0x02', - webhookUrl: 'https://example.com/hook', - createdAt: new Date('10/14/1987'), - ttl, - status: SubscriptionStatus.Rejected, - triesLeft: maxTries, - nextNonce: 0, - legitimacySecret, - }), - ]; - - beforeEach(async () => { - mockScheduleService = new MockScheduleService(); - mockHttpService = new MockHttpService(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SubscriptionsService, - ScheduleService, - HttpService, - mockPolymeshLoggerProvider, - { - provide: subscriptionsConfig.KEY, - useValue: { ttl, maxTries, retryInterval }, - }, - ], - }) - .overrideProvider(ScheduleService) - .useValue(mockScheduleService) - .overrideProvider(HttpService) - .useValue(mockHttpService) - .compile(); - - service = module.get(SubscriptionsService); - - unsafeService = service; - unsafeService.subscriptions = { - 1: subs[0], - 2: subs[1], - }; - unsafeService.currentId = 2; - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAll', () => { - it('should return all subscriptions', async () => { - const result = await service.findAll(); - - expect(result).toEqual(subs); - }); - - it('should filter results', async () => { - let result = await service.findAll({ - status: SubscriptionStatus.Done, - }); - - expect(result).toEqual([subs[0]]); - - result = await service.findAll({ - eventScope: '0x02', - }); - - expect(result).toEqual([subs[1]]); - - result = await service.findAll({ - eventType: EventType.TransactionUpdate, - }); - - expect(result).toEqual(subs); - - result = await service.findAll({ - excludeExpired: true, - }); - - expect(result).toEqual([]); - }); - }); - - describe('findOne', () => { - it('should return a single subscription by ID', async () => { - const result = await service.findOne(1); - - expect(result).toEqual(subs[0]); - }); - - it('should throw an error if there is no subscription with the passed id', () => { - return expect(service.findOne(4)).rejects.toThrow(AppNotFoundError); - }); - }); - - describe('createSubscription', () => { - it('should create a subscription and return its ID, and send a handshake to the webhook, retrying if it fails', async () => { - const eventType = EventType.TransactionUpdate; - const eventScope = '0x03'; - const webhookUrl = 'https://www.example.com'; - - const result = await service.createSubscription({ - eventScope, - eventType, - webhookUrl, - legitimacySecret, - }); - - expect(result).toEqual(3); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt: _, ...sub } = await service.findOne(3); - - expect(sub).toEqual({ - id: 3, - ttl, - triesLeft: maxTries, - status: SubscriptionStatus.Inactive, - eventType, - eventScope, - webhookUrl, - nextNonce: 0, - legitimacySecret, - }); - - // ignore expired subs - await unsafeService.sendHandshake(1); - - expect(mockHttpService.post).not.toHaveBeenCalled(); - - const handshakeSecret = 'cGxhY2Vob2xkZXI='; - mockRandomBytes.mockImplementation((_length, callback) => { - callback(undefined, Buffer.from(handshakeSecret, 'base64')); - }); - mockLastValueFrom.mockResolvedValue({ - status: 200, - headers: { - [HANDSHAKE_HEADER_KEY]: handshakeSecret, - }, - }); - - await unsafeService.sendHandshake(3); - - let subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Active); - - mockLastValueFrom.mockResolvedValue({ - status: 500, - }); - - await service.updateSubscription(3, { - status: SubscriptionStatus.Inactive, - }); - - await unsafeService.sendHandshake(3); - - subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Inactive); - - await service.updateSubscription(3, { - status: SubscriptionStatus.Inactive, - triesLeft: 1, - }); - - await unsafeService.sendHandshake(3); - - subscription = await service.findOne(3); - expect(subscription.status).toBe(SubscriptionStatus.Rejected); - }); - }); - - describe('updateSubscription', () => { - it('should update a subscription and return it, ignoring fields other than status or triesLeft', async () => { - const status = SubscriptionStatus.Active; - const triesLeft = 1; - const result = await service.updateSubscription(1, { - status, - triesLeft, - id: 4, - }); - - expect(result.status).toBe(status); - expect(result.triesLeft).toBe(triesLeft); - expect(result.id).toBe(1); - }); - }); - - describe('batchMarkAsDone', () => { - it('should mark a group of subscriptions as done', async () => { - await service.batchMarkAsDone([1, 2]); - - const result = await service.findAll({ status: SubscriptionStatus.Done }); - - expect(result.length).toBe(2); - }); - }); - - describe('batchBumpNonce', () => { - it('should mark a group of subscriptions as done', async () => { - await service.batchBumpNonce([1, 2]); - - const result = await service.findAll(); - - expect(result.every(({ nextNonce }) => nextNonce === 1)); - }); - }); -}); diff --git a/src/subscriptions/subscriptions.service.ts b/src/subscriptions/subscriptions.service.ts deleted file mode 100644 index bdc7cc7a..00000000 --- a/src/subscriptions/subscriptions.service.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; -import { AxiosResponse } from 'axios'; -import { filter, pick } from 'lodash'; -import { lastValueFrom } from 'rxjs'; - -import { AppNotFoundError } from '~/common/errors'; -import { generateBase64Secret } from '~/common/utils'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { ScheduleService } from '~/schedule/schedule.service'; -import subscriptionsConfig from '~/subscriptions/config/subscriptions.config'; -import { SubscriptionEntity } from '~/subscriptions/entities/subscription.entity'; -import { HANDSHAKE_HEADER_KEY } from '~/subscriptions/subscriptions.consts'; -import { SubscriptionStatus } from '~/subscriptions/types'; - -@Injectable() -export class SubscriptionsService { - private subscriptions: Record; - private currentId: number; - - private ttl: number; - private maxTries: number; - private retryInterval: number; - - constructor( - @Inject(subscriptionsConfig.KEY) config: ConfigType, - private readonly scheduleService: ScheduleService, - private readonly httpService: HttpService, - // TODO @polymath-eric: handle errors with specialized service - private readonly logger: PolymeshLogger - ) { - const { ttl, maxTries: triesLeft, retryInterval } = config; - - this.ttl = ttl; - this.maxTries = triesLeft; - this.retryInterval = retryInterval; - - this.subscriptions = {}; - this.currentId = 0; - - logger.setContext(SubscriptionsService.name); - } - - /** - * Fetch all subscriptions. Allows filtering by different parameters and excluding - * expired subscriptions from the result (default behavior is to include them) - */ - public async findAll( - filters: Partial> & { - excludeExpired?: boolean; - } = {} - ): Promise { - const { - status: statusFilter, - eventScope: scopeFilter, - eventType: typeFilter, - excludeExpired = false, - } = filters; - - return filter(this.subscriptions, subscription => { - const { status, eventScope, eventType } = subscription; - - return ( - (!statusFilter || statusFilter === status) && - (!scopeFilter || scopeFilter === eventScope) && - (!typeFilter || typeFilter === eventType) && - (!excludeExpired || !subscription.isExpired()) - ); - }); - } - - public async findOne(id: number): Promise { - const sub = this.subscriptions[id]; - - if (!sub) { - throw new AppNotFoundError('subscription', id.toString()); - } - - return sub; - } - - public async createSubscription( - sub: Pick - ): Promise { - const { subscriptions, ttl, maxTries: triesLeft } = this; - - this.currentId += 1; - const id = this.currentId; - - subscriptions[id] = new SubscriptionEntity({ - id, - ...sub, - createdAt: new Date(), - status: SubscriptionStatus.Inactive, - ttl, - triesLeft, - nextNonce: 0, - }); - - /** - * we add the subscription handshake to the scheduler cycle - */ - this.scheduleSendHandshake(id, 0); - - return id; - } - - /** - * @note ignores any properties other than `status`, `triesLeft` and `nextNonce` - */ - public async updateSubscription( - id: number, - data: Partial - ): Promise { - const { subscriptions } = this; - - const updater = pick(data, 'status', 'triesLeft', 'nextNonce'); - - const current = await this.findOne(id); - - const updated = new SubscriptionEntity({ - ...current, - ...updater, - }); - - subscriptions[id] = updated; - - return updated; - } - - /** - * Change the status of many subscriptions at once to "done" - */ - public async batchMarkAsDone(ids: number[]): Promise { - const { subscriptions } = this; - - ids.forEach(id => { - subscriptions[id].status = SubscriptionStatus.Done; - }); - } - - /** - * Increase the latest nonce of many subscriptions at once by one - */ - public async batchBumpNonce(ids: number[]): Promise { - const { subscriptions } = this; - - ids.forEach(id => { - subscriptions[id].nextNonce += 1; - }); - } - - /** - * Schedule a subscription handshake to be sent after a certain time has elapsed - * - * @param id - subscription ID - * @param ms - amount of milliseconds to wait before sending the handshake - */ - private scheduleSendHandshake(id: number, ms: number = this.retryInterval): void { - this.scheduleService.addTimeout( - this.getTimeoutId(id), - /* istanbul ignore next */ - () => this.sendHandshake(id), - ms - ); - } - - /** - * Generate an identifier for a "send handshake" scheduled task. This is used - * to track scheduled timeouts internally - * - * @param id - subscription ID - */ - private getTimeoutId(id: number): string { - return `sendSubscriptionHandshake_${id}`; - } - - /** - * Attempt to send a handshake request to the subscription URL. The response must have a status - * of 200 and contain the handshake secret in the headers. Otherwise, we schedule a retry - * - * @param id - subscription ID - */ - private async sendHandshake(id: number): Promise { - const subscription = await this.findOne(id); - - if (subscription.isExpired()) { - return; - } - - const { webhookUrl, triesLeft } = subscription; - const { httpService, logger } = this; - - try { - const secret = await generateBase64Secret(32); - - const response = await lastValueFrom( - httpService.post( - webhookUrl, - {}, - { - headers: { - [HANDSHAKE_HEADER_KEY]: secret, - }, - timeout: 10000, - } - ) - ); - - await this.handleHandshakeResponse(id, response, secret); - } catch (err) { - logger.error(`Error while sending handshake for subscription "${id}":`, err); - - await this.retry(id, triesLeft - 1); - } - } - - /** - * Mark the subscription as active if the response status is OK and contains the handshake secret in the - * headers. Otherwise, throw an error - * - * @param id - subscription ID - */ - private async handleHandshakeResponse( - id: number, - response: AxiosResponse, - secret: string - ): Promise { - const { status, headers } = response; - if (status === HttpStatus.OK && headers[HANDSHAKE_HEADER_KEY] === secret) { - await this.updateSubscription(id, { - status: SubscriptionStatus.Active, - }); - - return; - } - - throw new Error('Webhook did not respond with expected handshake'); - } - - /** - * Reschedule a subscription handshake to be sent later - * - * @param id - subscription ID - * @param triesLeft - amount of retries left for the subscription. If none are left, - * the subscription is marked as "rejected" and no retry is scheduled - */ - private async retry(id: number, triesLeft: number): Promise { - if (triesLeft === 0) { - await this.updateSubscription(id, { - triesLeft, - status: SubscriptionStatus.Rejected, - }); - - return; - } - - await this.updateSubscription(id, { - triesLeft, - }); - - this.scheduleSendHandshake(id); - } -} diff --git a/src/subscriptions/types.ts b/src/subscriptions/types.ts deleted file mode 100644 index 0e1dc060..00000000 --- a/src/subscriptions/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum SubscriptionStatus { - /** - * Not yet confirmed by receiver - */ - Inactive = 'inactive', - /** - * Confirmed by receiver. URL is ready to receive subscriptions - */ - Active = 'active', - /** - * Rejected by receiver (handshake was responded with a non-200 code or without secret in the headers after all retries) - */ - Rejected = 'rejected', - /** - * Subscription lifecycle finished (i.e. transaction already finalized) or terminated manually - */ - Done = 'done', -} diff --git a/src/subsidy/dto/create-subsidy.dto.ts b/src/subsidy/dto/create-subsidy.dto.ts deleted file mode 100644 index 5032ea14..00000000 --- a/src/subsidy/dto/create-subsidy.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class CreateSubsidyDto extends TransactionBaseDto { - @ApiProperty({ - description: 'Address of the Account to subsidize', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly beneficiary: string; - - @ApiProperty({ - description: 'Amount of POLYX to be subsidized. This can be increased/decreased later on', - type: 'string', - example: '1000', - }) - @IsBigNumber() - @ToBigNumber() - readonly allowance: BigNumber; -} diff --git a/src/subsidy/dto/modify-allowance.dto.ts b/src/subsidy/dto/modify-allowance.dto.ts deleted file mode 100644 index cf5dcb20..00000000 --- a/src/subsidy/dto/modify-allowance.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { IsString } from 'class-validator'; - -import { ToBigNumber } from '~/common/decorators/transformation'; -import { IsBigNumber } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ModifyAllowanceDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Address of the beneficiary of the Subsidy relationship whose allowance is being modified', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @IsString() - readonly beneficiary: string; - - @ApiProperty({ - description: 'Amount of POLYX to set the allowance to or increase/decrease the allowance by', - type: 'string', - example: '1000', - }) - @IsBigNumber() - @ToBigNumber() - readonly allowance: BigNumber; -} diff --git a/src/subsidy/dto/quit-subsidy.dto.ts b/src/subsidy/dto/quit-subsidy.dto.ts deleted file mode 100644 index 5fdadba6..00000000 --- a/src/subsidy/dto/quit-subsidy.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, ValidateIf } from 'class-validator'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class QuitSubsidyDto extends TransactionBaseDto { - @ApiProperty({ - description: - 'Beneficiary address of the Subsidy relationship to be quit. Note, this should be passed only if quitting as a subsidizer', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ValidateIf(({ beneficiary, subsidizer }: QuitSubsidyDto) => !subsidizer || !!beneficiary) - @IsString() - readonly beneficiary?: string; - - @ApiProperty({ - description: - 'Subsidizer address of the Subsidy relationship to be quit. Note, this should be passed only if quitting as a beneficiary', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ValidateIf(({ beneficiary, subsidizer }: QuitSubsidyDto) => !beneficiary || !!subsidizer) - @IsString() - readonly subsidizer?: string; -} diff --git a/src/subsidy/dto/subsidy-params.dto.ts b/src/subsidy/dto/subsidy-params.dto.ts deleted file mode 100644 index 10439b28..00000000 --- a/src/subsidy/dto/subsidy-params.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ - -import { IsString } from 'class-validator'; - -export class SubsidyParamsDto { - @IsString() - readonly beneficiary: string; - - @IsString() - readonly subsidizer: string; -} diff --git a/src/subsidy/models/subsidy.model.ts b/src/subsidy/models/subsidy.model.ts deleted file mode 100644 index 0331ff05..00000000 --- a/src/subsidy/models/subsidy.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { Type } from 'class-transformer'; - -import { FromBigNumber } from '~/common/decorators/transformation'; -import { AccountModel } from '~/identities/models/account.model'; - -export class SubsidyModel { - @ApiProperty({ - description: 'Account whose transactions are being paid for', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly beneficiary: AccountModel; - - @ApiProperty({ - description: 'Account that is paying for the transactions', - type: AccountModel, - }) - @Type(() => AccountModel) - readonly subsidizer: AccountModel; - - @ApiProperty({ - description: 'Amount of POLYX being subsidized', - type: 'string', - example: '12345', - }) - @FromBigNumber() - readonly allowance: BigNumber; - - constructor(model: SubsidyModel) { - Object.assign(this, model); - } -} diff --git a/src/subsidy/subsidy.controller.spec.ts b/src/subsidy/subsidy.controller.spec.ts deleted file mode 100644 index a6436d2c..00000000 --- a/src/subsidy/subsidy.controller.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AllowanceOperation, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { TransactionType } from '~/common/types'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyController } from '~/subsidy/subsidy.controller'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { txResult } from '~/test-utils/consts'; -import { createMockTransactionResult, MockAuthorizationRequest } from '~/test-utils/mocks'; -import { mockSubsidyServiceProvider } from '~/test-utils/service-mocks'; - -describe('SubsidyController', () => { - let controller: SubsidyController; - let mockService: DeepMocked; - let beneficiary: string; - let subsidizer: string; - let allowance: BigNumber; - - beforeEach(async () => { - beneficiary = 'beneficiary'; - subsidizer = 'subsidizer'; - allowance = new BigNumber(1000); - - const module: TestingModule = await Test.createTestingModule({ - controllers: [SubsidyController], - providers: [mockSubsidyServiceProvider], - }).compile(); - - mockService = mockSubsidyServiceProvider.useValue as DeepMocked; - - controller = module.get(SubsidyController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getSubsidy', () => { - it('should return subsidy details for a given beneficiary and subsidizer', async () => { - when(mockService.getAllowance) - .calledWith(beneficiary, subsidizer) - .mockResolvedValue(allowance); - - const result = await controller.getSubsidy({ beneficiary, subsidizer }); - - expect(result).toEqual( - expect.objectContaining({ - beneficiary: expect.objectContaining({ address: beneficiary }), - subsidizer: expect.objectContaining({ address: subsidizer }), - allowance, - }) - ); - }); - }); - - describe('subsidizeAccount', () => { - it('should accept CreateSubsidyDto and return the authorization request for adding as paying key', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.AcceptPayingKey, - }; - const mockAuthorization = new MockAuthorizationRequest(); - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - result: mockAuthorization, - }); - const mockPayload: CreateSubsidyDto = { - signer: 'Alice', - beneficiary, - allowance, - }; - - when(mockService.subsidizeAccount) - .calledWith(mockPayload) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - const result = await controller.subsidizeAccount(mockPayload); - - expect(result).toEqual( - new CreatedAuthorizationRequestModel({ - ...txResult, - transactions: [transaction], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }) - ); - }); - }); - - describe('setAllowance, increaseAllowance, decreaseAllowance', () => { - it('should accept ModifyAllowanceDto and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.UpdatePolyxLimit, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: ModifyAllowanceDto = { - signer: 'Alice', - beneficiary, - allowance, - }; - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Set) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - let result = await controller.setAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Increase) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - result = await controller.increaseAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - - when(mockService.modifyAllowance) - .calledWith(mockPayload, AllowanceOperation.Decrease) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - result = await controller.decreaseAllowance(mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); - - describe('quitSubsidy', () => { - it('should accept QuitSubsidyDto and return the transaction details', async () => { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag: TxTags.relayer.RemovePayingKey, - }; - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - const mockPayload: QuitSubsidyDto = { - signer: 'Alice', - beneficiary, - }; - - when(mockService.quit) - .calledWith(mockPayload) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue(testTxResult as any); - - const result = await controller.quitSubsidy(mockPayload); - - expect(result).toEqual(testTxResult); - }); - }); -}); diff --git a/src/subsidy/subsidy.controller.ts b/src/subsidy/subsidy.controller.ts deleted file mode 100644 index 3c98d5ab..00000000 --- a/src/subsidy/subsidy.controller.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post } from '@nestjs/common'; -import { - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, -} from '@nestjs/swagger'; -import { AllowanceOperation } from '@polymeshassociation/polymesh-sdk/types'; - -import { authorizationRequestResolver } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiTransactionFailedResponse, ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResponseModel } from '~/common/utils'; -import { AccountModel } from '~/identities/models/account.model'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyParamsDto } from '~/subsidy/dto/subsidy-params.dto'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; -import { SubsidyService } from '~/subsidy/subsidy.service'; - -@ApiTags('accounts', 'subsidy') -@Controller('accounts/subsidy') -export class SubsidyController { - constructor(private readonly subsidyService: SubsidyService) {} - - @ApiOperation({ - summary: 'Get Account Subsidy', - description: - 'The endpoint retrieves the subsidized balance of this Account and the subsidizer Account', - }) - @ApiParam({ - name: 'subsidizer', - description: 'The Account address of the subsidizer', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiParam({ - name: 'subsidizer', - description: 'The Account address of the beneficiary', - type: 'string', - example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV', - }) - @ApiOkResponse({ - description: 'Subsidy details for the Account', - type: SubsidyModel, - }) - @ApiNotFoundResponse({ - description: 'The Subsidy no longer exists', - }) - @Get(':subsidizer/:beneficiary') - async getSubsidy(@Param() { beneficiary, subsidizer }: SubsidyParamsDto): Promise { - const allowance = await this.subsidyService.getAllowance(beneficiary, subsidizer); - - return new SubsidyModel({ - beneficiary: new AccountModel({ address: beneficiary }), - subsidizer: new AccountModel({ address: subsidizer }), - allowance, - }); - } - - @ApiOperation({ - summary: 'Subsidize an account', - description: - 'This endpoint sends an Authorization Request to an Account to subsidize its transaction fees', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: [ - 'The Beneficiary Account already has a pending invitation to add this account as a subsidizer with the same allowance', - ], - }) - @Post('create') - async subsidizeAccount(@Body() params: CreateSubsidyDto): Promise { - const serviceResult = await this.subsidyService.subsidizeAccount(params); - - return handleServiceResult(serviceResult, authorizationRequestResolver); - } - - @ApiOperation({ - summary: 'Set allowance for a Subsidy relationship', - description: - 'This endpoint allows to set allowance of a Subsidy relationship. Note that only the subsidizer is allowed to set the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.BAD_REQUEST]: ['Amount of allowance to set is equal to the current allowance'], - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/set') - async setAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance(params, AllowanceOperation.Set); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Increase the allowance for a Subsidy relationship', - description: - 'This endpoint allows to increase the allowance of a Subsidy relationship. Note that only the subsidizer is allowed to increase the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/increase') - async increaseAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance( - params, - AllowanceOperation.Increase - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Decrease the allowance for a Subsidy relationship', - description: - 'This endpoint allows to decrease the allowance of a Subsidy relationship. Note that only the subsidizer is allowed to decrease the allowance', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.UNPROCESSABLE_ENTITY]: [ - 'Amount of allowance to decrease is more than the current allowance', - ], - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('allowance/decrease') - async decreaseAllowance(@Body() params: ModifyAllowanceDto): Promise { - const serviceResult = await this.subsidyService.modifyAllowance( - params, - AllowanceOperation.Decrease - ); - - return handleServiceResult(serviceResult); - } - - @ApiOperation({ - summary: 'Quit a Subsidy relationship', - description: - 'This endpoint terminates a Subsidy relationship. The beneficiary Account will be forced to pay for their own transactions', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiTransactionFailedResponse({ - [HttpStatus.NOT_FOUND]: ['The Subsidy no longer exists'], - }) - @Post('quit') - async quitSubsidy(@Body() params: QuitSubsidyDto): Promise { - const serviceResult = await this.subsidyService.quit(params); - return handleServiceResult(serviceResult); - } -} diff --git a/src/subsidy/subsidy.module.ts b/src/subsidy/subsidy.module.ts deleted file mode 100644 index 15e66ea1..00000000 --- a/src/subsidy/subsidy.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { forwardRef, Module } from '@nestjs/common'; - -import { AccountsModule } from '~/accounts/accounts.module'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { SubsidyController } from '~/subsidy/subsidy.controller'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule, forwardRef(() => AccountsModule)], - controllers: [SubsidyController], - providers: [SubsidyService], - exports: [SubsidyService], -}) -export class SubsidyModule {} diff --git a/src/subsidy/subsidy.service.spec.ts b/src/subsidy/subsidy.service.spec.ts deleted file mode 100644 index ab9f1a02..00000000 --- a/src/subsidy/subsidy.service.spec.ts +++ /dev/null @@ -1,328 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { AllowanceOperation, Subsidy, TxTags } from '@polymeshassociation/polymesh-sdk/types'; -import { when } from 'jest-when'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppValidationError } from '~/common/errors'; -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { testValues } from '~/test-utils/consts'; -import { - createMockSubsidy, - MockAccount, - MockAuthorizationRequest, - MockPolymesh, - MockTransaction, -} from '~/test-utils/mocks'; -import { - MockAccountsService, - mockTransactionsProvider, - MockTransactionsService, -} from '~/test-utils/service-mocks'; -import * as transactionsUtilModule from '~/transactions/transactions.util'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('SubsidyService', () => { - let service: SubsidyService; - let mockAccountsService: MockAccountsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - let beneficiary: string; - let subsidizer: string; - let allowance: BigNumber; - let signer: string; - let mockSubsidy: DeepMocked; - - beforeEach(async () => { - ({ signer } = testValues); - beneficiary = 'beneficiary'; - subsidizer = 'subsidizer'; - allowance = new BigNumber(100); - - mockPolymeshApi = new MockPolymesh(); - mockSubsidy = createMockSubsidy(); - - mockTransactionsService = mockTransactionsProvider.useValue; - mockAccountsService = new MockAccountsService(); - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [SubsidyService, AccountsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .overrideProvider(AccountsService) - .useValue(mockAccountsService) - .compile(); - - service = module.get(SubsidyService); - polymeshService = module.get(PolymeshService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getSubsidy', () => { - it('should return the Account Subsidy', async () => { - const mockSubsidyWithAllowance = { - subsidy: mockSubsidy, - allowance: new BigNumber(10), - }; - - const mockAccount = new MockAccount(); - mockAccount.getSubsidy.mockResolvedValue(mockSubsidyWithAllowance); - - when(mockAccountsService.findOne).calledWith(subsidizer).mockResolvedValue(mockAccount); - - const result = await service.getSubsidy(subsidizer); - - expect(result).toEqual(mockSubsidyWithAllowance); - }); - }); - - describe('findOne', () => { - it('should return a Subsidy instance for a given beneficiary and subsidizer', () => { - when(mockPolymeshApi.accountManagement.getSubsidy) - .calledWith({ beneficiary, subsidizer }) - .mockReturnValue(mockSubsidy); - - const result = service.findOne(beneficiary, subsidizer); - - expect(result).toEqual(mockSubsidy); - }); - }); - - describe('subsidizeAccount', () => { - it('should run a subsidizeAccount procedure and return the queue results', async () => { - const mockAuthRequest = new MockAuthorizationRequest(); - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.SetPayingKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - const body = { - signer, - beneficiary, - allowance: new BigNumber(100), - }; - - const result = await service.subsidizeAccount(body); - expect(result).toEqual({ - result: mockAuthRequest, - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockPolymeshApi.accountManagement.subsidizeAccount, - { beneficiary, allowance }, - { signer } - ); - }); - }); - - describe('quit', () => { - it('should run a quit procedure and return the queue results', async () => { - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.RemovePayingKey, - }; - const mockTransaction = new MockTransaction(mockTransactions); - - const findOneSpy = jest.spyOn(service, 'findOne'); - - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - - mockTransactionsService.getSigningAccount.mockResolvedValueOnce(subsidizer); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - let body = { - signer: subsidizer, - beneficiary, - } as QuitSubsidyDto; - - let result = await service.quit(body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.quit, - {}, - { signer: subsidizer } - ); - - when(findOneSpy).calledWith(subsidizer, beneficiary).mockReturnValue(mockSubsidy); - - mockTransactionsService.getSigningAccount.mockResolvedValueOnce(beneficiary); - - body = { - signer: beneficiary, - subsidizer, - }; - - result = await service.quit(body); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.quit, - {}, - { signer: beneficiary } - ); - }); - - it('should throw an error if no beneficiary or subsidizer is passed', () => { - mockTransactionsService.getSigningAccount.mockResolvedValueOnce('address'); - - return expect(() => service.quit({ signer: 'signer' })).rejects.toBeInstanceOf( - AppValidationError - ); - }); - - it('should throw an error if both beneficiary and subsidizer are passed', () => { - mockTransactionsService.getSigningAccount.mockResolvedValueOnce('address'); - - return expect(() => - service.quit({ signer: 'signer', beneficiary, subsidizer }) - ).rejects.toBeInstanceOf(AppValidationError); - }); - }); - - describe('modifyAllowance', () => { - let findOneSpy: jest.SpyInstance; - let body: ModifyAllowanceDto; - let mockTransaction: MockTransaction; - - beforeEach(() => { - body = { - signer, - beneficiary, - allowance, - }; - - const mockTransactions = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.relayer.RemovePayingKey, - }; - mockTransaction = new MockTransaction(mockTransactions); - - mockTransactionsService.submit.mockResolvedValue({ - transactions: [mockTransaction], - }); - - mockTransactionsService.getSigningAccount.mockResolvedValue(subsidizer); - - findOneSpy = jest.spyOn(service, 'findOne'); - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - }); - - it('should run a setAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Set); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.setAllowance, - { allowance }, - { signer } - ); - }); - - it('should run a increaseAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Increase); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.increaseAllowance, - { allowance }, - { signer } - ); - }); - - it('should run a decreaseAllowance procedure and return the queue results', async () => { - const result = await service.modifyAllowance(body, AllowanceOperation.Decrease); - expect(result).toEqual({ - transactions: [mockTransaction], - }); - - expect(mockTransactionsService.submit).toHaveBeenCalledWith( - mockSubsidy.decreaseAllowance, - { allowance }, - { signer } - ); - }); - }); - - describe('getAllowance', () => { - let findOneSpy: jest.SpyInstance; - - beforeEach(() => { - findOneSpy = jest.spyOn(service, 'findOne'); - - when(findOneSpy).calledWith(beneficiary, subsidizer).mockReturnValue(mockSubsidy); - }); - - it('should return the Subsidy allowance', async () => { - mockSubsidy.getAllowance.mockResolvedValue(allowance); - - const result = await service.getAllowance(beneficiary, subsidizer); - - expect(result).toEqual(allowance); - }); - - describe('otherwise', () => { - it('should call the handleSdkError method and throw an error', async () => { - const mockError = new Error('Some Error'); - mockSubsidy.getAllowance.mockRejectedValue(mockError); - - const handleSdkErrorSpy = jest.spyOn(transactionsUtilModule, 'handleSdkError'); - - await expect(() => service.getAllowance(beneficiary, subsidizer)).rejects.toThrowError(); - - expect(handleSdkErrorSpy).toHaveBeenCalledWith(mockError); - }); - }); - }); -}); diff --git a/src/subsidy/subsidy.service.ts b/src/subsidy/subsidy.service.ts deleted file mode 100644 index b41b096a..00000000 --- a/src/subsidy/subsidy.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - AllowanceOperation, - AuthorizationRequest, - Subsidy, - SubsidyWithAllowance, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountsService } from '~/accounts/accounts.service'; -import { AppValidationError } from '~/common/errors'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { CreateSubsidyDto } from '~/subsidy/dto/create-subsidy.dto'; -import { ModifyAllowanceDto } from '~/subsidy/dto/modify-allowance.dto'; -import { QuitSubsidyDto } from '~/subsidy/dto/quit-subsidy.dto'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { handleSdkError } from '~/transactions/transactions.util'; - -@Injectable() -export class SubsidyService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService, - private readonly accountsService: AccountsService - ) {} - - public async getSubsidy(address: string): Promise { - const account = await this.accountsService.findOne(address); - return account.getSubsidy(); - } - - public findOne(beneficiary: string, subsidizer: string): Subsidy { - return this.polymeshService.polymeshApi.accountManagement.getSubsidy({ - beneficiary, - subsidizer, - }); - } - - public async subsidizeAccount(params: CreateSubsidyDto): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { subsidizeAccount } = this.polymeshService.polymeshApi.accountManagement; - - return this.transactionsService.submit(subsidizeAccount, args, base); - } - - public async quit(params: QuitSubsidyDto): ServiceReturn { - const { - base, - args: { beneficiary, subsidizer }, - } = extractTxBase(params); - - const { signer } = base; - const address = await this.transactionsService.getSigningAccount(signer); - - let subsidy: Subsidy; - if (beneficiary && subsidizer) { - throw new AppValidationError('Only beneficiary or subsidizer should be provided'); - } else if (beneficiary) { - subsidy = this.findOne(beneficiary, address); - } else if (subsidizer) { - subsidy = this.findOne(address, subsidizer); - } else { - throw new AppValidationError('Either beneficiary or subsidizer should be provided'); - } - - return this.transactionsService.submit(subsidy.quit, {}, base); - } - - public async modifyAllowance( - params: ModifyAllowanceDto, - operation: AllowanceOperation - ): ServiceReturn { - const { - base, - args: { beneficiary, allowance }, - } = extractTxBase(params); - - const { signer } = base; - - const address = await this.transactionsService.getSigningAccount(signer); - - const subsidy = this.findOne(beneficiary, address); - - const procedureMap = { - [AllowanceOperation.Set]: subsidy.setAllowance, - [AllowanceOperation.Increase]: subsidy.increaseAllowance, - [AllowanceOperation.Decrease]: subsidy.decreaseAllowance, - }; - - return this.transactionsService.submit(procedureMap[operation], { allowance }, base); - } - - public async getAllowance(beneficiary: string, subsidizer: string): Promise { - const subsidy = this.findOne(beneficiary, subsidizer); - - return await subsidy.getAllowance().catch(error => { - throw handleSdkError(error); - }); - } -} diff --git a/src/subsidy/subsidy.util.spec.ts b/src/subsidy/subsidy.util.spec.ts deleted file mode 100644 index 09c39c3c..00000000 --- a/src/subsidy/subsidy.util.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; - -import { AccountModel } from '~/identities/models/account.model'; -import { createSubsidyModel } from '~/subsidy/subsidy.util'; -import { createMockSubsidy } from '~/test-utils/mocks'; - -describe('createSubsidyModel', () => { - it('should transform SubsidyWithAllowance to SubsidyModel', () => { - const subsidyWithAllowance = { - subsidy: createMockSubsidy(), - allowance: new BigNumber(10), - }; - - const result = createSubsidyModel(subsidyWithAllowance); - - expect(result).toEqual({ - beneficiary: new AccountModel({ address: 'beneficiary' }), - subsidizer: new AccountModel({ address: 'subsidizer' }), - allowance: new BigNumber(10), - }); - }); -}); diff --git a/src/subsidy/subsidy.util.ts b/src/subsidy/subsidy.util.ts deleted file mode 100644 index 78d71938..00000000 --- a/src/subsidy/subsidy.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SubsidyWithAllowance } from '@polymeshassociation/polymesh-sdk/types'; - -import { AccountModel } from '~/identities/models/account.model'; -import { SubsidyModel } from '~/subsidy/models/subsidy.model'; - -export function createSubsidyModel(subsidy: SubsidyWithAllowance): SubsidyModel { - const { - subsidy: { - beneficiary: { address: beneficiaryAddress }, - subsidizer: { address: subsidizerAddress }, - }, - allowance, - } = subsidy; - - return new SubsidyModel({ - beneficiary: new AccountModel({ address: beneficiaryAddress }), - subsidizer: new AccountModel({ address: subsidizerAddress }), - allowance, - }); -} diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index 7dbb96fe..2c84d850 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -1,92 +1,18 @@ -import { createMock } from '@golevelup/ts-jest'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { - Account, - PayingAccountType, - TransactionStatus, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { UserModel } from '~/users/model/user.model'; - -const signer = 'alice'; -const did = '0x01'.padEnd(66, '0'); -const dryRun = false; -const ticker = 'TICKER'; - -const user = new UserModel({ - id: '-1', - name: 'TestUser', +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { BatchTransactionModel } from '~/polymesh-rest-api/src/common/models/batch-transaction.model'; +import { TransactionModel } from '~/polymesh-rest-api/src/common/models/transaction.model'; +import { TransactionType } from '~/polymesh-rest-api/src/common/types'; + +export * from '~/polymesh-rest-api/src/test-utils/consts'; + +export const getMockTransaction = ( + tag: string, + type = TransactionType.Single +): TransactionModel | BatchTransactionModel => ({ + blockHash: '0x1', + transactionHash: '0x2', + blockNumber: new BigNumber(1), + type, + transactionTag: tag, }); - -const resource = { - type: 'TestResource', - id: '-1', -} as const; - -export const testAccount = createMock({ address: 'address' }); -export const txResult = { - transactions: [ - { - transactionTag: 'tag', - type: TransactionType.Single, - blockNumber: new BigNumber(1), - blockHash: 'hash', - transactionHash: 'hash', - }, - ], - details: { - status: TransactionStatus.Succeeded, - fees: { - gas: new BigNumber(1), - protocol: new BigNumber(1), - total: new BigNumber(1), - }, - supportsSubsidy: false, - payingAccount: { - address: did, - balance: new BigNumber(1), - type: PayingAccountType.Caller, - }, - }, -}; - -export const testValues = { - signer, - did, - user, - resource, - testAccount, - txResult, - dryRun, - ticker, -}; - -export const extrinsic = { - blockHash: 'blockHash', - blockNumber: new BigNumber(1000000), - blockDate: new Date(), - extrinsicIdx: new BigNumber(1), - address: 'someAccount', - nonce: new BigNumber(123456), - txTag: TxTags.asset.RegisterTicker, - params: [ - { - name: 'ticker', - value: 'TICKER', - }, - ], - success: true, - specVersionId: new BigNumber(3002), - extrinsicHash: 'extrinsicHash', -}; - -export const extrinsicWithFees = { - ...extrinsic, - fee: { - gas: new BigNumber('1.234'), - protocol: new BigNumber(0), - total: new BigNumber('1.234'), - }, -}; diff --git a/src/test-utils/mocks.ts b/src/test-utils/mocks.ts index 449eabed..e5ab41c2 100644 --- a/src/test-utils/mocks.ts +++ b/src/test-utils/mocks.ts @@ -1,109 +1,39 @@ /* istanbul ignore file */ import { createMock, DeepMocked, PartialFuncReturn } from '@golevelup/ts-jest'; -import { ValueProvider } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { SettlementResultEnum } from '@polymeshassociation/polymesh-sdk/middleware/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { - Account, - AuthorizationType, - ComplianceManagerTx, - HistoricSettlement, - MetadataEntry, - MetadataType, - ResultSet, - SettlementLeg, - Subsidy, - TransactionStatus, - TrustedClaimIssuer, + ConfidentialAccount, + ConfidentialAsset, + ConfidentialTransaction, + ConfidentialVenue, + Identity, TxTag, - TxTags, -} from '@polymeshassociation/polymesh-sdk/types'; -import { Response } from 'express'; +} from '@polymeshassociation/polymesh-private-sdk/types'; -import { TransactionType } from '~/common/types'; -import { ServiceReturn } from '~/common/utils'; -import { EventType } from '~/events/types'; -import { NotificationPayload } from '~/notifications/types'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues, txResult } from '~/test-utils/consts'; -import { TransactionResult } from '~/transactions/transactions.util'; - -const { did } = testValues; - -export type Mocked = T & { - [K in keyof T]: T[K] extends (...args: infer Args) => unknown - ? T[K] & jest.Mock, Args> - : T[K]; -}; - -export const mockTrustedClaimIssuer = createMock>(); - -export const createMockTransactionResult = ({ - details, - transactions, - result, -}: { - details: TransactionResult['details']; - transactions: TransactionResult['transactions']; - result?: TransactionResult['result']; -}): DeepMocked> => { - return { transactions, result, details } as DeepMocked>; -}; - -export const createMockResponseObject = (): DeepMocked => { - return createMock({ - json: jest.fn().mockReturnThis(), - status: jest.fn().mockReturnThis(), - }); -}; - -export const MockPolymeshService = createMock(); - -export const mockPolymeshServiceProvider: ValueProvider = { - provide: PolymeshService, - useValue: createMock(), -}; - -/* Polymesh SDK */ - -export class MockPolymesh { - public static create = jest.fn().mockResolvedValue(new MockPolymesh()); +import { + MockIdentity as MockIdentityRestApi, + MockPolymesh as MockPublicPolymesh, +} from '~/polymesh-rest-api/src/test-utils/mocks'; - public disconnect = jest.fn(); - public setSigningManager = jest.fn(); +export * from '~/polymesh-rest-api/src/test-utils/mocks'; - public network = { - getLatestBlock: jest.fn(), - transferPolyx: jest.fn(), - getSs58Format: jest.fn(), - getNetworkProperties: jest.fn(), - getTreasuryAccount: jest.fn(), - getTransactionByHash: jest.fn(), +export class MockPolymesh extends MockPublicPolymesh { + public confidentialAccounts = { + getConfidentialAccount: jest.fn(), + createConfidentialAccount: jest.fn(), }; - public assets = { - getFungibleAsset: jest.fn(), - getAssets: jest.fn(), - getAsset: jest.fn(), - getNftCollection: jest.fn(), - reserveTicker: jest.fn(), - createAsset: jest.fn(), - getTickerReservation: jest.fn(), - getTickerReservations: jest.fn(), - getGlobalMetadataKeys: jest.fn(), + public confidentialAssets = { + getConfidentialAsset: jest.fn(), + getConfidentialAssetFromTicker: jest.fn(), + createConfidentialAsset: jest.fn(), }; - public accountManagement = { - getAccount: jest.fn(), - getAccountBalance: jest.fn(), - inviteAccount: jest.fn(), - freezeSecondaryAccounts: jest.fn(), - unfreezeSecondaryAccounts: jest.fn(), - revokePermissions: jest.fn(), - modifyPermissions: jest.fn(), - subsidizeAccount: jest.fn(), - getSubsidy: jest.fn(), + public confidentialSettlements = { + getTransaction: jest.fn(), + getVenue: jest.fn(), + createVenue: jest.fn(), }; public identities = { @@ -111,238 +41,6 @@ export class MockPolymesh { getIdentity: jest.fn(), createPortfolio: jest.fn(), }; - - public settlements = { - getInstruction: jest.fn(), - getVenue: jest.fn(), - createVenue: jest.fn(), - }; - - public claims = { - getIssuedClaims: jest.fn(), - getIdentitiesWithClaims: jest.fn(), - addClaims: jest.fn(), - editClaims: jest.fn(), - revokeClaims: jest.fn(), - getCddClaims: jest.fn(), - getClaimScopes: jest.fn(), - addInvestorUniquenessClaim: jest.fn(), - getInvestorUniquenessClaims: jest.fn(), - getCustomClaimTypeByName: jest.fn(), - getCustomClaimTypeById: jest.fn(), - registerCustomClaimType: jest.fn(), - getAllCustomClaimTypes: jest.fn(), - }; - - public _polkadotApi = { - tx: { - balances: { - transfer: jest.fn(), - setBalance: jest.fn(), - }, - cddServiceProviders: { - addMember: jest.fn(), - }, - identity: { - addClaim: jest.fn(), - cddRegisterDid: jest.fn(), - }, - sudo: { - sudo: jest.fn(), - }, - testUtils: { - mockCddRegisterDid: jest.fn().mockReturnValue({ - signAndSend: jest.fn(), - }), - }, - utility: { - batchAtomic: jest.fn(), - }, - }, - }; -} - -export class MockAsset { - ticker = 'TICKER'; - public details = jest.fn(); - public getIdentifiers = jest.fn(); - public currentFundingRound = jest.fn(); - public isFrozen = jest.fn(); - public transferOwnership = jest.fn(); - public redeem = jest.fn(); - public freeze = jest.fn(); - public unfreeze = jest.fn(); - public controllerTransfer = jest.fn(); - public getOperationHistory = jest.fn(); - - public assetHolders = { - get: jest.fn(), - }; - - public documents = { - get: jest.fn(), - set: jest.fn(), - }; - - public settlements = { - canTransfer: jest.fn(), - }; - - public compliance = { - requirements: { - get: jest.fn(), - set: jest.fn(), - arePaused: jest.fn(), - }, - trustedClaimIssuers: { - get: jest.fn(), - set: jest.fn(), - add: jest.fn(), - remove: jest.fn(), - }, - }; - - public offerings = { - get: jest.fn(), - }; - - public checkpoints = { - get: jest.fn(), - create: jest.fn(), - getOne: jest.fn(), - - schedules: { - get: jest.fn(), - getOne: jest.fn(), - create: jest.fn(), - remove: jest.fn(), - }, - }; - - public corporateActions = { - distributions: { - get: jest.fn(), - getOne: jest.fn(), - configureDividendDistribution: jest.fn(), - }, - getDefaultConfig: jest.fn(), - setDefaultConfig: jest.fn(), - remove: jest.fn(), - }; - - public issuance = { - issue: jest.fn(), - }; - - public metadata = { - register: jest.fn(), - get: jest.fn(), - getOne: jest.fn(), - }; - - public toHuman = jest.fn().mockImplementation(() => this.ticker); -} - -export class MockInstruction { - public getStatus = jest.fn(); - public affirm = jest.fn(); - public reject = jest.fn(); - public details = jest.fn(); - public getLegs = jest.fn(); - public getAffirmations = jest.fn(); - public withdraw = jest.fn(); - public reschedule = jest.fn(); -} - -export class MockVenue { - id = new BigNumber(1); - public addInstruction = jest.fn(); - public details = jest.fn(); - public modify = jest.fn(); -} - -export class MockIdentityAuthorization { - public getSent = jest.fn(); - public getReceived = jest.fn(); - public getOne = jest.fn(); -} - -export class MockPortfolios { - public getPortfolios = jest.fn(); - public getPortfolio = jest.fn(); - public create = jest.fn(); - public delete = jest.fn(); - public getCustodiedPortfolios = jest.fn(); -} - -export class MockIdentity { - did = did; - portfolios = new MockPortfolios(); - authorizations = new MockIdentityAuthorization(); - public getPrimaryAccount = jest.fn(); - public areSecondaryAccountsFrozen = jest.fn(); - public getInstructions = jest.fn(); - public getVenues = jest.fn(); - public createVenue = jest.fn(); - public getSecondaryAccounts = jest.fn(); - public getTrustingAssets = jest.fn(); - public getHeldAssets = jest.fn(); -} - -export class MockPortfolio { - id = new BigNumber(1); - owner = new MockIdentity(); - public getName = jest.fn(); - public createdAt = jest.fn(); - public getAssetBalances = jest.fn(); - public isCustodiedBy = jest.fn(); - public getCustodian = jest.fn(); - public setCustodian = jest.fn(); - public moveFunds = jest.fn(); - public getTransactionHistory = jest.fn(); - public quitCustody = jest.fn(); - public toHuman = jest.fn().mockImplementation(() => { - return { - id: '1', - did, - }; - }); -} - -export class MockCheckpoint { - id = new BigNumber(1); - ticker = 'TICKER'; - balance = jest.fn(); - allBalances = jest.fn(); - createdAt = jest.fn(); - totalSupply = jest.fn(); -} - -export class MockCheckpointSchedule { - id = new BigNumber(1); - ticker = 'TICKER'; - pendingPoints = [new Date('10/14/1987')]; - expiryDate = new Date('10/14/2000'); -} - -export class MockAuthorizationRequest { - authId = new BigNumber(1); - expiry = null; - data = { - type: AuthorizationType.PortfolioCustody, - value: { - did, - id: new BigNumber(1), - }, - }; - - issuer = new MockIdentity(); - target = { - did, - }; - - public accept = jest.fn(); - public remove = jest.fn(); } export class MockTransaction { @@ -360,131 +58,56 @@ export class MockTransaction { public run = jest.fn(); } -export class MockHistoricSettlement { - constructor( - readonly settlement?: { - blockNumber?: BigNumber; - blockHash?: string; - accounts?: Account[]; - legs?: SettlementLeg[]; - } - ) { - const defaultValue: HistoricSettlement = { - blockNumber: new BigNumber(1), - blockHash: '0x1', - status: SettlementResultEnum.Executed, - accounts: [], - legs: [], - }; +export class MockIdentity extends MockIdentityRestApi { + public getConfidentialVenues = jest.fn(); + public getInvolvedConfidentialTransactions = jest.fn(); +} - Object.assign(this, { ...defaultValue, ...settlement }); +export function createMockIdentity( + partial: PartialFuncReturn = { + did: 'SOME_DID', } +): DeepMocked { + return createMock(partial); } -class MockPolymeshTransactionBase { - blockHash?: string; - txHash?: string; - blockNumber?: BigNumber; - status: TransactionStatus = TransactionStatus.Unapproved; - error: Error; - getTotalFees = jest.fn().mockResolvedValue({ - total: new BigNumber(1), - payingAccountData: { account: { address: 'address' } }, - }); - - supportsSubsidy = jest.fn().mockReturnValue(false); - run = jest.fn().mockReturnValue(Promise.resolve()); - onStatusChange = jest.fn(); -} -export class MockPolymeshTransaction extends MockPolymeshTransactionBase { - tag: TxTag = TxTags.asset.RegisterTicker; +export function createMockConfidentialAsset( + partial: PartialFuncReturn = { + id: 'SOME-CONFIDENTIAL-ASSET-ID', + } +): DeepMocked { + return createMock(partial); } -export class MockPolymeshTransactionBatch extends MockPolymeshTransactionBase { - transactions: { tag: TxTag }[] = [ - { - tag: TxTags.asset.RegisterTicker, +export function createMockConfidentialAccount( + partial: PartialFuncReturn = { + publicKey: 'SOME_KEY', + getIdentity(): PartialFuncReturn> { + return { did: 'SOME_OWNER' } as PartialFuncReturn>; }, - { - tag: TxTags.asset.CreateAsset, + getBalance(): PartialFuncReturn> { + return '0x0ceabalance' as PartialFuncReturn>; }, - ]; -} - -export type CallbackFn = (tx: T) => Promise; - -export class MockOffering { - id = new BigNumber(1); - ticker = 'TICKER'; - public getInvestments = jest.fn(); -} -export class MockTickerReservation { - ticker = 'TICKER'; - - public transferOwnership = jest.fn(); - public extend = jest.fn(); - public details = jest.fn(); -} - -export class MockAuthorizations { - getOne = jest.fn(); -} -export class MockAccount { - address: string; - authorizations = new MockAuthorizations(); - getTransactionHistory = jest.fn(); - getPermissions = jest.fn(); - getIdentity = jest.fn(); - getSubsidy = jest.fn(); - - constructor(address = 'address') { - this.address = address; } +): DeepMocked { + return createMock(partial); } -export function createMockMetadataEntry( - partial: PartialFuncReturn = { +export function createMockConfidentialTransaction( + partial: PartialFuncReturn = { id: new BigNumber(1), - type: MetadataType.Local, - asset: { ticker: 'TICKER' }, } -): DeepMocked { - return createMock(partial); +): DeepMocked { + return createMock(partial); } -export function createMockSubsidy( - partial: PartialFuncReturn = { - beneficiary: { address: 'beneficiary' }, - subsidizer: { address: 'subsidizer' }, +export function createMockConfidentialVenue( + partial: PartialFuncReturn = { + id: new BigNumber(1), + creator(): PartialFuncReturn> { + return { did: 'SOME_OWNER' } as PartialFuncReturn>; + }, } -): DeepMocked { - return createMock(partial); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createMockResultSet(data: T): ResultSet { - return { - data, - next: '0', - count: new BigNumber(data.length), - }; -} - -export function createMockTxResult( - transactionTag: ComplianceManagerTx -): TransactionResult | ServiceReturn | NotificationPayload { - const transaction = { - blockHash: '0x1', - transactionHash: '0x2', - blockNumber: new BigNumber(1), - type: TransactionType.Single, - transactionTag, - }; - - const testTxResult = createMockTransactionResult({ - ...txResult, - transactions: [transaction], - }); - - return testTxResult; +): DeepMocked { + return createMock(partial); } diff --git a/src/test-utils/repo-mocks.ts b/src/test-utils/repo-mocks.ts deleted file mode 100644 index ae199e84..00000000 --- a/src/test-utils/repo-mocks.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ - -import { createMock } from '@golevelup/ts-jest'; -import { ValueProvider } from '@nestjs/common'; - -import { ApiKeyRepo } from '~/auth/repos/api-key.repo'; -import { UsersRepo } from '~/users/repo/user.repo'; - -export const mockApiKeyRepoProvider: ValueProvider = { - provide: ApiKeyRepo, - useValue: createMock(), -}; - -export const mockUserRepoProvider: ValueProvider = { - provide: UsersRepo, - useValue: createMock(), -}; - -export class MockPostgresApiRepository { - create = jest.fn(); - findOneBy = jest.fn(); - delete = jest.fn(); - save = jest.fn(); -} - -export class MockPostgresUserRepository { - findOneBy = jest.fn(); - save = jest.fn(); - delete = jest.fn(); - create = jest.fn(); -} diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index dfeef264..3826d6fe 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -2,280 +2,44 @@ import { createMock } from '@golevelup/ts-jest'; import { ValueProvider } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AuthService } from '~/auth/auth.service'; -import { ClaimsService } from '~/claims/claims.service'; -import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; -import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; -import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; -import { MetadataService } from '~/metadata/metadata.service'; -import { NetworkService } from '~/network/network.service'; -import { NftsService } from '~/nfts/nfts.service'; -import { SubsidyService } from '~/subsidy/subsidy.service'; -import { ServiceProvider } from '~/test-utils/types'; -import { TransactionsService } from '~/transactions/transactions.service'; -import { UsersService } from '~/users/users.service'; +import { ConfidentialAccountsService } from '~/confidential-accounts/confidential-accounts.service'; +import { ConfidentialAssetsService } from '~/confidential-assets/confidential-assets.service'; +import { ConfidentialProofsService } from '~/confidential-proofs/confidential-proofs.service'; +import { ConfidentialTransactionsService } from '~/confidential-transactions/confidential-transactions.service'; +import { + MockHttpService as MockHttpServiceRestApi, + MockIdentitiesService as MockIdentitiesServiceRestApi, +} from '~/polymesh-rest-api/src/test-utils/service-mocks'; -export class MockAssetService { - findOne = jest.fn(); - findFungible = jest.fn(); - findNftCollection = jest.fn(); - findHolders = jest.fn(); - findDocuments = jest.fn(); - setDocuments = jest.fn(); - findAllByOwner = jest.fn(); - registerTicker = jest.fn(); - createAsset = jest.fn(); - issue = jest.fn(); - transferOwnership = jest.fn(); - redeem = jest.fn(); - freeze = jest.fn(); - unfreeze = jest.fn(); - controllerTransfer = jest.fn(); - getOperationHistory = jest.fn(); -} - -export class MockTransactionsService { - submit = jest.fn(); - getSigningAccount = jest.fn(); -} - -export const mockTransactionsProvider = { - provide: TransactionsService, - useValue: new MockTransactionsService(), -}; - -export class MockComplianceRequirementsService { - setRequirements = jest.fn(); - findComplianceRequirements = jest.fn(); - pauseRequirements = jest.fn(); - unpauseRequirements = jest.fn(); - deleteAll = jest.fn(); - deleteOne = jest.fn(); - add = jest.fn(); - modify = jest.fn(); -} - -export const mockComplianceRequirementsServiceProvider: ValueProvider = - { - provide: ComplianceRequirementsService, - useValue: createMock(), - }; - -export const mockDeveloperServiceProvider: ValueProvider = { - provide: DeveloperTestingService, - useValue: createMock(), -}; - -export class MockSigningService { - public getAddressByHandle = jest.fn(); -} - -export class MockTickerReservationsService { - findOne = jest.fn(); - reserve = jest.fn(); - transferOwnership = jest.fn(); - extend = jest.fn(); - findAllByOwner = jest.fn(); -} - -export class MockAuthorizationsService { - findPendingByDid = jest.fn(); - findIssuedByDid = jest.fn(); - findOneByDid = jest.fn(); - accept = jest.fn(); - remove = jest.fn(); -} - -export class MockAccountsService { - getAccountBalance = jest.fn(); - transferPolyx = jest.fn(); - getTransactionHistory = jest.fn(); - getPermissions = jest.fn(); - findOne = jest.fn(); - freezeSecondaryAccounts = jest.fn(); - unfreezeSecondaryAccounts = jest.fn(); - modifyPermissions = jest.fn(); - revokePermissions = jest.fn(); - getTreasuryAccount = jest.fn(); -} - -export class MockEventsService { - createEvent = jest.fn(); - findOne = jest.fn(); -} - -export class MockSubscriptionsService { - findAll = jest.fn(); - findOne = jest.fn(); - createSubscription = jest.fn(); - updateSubscription = jest.fn(); - batchMarkAsDone = jest.fn(); - batchBumpNonce = jest.fn(); -} - -export class MockNotificationsService { - findOne = jest.fn(); - createNotifications = jest.fn(); - updateNotification = jest.fn(); -} +export * from '~/polymesh-rest-api/src/test-utils/service-mocks'; -export class MockHttpService { - post = jest.fn(); +export class MockHttpService extends MockHttpServiceRestApi { + request = jest.fn(); } -export class MockScheduleService { - addTimeout = jest.fn(); - addInterval = jest.fn(); - deleteInterval = jest.fn(); -} - -export class MockIdentitiesService { - findOne = jest.fn(); - findTrustingAssets = jest.fn(); - findHeldAssets = jest.fn(); - addSecondaryAccount = jest.fn(); - createMockCdd = jest.fn(); - registerDid = jest.fn(); -} - -export class MockSettlementsService { - findInstruction = jest.fn(); - createInstruction = jest.fn(); - affirmInstruction = jest.fn(); - rejectInstruction = jest.fn(); - findVenueDetails = jest.fn(); - findAffirmations = jest.fn(); - createVenue = jest.fn(); - modifyVenue = jest.fn(); - canTransfer = jest.fn(); - findGroupedInstructionsByDid = jest.fn(); - findVenuesByOwner = jest.fn(); - withdrawAffirmation = jest.fn(); - rescheduleInstruction = jest.fn(); -} - -export class MockClaimsService { - findIssuedByDid = jest.fn(); - findAssociatedByDid = jest.fn(); - findCddClaimsByDid = jest.fn(); -} - -export class MockPortfoliosService { - moveAssets = jest.fn(); - findAllByOwner = jest.fn(); - createPortfolio = jest.fn(); - deletePortfolio = jest.fn(); - updatePortfolioName = jest.fn(); - getCustodiedPortfolios = jest.fn(); - getTransactions = jest.fn(); - findOne = jest.fn(); - createdAt = jest.fn(); - setCustodian = jest.fn(); - quitCustody = jest.fn(); -} - -export class MockOfferingsService { - findInvestmentsByTicker = jest.fn(); - findAllByTicker = jest.fn(); -} - -export class MockCorporateActionsService { - findDefaultConfigByTicker = jest.fn(); - updateDefaultConfigByTicker = jest.fn(); - findDistributionsByTicker = jest.fn(); - findDistribution = jest.fn(); - createDividendDistribution = jest.fn(); - remove = jest.fn(); - payDividends = jest.fn(); - claimDividends = jest.fn(); - linkDocuments = jest.fn(); - reclaimRemainingFunds = jest.fn(); - modifyCheckpoint = jest.fn(); -} - -export class MockCheckpointsService { - findAllByTicker = jest.fn(); - findSchedulesByTicker = jest.fn(); - findScheduleById = jest.fn(); - createByTicker = jest.fn(); - createScheduleByTicker = jest.fn(); - getAssetBalance = jest.fn(); - getHolders = jest.fn(); - deleteScheduleByTicker = jest.fn(); +export class MockIdentitiesService extends MockIdentitiesServiceRestApi { findOne = jest.fn(); + getInvolvedConfidentialTransactions = jest.fn(); } -export class MockAuthService { - createApiKey = jest.fn(); - deleteApiKey = jest.fn(); - validateApiKey = jest.fn(); -} - -export class MockTrustedClaimIssuersService { - find = jest.fn(); - set = jest.fn(); - add = jest.fn(); - remove = jest.fn(); -} - -export const mockAuthServiceProvider = { - provide: AuthService, - useValue: new MockAuthService(), -}; - -export const mockUserServiceProvider: ValueProvider = { - provide: UsersService, - useValue: createMock(), -}; - -export const mockTrustedClaimIssuersServiceProvider: ValueProvider = { - provide: TrustedClaimIssuersService, - useValue: createMock(), +export const mockConfidentialAssetsServiceProvider: ValueProvider = { + provide: ConfidentialAssetsService, + useValue: createMock(), }; -export const mockMetadataServiceProvider: ValueProvider = { - provide: MetadataService, - useValue: createMock(), +export const mockConfidentialAccountsServiceProvider: ValueProvider = { + provide: ConfidentialAccountsService, + useValue: createMock(), }; -export const mockSubsidyServiceProvider: ValueProvider = { - provide: SubsidyService, - useValue: createMock(), -}; - -export const mockNftsServiceProvider: ValueProvider = { - provide: NftsService, - useValue: createMock(), -}; - -/** - * Given a set of key values to use as config, will wrap and return as a Nest "provider" for config - */ -export const makeMockConfigProvider = (config: Record): ServiceProvider => { - return { - useValue: { - get: (key: string): unknown => config[key], - getOrThrow: (key: string): unknown => { - const value = config[key]; - if (value) { - return value; - } else { - throw new Error(`mock config error: "${key}" was not found`); - } - }, - }, - provide: ConfigService, +export const mockConfidentialTransactionsServiceProvider: ValueProvider = + { + provide: ConfidentialTransactionsService, + useValue: createMock(), }; -}; - -export const mockNetworkServiceProvider: ValueProvider = { - provide: NetworkService, - useValue: createMock(), -}; -export const mockClaimsServiceProvider: ValueProvider = { - provide: ClaimsService, - useValue: createMock(), +export const mockConfidentialProofsServiceProvider: ValueProvider = { + provide: ConfidentialProofsService, + useValue: createMock(), }; diff --git a/src/test-utils/types.ts b/src/test-utils/types.ts index b6d25281..984dbe41 100644 --- a/src/test-utils/types.ts +++ b/src/test-utils/types.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { Class } from '~/common/types'; +import { Class } from '~/polymesh-rest-api/src/common/types'; export type ValidCase = [string, Record]; diff --git a/src/ticker-reservations/dto/reserve-ticker.dto.ts b/src/ticker-reservations/dto/reserve-ticker.dto.ts deleted file mode 100644 index 7c4b7de0..00000000 --- a/src/ticker-reservations/dto/reserve-ticker.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -import { IsTicker } from '~/common/decorators/validation'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; - -export class ReserveTickerDto extends TransactionBaseDto { - @ApiProperty({ - type: 'string', - description: 'Ticker to be reserved', - example: 'TICKER', - }) - @IsTicker() - readonly ticker: string; -} diff --git a/src/ticker-reservations/models/extended-ticker-reservation.model.ts b/src/ticker-reservations/models/extended-ticker-reservation.model.ts deleted file mode 100644 index 73300a9c..00000000 --- a/src/ticker-reservations/models/extended-ticker-reservation.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; - -export class ExtendedTickerReservationModel extends TransactionQueueModel { - @ApiProperty({ - description: 'Details of the Ticker Reservation', - type: TickerReservationModel, - }) - @Type(() => TickerReservationModel) - readonly tickerReservation: TickerReservationModel; - - constructor(model: ExtendedTickerReservationModel) { - const { transactions, details, ...rest } = model; - super({ transactions, details }); - - Object.assign(this, rest); - } -} diff --git a/src/ticker-reservations/models/ticker-reservation.model.ts b/src/ticker-reservations/models/ticker-reservation.model.ts deleted file mode 100644 index ade0d3aa..00000000 --- a/src/ticker-reservations/models/ticker-reservation.model.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { Identity, TickerReservationStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { FromEntity } from '~/common/decorators/transformation'; - -export class TickerReservationModel { - @ApiProperty({ - description: - "The DID of the Reservation owner. A null value means the ticker isn't currently reserved", - type: 'string', - example: '0x0600000000000000000000000000000000000000000000000000000000000000', - nullable: true, - }) - @FromEntity() - readonly owner: Identity | null; - - @ApiProperty({ - description: - 'Date at which the Reservation expires. A null value means it never expires (permanent Reservation or Asset already launched)', - type: 'string', - example: new Date('05/23/2021').toISOString(), - nullable: true, - }) - readonly expiryDate: Date | null; - - @ApiProperty({ - description: 'Status of the ticker Reservation', - type: 'string', - enum: TickerReservationStatus, - example: TickerReservationStatus.Free, - }) - readonly status: TickerReservationStatus; - - constructor(model: TickerReservationModel) { - Object.assign(this, model); - } -} diff --git a/src/ticker-reservations/ticker-reservations.controller.spec.ts b/src/ticker-reservations/ticker-reservations.controller.spec.ts deleted file mode 100644 index fadb1284..00000000 --- a/src/ticker-reservations/ticker-reservations.controller.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TickerReservationStatus } from '@polymeshassociation/polymesh-sdk/types'; - -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { testValues } from '~/test-utils/consts'; -import { MockAuthorizationRequest, MockIdentity, MockTickerReservation } from '~/test-utils/mocks'; -import { MockTickerReservationsService } from '~/test-utils/service-mocks'; -import { TickerReservationsController } from '~/ticker-reservations/ticker-reservations.controller'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -describe('TickerReservationsController', () => { - let controller: TickerReservationsController; - const { signer, txResult, dryRun } = testValues; - const mockTickerReservationsService = new MockTickerReservationsService(); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TickerReservationsController], - providers: [TickerReservationsService], - }) - .overrideProvider(TickerReservationsService) - .useValue(mockTickerReservationsService) - .compile(); - - controller = module.get(TickerReservationsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('reserve', () => { - it('should call the service and return the results', async () => { - mockTickerReservationsService.reserve.mockResolvedValue(txResult); - - const ticker = 'SOME_TICKER'; - const result = await controller.reserve({ ticker, signer }); - - expect(result).toEqual(txResult); - expect(mockTickerReservationsService.reserve).toHaveBeenCalledWith(ticker, { signer }); - }); - }); - - describe('getDetails', () => { - it('should call the service and return the details', async () => { - const mockDetails = { - owner: '0x6000', - expiryDate: null, - status: TickerReservationStatus.AssetCreated, - }; - const mockTickerReservation = new MockTickerReservation(); - mockTickerReservation.details.mockResolvedValue(mockDetails); - mockTickerReservationsService.findOne.mockResolvedValue(mockTickerReservation); - - const ticker = 'SOME_TICKER'; - const result = await controller.getDetails({ ticker }); - - expect(result).toEqual(mockDetails); - expect(mockTickerReservationsService.findOne).toHaveBeenCalledWith(ticker); - }); - }); - - describe('transferOwnership', () => { - it('should call the service and return the results', async () => { - const mockAuthorization = new MockAuthorizationRequest(); - const mockData = { - ...txResult, - result: mockAuthorization, - }; - mockTickerReservationsService.transferOwnership.mockResolvedValue(mockData); - - const body = { signer, target: '0x1000' }; - const ticker = 'SOME_TICKER'; - - const result = await controller.transferOwnership({ ticker }, body); - - expect(result).toEqual({ - ...txResult, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any), - }); - expect(mockTickerReservationsService.transferOwnership).toHaveBeenCalledWith(ticker, body); - }); - }); - - describe('extendReservation', () => { - it('should call the service and return the results', async () => { - const mockDate = new Date(); - const mockResult = { - owner: new MockIdentity(), - expiryDate: mockDate, - status: TickerReservationStatus.Reserved, - }; - - const mockTickerReservation = new MockTickerReservation(); - mockTickerReservation.details.mockResolvedValue(mockResult); - - const mockData = { - ...txResult, - result: mockTickerReservation, - }; - mockTickerReservationsService.extend.mockResolvedValue(mockData); - - const webhookUrl = 'http://example.com/webhook'; - const ticker = 'SOME_TICKER'; - - const result = await controller.extendReservation({ ticker }, { signer, webhookUrl, dryRun }); - - expect(result).toEqual({ - ...txResult, - tickerReservation: mockResult, - }); - expect(mockTickerReservationsService.extend).toHaveBeenCalledWith(ticker, { - signer, - webhookUrl, - dryRun, - }); - }); - }); -}); diff --git a/src/ticker-reservations/ticker-reservations.controller.ts b/src/ticker-reservations/ticker-reservations.controller.ts deleted file mode 100644 index 3c43e2c7..00000000 --- a/src/ticker-reservations/ticker-reservations.controller.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnprocessableEntityResponse, -} from '@nestjs/swagger'; -import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerParamsDto } from '~/assets/dto/ticker-params.dto'; -import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util'; -import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model'; -import { ApiTransactionResponse } from '~/common/decorators/swagger'; -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { TransactionQueueModel } from '~/common/models/transaction-queue.model'; -import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils'; -import { ReserveTickerDto } from '~/ticker-reservations/dto/reserve-ticker.dto'; -import { ExtendedTickerReservationModel } from '~/ticker-reservations/models/extended-ticker-reservation.model'; -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; -import { createTickerReservationModel } from '~/ticker-reservations/ticker-reservations.util'; - -@ApiTags('ticker-reservations') -@Controller('ticker-reservations') -export class TickerReservationsController { - constructor(private readonly tickerReservationsService: TickerReservationsService) {} - - @ApiOperation({ - summary: 'Reserve a Ticker', - description: 'Reserves a ticker so that an Asset can be created with it later', - }) - @ApiTransactionResponse({ - description: 'Details about the transaction', - type: TransactionQueueModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'The ticker has already been reserved', - }) - @Post('reserve-ticker') - public async reserve( - @Body() { ticker, ...transactionBaseDto }: ReserveTickerDto - ): Promise { - const result = await this.tickerReservationsService.reserve(ticker, transactionBaseDto); - - return handleServiceResult(result); - } - - @ApiOperation({ - summary: 'Get ticker reservation details', - description: 'This endpoint returns details of ticker reservation', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose details are to be fetched', - type: 'string', - example: 'TICKER', - }) - @ApiOkResponse({ - description: 'Details of the ticker reservation', - type: TickerReservationModel, - }) - @Get(':ticker') - public async getDetails(@Param() { ticker }: TickerParamsDto): Promise { - const tickerReservation = await this.tickerReservationsService.findOne(ticker); - return createTickerReservationModel(tickerReservation); - } - - @ApiOperation({ - summary: 'Transfer ownership of the ticker Reservation', - description: - 'This endpoint transfers ownership of the ticker Reservation to `target` Identity. This generates an authorization request that must be accepted by the `target` Identity', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose ownership is to be transferred', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Newly created Authorization Request along with transaction details', - type: CreatedAuthorizationRequestModel, - }) - @ApiUnprocessableEntityResponse({ - description: 'Asset has already been created for the ticker', - }) - @Post(':ticker/transfer-ownership') - public async transferOwnership( - @Param() { ticker }: TickerParamsDto, - @Body() params: TransferOwnershipDto - ): Promise { - const serviceResult = await this.tickerReservationsService.transferOwnership(ticker, params); - - const resolver: TransactionResolver = ({ - transactions, - details, - result, - }) => - new CreatedAuthorizationRequestModel({ - transactions, - details, - authorizationRequest: createAuthorizationRequestModel(result), - }); - - return handleServiceResult(serviceResult, resolver); - } - - @ApiOperation({ - summary: 'Extend ticker reservation', - description: - 'This endpoint extends the time period of a ticker reservation for 60 days from now', - }) - @ApiParam({ - name: 'ticker', - description: 'Ticker whose expiry date is to be extended', - type: 'string', - example: 'TICKER', - }) - @ApiTransactionResponse({ - description: 'Details of extended ticker reservation along with transaction details', - type: ExtendedTickerReservationModel, - }) - @ApiUnprocessableEntityResponse({ - description: - '
    ' + - '
  • Asset has already been created for the ticker
  • ' + - '
  • Ticker not reserved or the Reservation has expired
  • ' + - '
', - }) - @Post(':ticker/extend') - public async extendReservation( - @Param() { ticker }: TickerParamsDto, - @Body() transactionBaseDto: TransactionBaseDto - ): Promise { - const serviceResult = await this.tickerReservationsService.extend(ticker, transactionBaseDto); - - const resolver: TransactionResolver = async ({ - transactions, - details, - result, - }) => - new ExtendedTickerReservationModel({ - transactions, - details, - tickerReservation: await createTickerReservationModel(result), - }); - - return handleServiceResult(serviceResult, resolver); - } -} diff --git a/src/ticker-reservations/ticker-reservations.module.ts b/src/ticker-reservations/ticker-reservations.module.ts deleted file mode 100644 index 4b42366d..00000000 --- a/src/ticker-reservations/ticker-reservations.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { TickerReservationsController } from '~/ticker-reservations/ticker-reservations.controller'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; -import { TransactionsModule } from '~/transactions/transactions.module'; - -@Module({ - imports: [PolymeshModule, TransactionsModule], - controllers: [TickerReservationsController], - providers: [TickerReservationsService], - exports: [TickerReservationsService], -}) -export class TickerReservationsModule {} diff --git a/src/ticker-reservations/ticker-reservations.service.spec.ts b/src/ticker-reservations/ticker-reservations.service.spec.ts deleted file mode 100644 index 52fa806a..00000000 --- a/src/ticker-reservations/ticker-reservations.service.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable import/first */ -const mockIsPolymeshTransaction = jest.fn(); - -import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { POLYMESH_API } from '~/polymesh/polymesh.consts'; -import { PolymeshModule } from '~/polymesh/polymesh.module'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { testValues } from '~/test-utils/consts'; -import { - MockAuthorizationRequest, - MockPolymesh, - MockTickerReservation, - MockTransaction, -} from '~/test-utils/mocks'; -import { mockTransactionsProvider, MockTransactionsService } from '~/test-utils/service-mocks'; -import { TickerReservationsService } from '~/ticker-reservations/ticker-reservations.service'; - -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), - isPolymeshTransaction: mockIsPolymeshTransaction, -})); - -describe('TickerReservationsService', () => { - let service: TickerReservationsService; - let polymeshService: PolymeshService; - let mockPolymeshApi: MockPolymesh; - let mockTransactionsService: MockTransactionsService; - const { signer } = testValues; - - beforeEach(async () => { - mockPolymeshApi = new MockPolymesh(); - mockTransactionsService = mockTransactionsProvider.useValue; - - const module: TestingModule = await Test.createTestingModule({ - imports: [PolymeshModule], - providers: [TickerReservationsService, mockTransactionsProvider], - }) - .overrideProvider(POLYMESH_API) - .useValue(mockPolymeshApi) - .compile(); - - polymeshService = module.get(PolymeshService); - service = module.get(TickerReservationsService); - - mockIsPolymeshTransaction.mockReturnValue(true); - }); - - afterEach(async () => { - await polymeshService.close(); - }); - - afterAll(() => { - mockIsPolymeshTransaction.mockReset(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findOne', () => { - it('should return the reservation', async () => { - const mockTickerReservation = new MockTickerReservation(); - mockPolymeshApi.assets.getTickerReservation.mockResolvedValue(mockTickerReservation); - - const result = await service.findOne('TICKER'); - expect(result).toEqual(mockTickerReservation); - }); - }); - - describe('reserve', () => { - const ticker = 'TICKER'; - - it('should run a reserveTicker procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterTicker, - }; - const mockResult = new MockTickerReservation(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const result = await service.reserve(ticker, { signer }); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('transferOwnership', () => { - const ticker = 'TICKER'; - const body = { - signer: '0x6000', - target: '0x1000', - expiry: new Date(), - }; - let mockTickerReservation: MockTickerReservation; - - beforeEach(() => { - mockTickerReservation = new MockTickerReservation(); - }); - - it('should run a transferOwnership procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.identity.AddAuthorization, - }; - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockTickerReservation as any); - - const mockResult = new MockAuthorizationRequest(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - mockTickerReservation.transferOwnership.mockResolvedValue(mockTransaction); - - const result = await service.transferOwnership(ticker, body); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('extend', () => { - const ticker = 'TICKER'; - let mockTickerReservation: MockTickerReservation; - - beforeEach(() => { - mockTickerReservation = new MockTickerReservation(); - }); - - it('should run a extend procedure and return the queue data', async () => { - const transaction = { - blockHash: '0x1', - txHash: '0x2', - blockNumber: new BigNumber(1), - tag: TxTags.asset.RegisterTicker, - }; - - const findOneSpy = jest.spyOn(service, 'findOne'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - findOneSpy.mockResolvedValue(mockTickerReservation as any); - - const mockResult = new MockTickerReservation(); - - const mockTransaction = new MockTransaction(transaction); - mockTransactionsService.submit.mockResolvedValue({ - result: mockResult, - transactions: [mockTransaction], - }); - - const result = await service.extend(ticker, { signer }); - expect(result).toEqual({ - result: mockResult, - transactions: [mockTransaction], - }); - }); - }); - - describe('findAllByOwner', () => { - it('should return the list of TickerReservations', async () => { - const mockTickerReservation = new MockTickerReservation(); - mockPolymeshApi.assets.getTickerReservations.mockResolvedValue([mockTickerReservation]); - - const result = await service.findAllByOwner('0x6000'); - expect(result).toEqual([mockTickerReservation]); - }); - }); -}); diff --git a/src/ticker-reservations/ticker-reservations.service.ts b/src/ticker-reservations/ticker-reservations.service.ts deleted file mode 100644 index f29ec458..00000000 --- a/src/ticker-reservations/ticker-reservations.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthorizationRequest, TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto'; -import { extractTxBase, ServiceReturn } from '~/common/utils'; -import { PolymeshService } from '~/polymesh/polymesh.service'; -import { TransactionsService } from '~/transactions/transactions.service'; - -@Injectable() -export class TickerReservationsService { - constructor( - private readonly polymeshService: PolymeshService, - private readonly transactionsService: TransactionsService - ) {} - - public async findOne(ticker: string): Promise { - return this.polymeshService.polymeshApi.assets.getTickerReservation({ - ticker, - }); - } - - public async reserve( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { transactionsService, polymeshService } = this; - const { reserveTicker } = polymeshService.polymeshApi.assets; - - return transactionsService.submit(reserveTicker, { ticker }, transactionBaseDto); - } - - public async transferOwnership( - ticker: string, - params: TransferOwnershipDto - ): ServiceReturn { - const { base, args } = extractTxBase(params); - - const { transferOwnership } = await this.findOne(ticker); - return this.transactionsService.submit(transferOwnership, args, base); - } - - public async extend( - ticker: string, - transactionBaseDto: TransactionBaseDto - ): ServiceReturn { - const { extend } = await this.findOne(ticker); - - return this.transactionsService.submit(extend, {}, transactionBaseDto); - } - - public async findAllByOwner(owner: string): Promise { - const { - polymeshService: { polymeshApi }, - } = this; - return polymeshApi.assets.getTickerReservations({ owner }); - } -} diff --git a/src/ticker-reservations/ticker-reservations.util.ts b/src/ticker-reservations/ticker-reservations.util.ts deleted file mode 100644 index d430d85b..00000000 --- a/src/ticker-reservations/ticker-reservations.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { TickerReservation } from '@polymeshassociation/polymesh-sdk/types'; - -import { TickerReservationModel } from '~/ticker-reservations/models/ticker-reservation.model'; - -export async function createTickerReservationModel( - tickerReservation: TickerReservation -): Promise { - const { owner, expiryDate, status } = await tickerReservation.details(); - - return new TickerReservationModel({ owner, expiryDate, status }); -} diff --git a/src/transactions/dto/payload.dto.ts b/src/transactions/dto/payload.dto.ts new file mode 100644 index 00000000..6e4e99a8 --- /dev/null +++ b/src/transactions/dto/payload.dto.ts @@ -0,0 +1,107 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsHexadecimal, IsNumber, IsString, Length } from 'class-validator'; + +export class PayloadDto { + @ApiProperty({ + type: 'string', + description: 'The transaction spec version. This changes when the chain gets upgraded', + example: '0x005b8d84', + }) + @IsHexadecimal() + readonly specVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The transaction version', + example: '0x00000004', + }) + @IsHexadecimal() + readonly transactionVersion: string; + + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block hash when this transaction was created. Used to control transaction lifetime', + example: '0xec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + @IsHexadecimal() + @Length(66) + readonly blockHash: string; + + @ApiProperty({ + type: 'string', + description: + 'The latest block number when this transaction was created. Used to control transaction lifetime (Alternative to block hash)', + example: '0x00000000', + }) + @IsHexadecimal() + readonly blockNumber: string; + + @ApiProperty({ + type: 'string', + description: 'How long this transaction is valid for', + example: '0xc501', + }) + @IsHexadecimal() + readonly era: string; + + @ApiProperty({ + type: 'string', + description: 'The chain this transaction is intended for', + example: '0xfbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286a', + }) + @IsHexadecimal() + @Length(66) + readonly genesisHash: string; + + @ApiProperty({ + type: 'string', + description: 'The hex encoded transaction details', + example: '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000', + }) + @IsHexadecimal() + readonly method: `0x${string}`; + + @ApiProperty({ + type: 'string', + description: 'The account nonce', + example: '0x00000007', + }) + @IsHexadecimal() + readonly nonce: string; + + @ApiProperty({ + type: 'string', + description: 'Signed extensions', + isArray: true, + example: [], + }) + @IsString({ each: true }) + readonly signedExtensions: string[]; + + @ApiProperty({ + type: 'string', + example: '0x00000000000000000000000000000000', + description: 'Additional fees paid (Should be 0 for Polymesh)', + }) + @IsHexadecimal() + tip: `0x${string}`; + + @ApiProperty({ + type: 'number', + example: 4, + description: 'The transaction version', + }) + @IsNumber() + readonly version: number; +} diff --git a/src/transactions/dto/raw-payload.dto.ts b/src/transactions/dto/raw-payload.dto.ts new file mode 100644 index 00000000..500d1089 --- /dev/null +++ b/src/transactions/dto/raw-payload.dto.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsHexadecimal, IsString } from 'class-validator'; + +export class RawPayloadDto { + @ApiProperty({ + type: 'string', + description: 'The signing address', + example: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + type: 'string', + description: 'The raw transaction hex encoded', + example: + '0x1a075449434b455200000000000000ca9a3b00000000000000000000000000c5011c00848d5b0004000000fbd550612d800930567fda9db77af4591823bcee65812194c5eae52da2a1286aec1d41dd553ce03c3e462aab8bcfba0e1726e6bf310db6e06a933bf0430419c0', + }) + @IsHexadecimal() + readonly data: string; + + @ApiProperty({ + type: 'string', + description: 'The type of `data`', + example: 'payload', + }) + @IsString() + readonly type: 'payload' | 'bytes'; +} diff --git a/src/transactions/dto/transaction.dto.ts b/src/transactions/dto/transaction.dto.ts new file mode 100644 index 00000000..1065338a --- /dev/null +++ b/src/transactions/dto/transaction.dto.ts @@ -0,0 +1,50 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsHexadecimal, IsObject, ValidateNested } from 'class-validator'; + +import { PayloadDto } from '~/transactions/dto/payload.dto'; +import { RawPayloadDto } from '~/transactions/dto/raw-payload.dto'; + +export class TransactionDto { + @ApiProperty({ + type: 'string', + description: + 'The signature for the transaction (note: the first byte indicates key type `00` for ed25519 `01` for sr25519)', + example: + '0x012016ceb0854616be2feed01212aa42815a92d2ae34feae3a0924058563ca81042933ebc25303e7d79026f734d867da4de106d22c0fb22a0a8303a9b0be49bd8f', + }) + @IsHexadecimal() + readonly signature: string; + + @ApiProperty({ + type: 'string', + description: 'The method of the transaction', + example: '0x80041a075449434b455200000000000000ca9a3b00000000000000000000000000', + }) + @IsHexadecimal() + readonly method: `0x${string}`; + + @ApiProperty({ + type: PayloadDto, + description: 'The transaction payload', + }) + @Type(() => PayloadDto) + @ValidateNested() + readonly payload: PayloadDto; + + @ApiProperty({ + type: RawPayloadDto, + description: 'The raw transaction payload', + }) + @Type(() => RawPayloadDto) + @ValidateNested() + readonly rawPayload: RawPayloadDto; + + @ApiProperty({ + description: 'Additional information associated with the transaction', + }) + @IsObject() + readonly metadata: Record; +} diff --git a/src/transactions/models/extrinsic-details.model.ts b/src/transactions/models/extrinsic-details.model.ts index 9fb99253..9a8f4029 100644 --- a/src/transactions/models/extrinsic-details.model.ts +++ b/src/transactions/models/extrinsic-details.model.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ import { ApiProperty } from '@nestjs/swagger'; -import { ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-sdk/types'; +import { ExtrinsicDataWithFees } from '@polymeshassociation/polymesh-private-sdk/types'; import { Type } from 'class-transformer'; -import { ExtrinsicModel } from '~/common/models/extrinsic.model'; -import { FeesModel } from '~/common/models/fees.model'; +import { ExtrinsicModel } from '~/polymesh-rest-api/src/common/models/extrinsic.model'; +import { FeesModel } from '~/polymesh-rest-api/src/common/models/fees.model'; export class ExtrinsicDetailsModel extends ExtrinsicModel { @ApiProperty({ diff --git a/src/transactions/models/submit-result.model.ts b/src/transactions/models/submit-result.model.ts new file mode 100644 index 00000000..3700ff47 --- /dev/null +++ b/src/transactions/models/submit-result.model.ts @@ -0,0 +1,34 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; + +import { FromBigNumber } from '~/polymesh-rest-api/src/common/decorators/transformation'; + +export class SubmitResultModel { + @ApiProperty({ + description: 'The block hash the transaction was included in', + example: '0x08e8dc9104dbe8a6f38b59c2a44b29348e1a204824fe8514ae3c40a015210d9e', + type: 'string', + }) + readonly blockHash: string; + + @ApiProperty({ + description: 'The index of the transaction within the block', + type: 'string', + example: '1', + }) + @FromBigNumber() + readonly transactionIndex: BigNumber; + + @ApiProperty({ + description: 'The transaction hash of the submitted transaction', + type: 'string', + example: '0x92cfb6d8cd3186e46e3cc7319ea0bca0f6a990026b30519e1cb43bb8351b3650', + }) + readonly transactionHash: string; + + constructor(model: SubmitResultModel) { + Object.assign(this, model); + } +} diff --git a/src/transactions/transactions.controller.spec.ts b/src/transactions/transactions.controller.spec.ts index 4db02920..57f4b633 100644 --- a/src/transactions/transactions.controller.spec.ts +++ b/src/transactions/transactions.controller.spec.ts @@ -5,7 +5,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NetworkService } from '~/network/network.service'; import { extrinsicWithFees } from '~/test-utils/consts'; import { mockNetworkServiceProvider } from '~/test-utils/service-mocks'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; +import { SubmitResultModel } from '~/transactions/models/submit-result.model'; import { TransactionsController } from '~/transactions/transactions.controller'; describe('TransactionsController', () => { @@ -46,4 +48,23 @@ describe('TransactionsController', () => { }); }); }); + + describe('submit', () => { + it('should call the service and return the results', async () => { + const body = { + method: '0x01', + signature: '0x02', + payload: {}, + rawPayload: {}, + } as unknown as TransactionDto; + + const txResult = 'fakeResult' as unknown as SubmitResultModel; + + mockNetworkService.submitTransaction.mockResolvedValue(txResult); + + const result = await controller.submitTransaction(body); + expect(result).toEqual(txResult); + expect(mockNetworkService.submitTransaction).toHaveBeenCalledWith(body); + }); + }); }); diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index b2f77911..072fc791 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,10 +1,12 @@ -import { Controller, Get, HttpStatus, NotFoundException, Param } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiTransactionFailedResponse } from '~/common/decorators/swagger'; -import { NetworkService } from '~/network/network.service'; +import { ApiTransactionFailedResponse } from '~/polymesh-rest-api/src/common/decorators/swagger'; +import { NetworkService } from '~/polymesh-rest-api/src/network/network.service'; +import { TransactionDto } from '~/transactions/dto/transaction.dto'; import { TransactionHashParamsDto } from '~/transactions/dto/transaction-hash-params.dto'; import { ExtrinsicDetailsModel } from '~/transactions/models/extrinsic-details.model'; +import { SubmitResultModel } from '~/transactions/models/submit-result.model'; @ApiTags('transactions') @Controller('transactions') @@ -42,4 +44,18 @@ export class TransactionsController { return new ExtrinsicDetailsModel(result); } + + @ApiOperation({ + summary: 'Submit an offline transaction with its signature', + description: + 'This endpoint allows for a transaction that has been signed offline to be submitted with its signature. For example when the transaction was made from using the option `processMode: "offline"`, this endpoint will attach the signature and forward it to the chain', + }) + @ApiOkResponse({ + description: 'Information about the block the transaction was included in', + type: SubmitResultModel, + }) + @Post('/submit') + public async submitTransaction(@Body() transaction: TransactionDto): Promise { + return this.networkService.submitTransaction(transaction); + } } diff --git a/src/transactions/transactions.module.ts b/src/transactions/transactions.module.ts index 94464212..549500b8 100644 --- a/src/transactions/transactions.module.ts +++ b/src/transactions/transactions.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { EventsModule } from '~/events/events.module'; -import { LoggerModule } from '~/logger/logger.module'; -import { NetworkModule } from '~/network/network.module'; -import { SigningModule } from '~/signing/signing.module'; -import { SubscriptionsModule } from '~/subscriptions/subscriptions.module'; +import { PolymeshModule } from '~/polymesh/polymesh.module'; +import { EventsModule } from '~/polymesh-rest-api/src/events/events.module'; +import { LoggerModule } from '~/polymesh-rest-api/src/logger/logger.module'; +import { NetworkModule } from '~/polymesh-rest-api/src/network/network.module'; +import { OfflineStarterModule } from '~/polymesh-rest-api/src/offline-starter/offline-starter.module'; +import { SigningModule } from '~/polymesh-rest-api/src/signing/signing.module'; +import { SubscriptionsModule } from '~/polymesh-rest-api/src/subscriptions/subscriptions.module'; import transactionsConfig from '~/transactions/config/transactions.config'; import { TransactionsController } from '~/transactions/transactions.controller'; import { TransactionsService } from '~/transactions/transactions.service'; @@ -18,6 +20,8 @@ import { TransactionsService } from '~/transactions/transactions.service'; SubscriptionsModule, LoggerModule, NetworkModule, + OfflineStarterModule, + PolymeshModule, ], providers: [TransactionsService], exports: [TransactionsService], diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index 16342830..0dc35d6b 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -1,32 +1,50 @@ /* eslint-disable import/first */ const mockIsPolymeshTransaction = jest.fn(); const mockIsPolymeshTransactionBatch = jest.fn(); +const mockIsPolymeshError = jest.fn(); +import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; -import { ProcedureOpts, TransactionStatus, TxTags } from '@polymeshassociation/polymesh-sdk/types'; - -import { TransactionType } from '~/common/types'; -import { EventsService } from '~/events/events.service'; -import { EventType } from '~/events/types'; -import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; -import { mockSigningProvider } from '~/signing/signing.mock'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; +import { SignerPayloadJSON } from '@polkadot/types/types'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; +import { + ProcedureOpts, + TransactionStatus, + TxTags, +} from '@polymeshassociation/polymesh-private-sdk/types'; +import { when } from 'jest-when'; + +import { AppInternalError } from '~/polymesh-rest-api/src/common/errors'; +import { ProcessMode, TransactionType } from '~/polymesh-rest-api/src/common/types'; +import { AddressName } from '~/polymesh-rest-api/src/common/utils/amqp'; +import { EventsService } from '~/polymesh-rest-api/src/events/events.service'; +import { EventType } from '~/polymesh-rest-api/src/events/types'; +import { mockPolymeshLoggerProvider } from '~/polymesh-rest-api/src/logger/mock-polymesh-logger'; +import { OfflineReceiptModel } from '~/polymesh-rest-api/src/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/polymesh-rest-api/src/offline-starter/offline-starter.service'; +import { SigningService } from '~/polymesh-rest-api/src/signing/services'; +import { mockSigningProvider } from '~/polymesh-rest-api/src/signing/signing.mock'; +import { SubscriptionsService } from '~/polymesh-rest-api/src/subscriptions/subscriptions.service'; import { CallbackFn, MockPolymeshTransaction, MockPolymeshTransactionBatch, } from '~/test-utils/mocks'; -import { MockEventsService, MockSubscriptionsService } from '~/test-utils/service-mocks'; +import { + MockEventsService, + mockOfflineStarterProvider, + MockSubscriptionsService, +} from '~/test-utils/service-mocks'; import transactionsConfig from '~/transactions/config/transactions.config'; import { TransactionsService } from '~/transactions/transactions.service'; import { TransactionResult } from '~/transactions/transactions.util'; import { Transaction } from '~/transactions/types'; -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), +jest.mock('@polymeshassociation/polymesh-private-sdk/utils', () => ({ + ...jest.requireActual('@polymeshassociation/polymesh-private-sdk/utils'), isPolymeshTransaction: mockIsPolymeshTransaction, isPolymeshTransactionBatch: mockIsPolymeshTransactionBatch, + isPolymeshError: mockIsPolymeshError, })); /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -47,11 +65,12 @@ const makeMockMethod = ( describe('TransactionsService', () => { const signer = 'signer'; const legitimacySecret = 'someSecret'; - const dryRun = false; let service: TransactionsService; let mockEventsService: MockEventsService; let mockSubscriptionsService: MockSubscriptionsService; + let mockSigningService: DeepMocked; + let mockOfflineStarterService: DeepMocked; beforeEach(async () => { mockEventsService = new MockEventsService(); @@ -64,6 +83,7 @@ describe('TransactionsService', () => { EventsService, SubscriptionsService, mockSigningProvider, + mockOfflineStarterProvider, { provide: transactionsConfig.KEY, useValue: { legitimacySecret }, @@ -77,6 +97,8 @@ describe('TransactionsService', () => { .compile(); service = module.get(TransactionsService); + mockOfflineStarterService = module.get(OfflineStarterService); + mockSigningService = module.get(SigningService); }); afterEach(() => { @@ -88,19 +110,36 @@ describe('TransactionsService', () => { expect(service).toBeDefined(); }); + describe('getSigningAccount', () => { + const address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; + it('should return the address if given an address', async () => { + mockSigningService.isAddress.mockReturnValue(true); + const result = await service.getSigningAccount(address); + + expect(result).toEqual(address); + }); + + it('should return the address when given a handle', async () => { + mockSigningService.isAddress.mockReturnValue(false); + when(mockSigningService.getAddressByHandle).calledWith('alice').mockResolvedValue(address); + const result = await service.getSigningAccount('alice'); + + expect(result).toEqual(address); + }); + }); + describe('submit (without webhookUrl)', () => { it('should process the transaction and return the result', async () => { const transaction: MockPolymeshTransaction = new MockPolymeshTransaction(); mockIsPolymeshTransaction.mockReturnValue(true); const mockMethod = makeMockMethod(transaction); - const { result, transactions, details } = (await service.submit( + const { transactions, details } = (await service.submit( mockMethod, {}, - { signer } + { signer, processMode: ProcessMode.Submit } )) as TransactionResult; - expect(result).toBeUndefined(); expect(transactions).toEqual([ { blockHash: undefined, @@ -119,14 +158,12 @@ describe('TransactionsService', () => { mockIsPolymeshTransactionBatch.mockReturnValue(true); const mockMethod = makeMockMethod(transaction); - const { result, transactions, details } = (await service.submit( + const { transactions, details } = (await service.submit( mockMethod, {}, - { signer } + { signer, processMode: ProcessMode.Submit } )) as TransactionResult; - expect(result).toBeUndefined(); - expect(transactions).toEqual([ { blockHash: undefined, @@ -139,6 +176,78 @@ describe('TransactionsService', () => { expect(details).toBeDefined(); }); + + it('should forward SDK params when present', async () => { + const transaction: MockPolymeshTransactionBatch = new MockPolymeshTransactionBatch(); + mockIsPolymeshTransaction.mockReturnValue(false); + mockIsPolymeshTransactionBatch.mockReturnValue(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockMethod: any = jest.fn().mockResolvedValue(transaction); + + const mortality = { + immortal: true, + }; + + const nonce = new BigNumber(7); + + await service.submit( + mockMethod, + {}, + { signer, processMode: ProcessMode.Submit, mortality, nonce } + ); + + expect(mockMethod).toHaveBeenCalledWith(expect.objectContaining({ nonce, mortality })); + }); + + it('should transform SDK errors into app errors', async () => { + const transaction = new MockPolymeshTransaction(); + mockIsPolymeshTransaction.mockReturnValue(true); + const mockMethod = makeMockMethod(transaction); + + mockIsPolymeshError.mockReturnValue(true); + const fakeSdkError = new Error('fake error'); + transaction.run.mockRejectedValue(fakeSdkError); + + await expect( + service.submit(mockMethod, {}, { signer, processMode: ProcessMode.Submit }) + ).rejects.toThrow(AppInternalError); + }); + + it('should throw an error if unknown details are received', async () => { + const transaction: MockPolymeshTransactionBatch = new MockPolymeshTransactionBatch(); + mockIsPolymeshTransaction.mockReturnValue(false); + mockIsPolymeshTransactionBatch.mockReturnValue(false); + const mockMethod = makeMockMethod(transaction); + + await expect( + service.submit(mockMethod, {}, { signer, processMode: ProcessMode.Submit }) + ).rejects.toThrow(AppInternalError); + }); + }); + + describe('submit (with AMQP)', () => { + const fakeReceipt = new OfflineReceiptModel({ + id: 'someId', + deliveryId: new BigNumber(1), + topicName: AddressName.Requests, + payload: {} as SignerPayloadJSON, + metadata: {}, + }); + it('should call the offline starter when given AMQP process mode', async () => { + mockOfflineStarterService.beginTransaction.mockResolvedValue(fakeReceipt); + + const transaction = new MockPolymeshTransactionBatch(); + + const mockMethod = makeMockMethod(transaction); + + const result = await service.submit( + mockMethod, + {}, + { signer, processMode: ProcessMode.AMQP } + ); + + expect(result).toEqual(fakeReceipt); + }); }); describe('submit (with webhookUrl)', () => { @@ -170,7 +279,11 @@ describe('TransactionsService', () => { mockIsPolymeshTransaction.mockReturnValue(true); - const result = await service.submit(mockMethod, {}, { signer, webhookUrl, dryRun }); + const result = await service.submit( + mockMethod, + {}, + { signer, webhookUrl, processMode: ProcessMode.SubmitWithCallback } + ); const expectedPayload = { type: TransactionType.Single, @@ -248,7 +361,11 @@ describe('TransactionsService', () => { const mockMethod = makeMockMethod(transaction); - const result = await service.submit(mockMethod, {}, { signer, webhookUrl, dryRun }); + const result = await service.submit( + mockMethod, + {}, + { signer, webhookUrl, processMode: ProcessMode.SubmitWithCallback } + ); expect(mockPolymeshLoggerProvider.useValue.error).toHaveBeenCalled(); diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index 39838b08..648b3afe 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -1,23 +1,30 @@ import { Inject, Injectable } from '@nestjs/common'; import { ConfigType } from '@nestjs/config'; -import { TransactionStatus } from '@polymeshassociation/polymesh-sdk/types'; -import { isPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/utils'; - -import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; -import { TransactionType } from '~/common/types'; -import { EventsService } from '~/events/events.service'; -import { EventType, TransactionUpdateEvent, TransactionUpdatePayload } from '~/events/types'; -import { PolymeshLogger } from '~/logger/polymesh-logger.service'; -import { NotificationPayload } from '~/notifications/types'; -import { SigningService } from '~/signing/services/signing.service'; -import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; -import { SubscriptionStatus } from '~/subscriptions/types'; +import { TransactionStatus } from '@polymeshassociation/polymesh-private-sdk/types'; +import { isPolymeshTransaction } from '@polymeshassociation/polymesh-private-sdk/utils'; + +import { TransactionOptionsDto } from '~/polymesh-rest-api/src/common/dto/transaction-options.dto'; +import { ProcessMode, TransactionType } from '~/polymesh-rest-api/src/common/types'; +import { EventsService } from '~/polymesh-rest-api/src/events/events.service'; +import { + EventType, + TransactionUpdateEvent, + TransactionUpdatePayload, +} from '~/polymesh-rest-api/src/events/types'; +import { PolymeshLogger } from '~/polymesh-rest-api/src/logger/polymesh-logger.service'; +import { NotificationPayload } from '~/polymesh-rest-api/src/notifications/types'; +import { OfflineReceiptModel } from '~/polymesh-rest-api/src/offline-starter/models/offline-receipt.model'; +import { OfflineStarterService } from '~/polymesh-rest-api/src/offline-starter/offline-starter.service'; +import { SigningService } from '~/polymesh-rest-api/src/signing/services'; +import { SubscriptionsService } from '~/polymesh-rest-api/src/subscriptions/subscriptions.service'; +import { SubscriptionStatus } from '~/polymesh-rest-api/src/subscriptions/types'; import transactionsConfig from '~/transactions/config/transactions.config'; import { handleSdkError, Method, prepareProcedure, processTransaction, + TransactionPayloadResult, TransactionResult, } from '~/transactions/transactions.util'; import { Transaction } from '~/transactions/types'; @@ -56,37 +63,50 @@ export class TransactionsService { private readonly subscriptionsService: SubscriptionsService, private readonly signingService: SigningService, // TODO @polymath-eric handle errors with specialized service - private readonly logger: PolymeshLogger + private readonly logger: PolymeshLogger, + private readonly offlineStarter: OfflineStarterService ) { logger.setContext(TransactionsService.name); this.legitimacySecret = config.legitimacySecret; } public async getSigningAccount(signer: string): Promise { + const isAddress = this.signingService.isAddress(signer); + if (isAddress) { + return signer; + } + return this.signingService.getAddressByHandle(signer); } public async submit( method: Method, args: MethodArgs, - transactionBaseDto: TransactionBaseDto - ): Promise> { - const { signer, webhookUrl, dryRun } = transactionBaseDto; + transactionOptions: TransactionOptionsDto + ): Promise< + | TransactionPayloadResult + | NotificationPayload + | TransactionResult + | OfflineReceiptModel + > { + const { processMode, signer, webhookUrl, mortality, nonce, metadata } = transactionOptions; const signingAccount = await this.getSigningAccount(signer); + const sdkOptions = { signingAccount, mortality, nonce }; try { - if (!webhookUrl) { - return processTransaction(method, args, { signingAccount }, dryRun); - } else { - // prepare the procedure so the SDK will run its validation and throw if something isn't right - const transaction = await prepareProcedure(method, args, { signingAccount }); - + const transaction = await prepareProcedure(method, args, sdkOptions); + if (processMode === ProcessMode.SubmitWithCallback) { return this.submitAndSubscribe( transaction as Transaction, - webhookUrl, + webhookUrl!, this.legitimacySecret ); + } else if (processMode === ProcessMode.AMQP) { + return this.offlineStarter.beginTransaction(transaction, metadata); + } else { + return processTransaction(method, args, sdkOptions, transactionOptions); } } catch (error) { + /* istanbul ignore next */ throw handleSdkError(error); } } diff --git a/src/transactions/transactions.util.spec.ts b/src/transactions/transactions.util.spec.ts index d865ef59..5919c05b 100644 --- a/src/transactions/transactions.util.spec.ts +++ b/src/transactions/transactions.util.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable import/first */ const mockIsPolymeshError = jest.fn(); +import { ErrorCode } from '@polymeshassociation/polymesh-private-sdk/types'; import { PolymeshError } from '@polymeshassociation/polymesh-sdk/base/PolymeshError'; -import { ErrorCode } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; import { @@ -12,17 +12,17 @@ import { AppUnauthorizedError, AppUnprocessableError, AppValidationError, -} from '~/common/errors'; -import { Class } from '~/common/types'; -import { MockVenue } from '~/test-utils/mocks'; +} from '~/polymesh-rest-api/src/common/errors'; +import { Class, ProcessMode } from '~/polymesh-rest-api/src/common/types'; +import { MockPolymeshTransaction, MockVenue } from '~/test-utils/mocks'; import { handleSdkError, prepareProcedure, processTransaction, } from '~/transactions/transactions.util'; -jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({ - ...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'), +jest.mock('@polymeshassociation/polymesh-private-sdk/utils', () => ({ + ...jest.requireActual('@polymeshassociation/polymesh-private-sdk/utils'), isPolymeshError: mockIsPolymeshError, })); @@ -47,10 +47,41 @@ describe('processTransaction', () => { mockIsPolymeshError.mockReturnValue(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await expect(processTransaction(mockVenue.modify as any, {}, {})).rejects.toBeInstanceOf( - expected - ); + await expect( + processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.Submit, signer: 'Alice' } + ) + ).rejects.toBeInstanceOf(expected); + + mockIsPolymeshError.mockReset(); + }); + + it('should catch address not present in signing manager errors', async () => { + const mockVenue = new MockVenue(); + + const mockError = { + code: ErrorCode.General, + message: 'The Account is not part of the Signing Manager attached to the SDK', + }; + mockVenue.modify.mockImplementation(() => { + throw mockError; + }); + + mockIsPolymeshError.mockReturnValue(true); + + await expect( + processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.Submit, signer: 'Alice' } + ) + ).rejects.toBeInstanceOf(AppValidationError); mockIsPolymeshError.mockReset(); }); @@ -59,8 +90,13 @@ describe('processTransaction', () => { describe('it should handle non polymesh errors', () => { it('should transform errors into AppInternalError', async () => { const mockVenue = new MockVenue(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = processTransaction(mockVenue.modify as any, {}, {}); + const result = processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.Submit, signer: 'Alice' } + ); mockVenue.modify.mockImplementationOnce(() => { throw new Error('Foo'); @@ -76,6 +112,48 @@ describe('processTransaction', () => { await expect(result).rejects.toBeInstanceOf(AppInternalError); }); }); + + describe('with dryRun', () => { + it('should handle dry run process mode', async () => { + const mockVenue = new MockVenue(); + + const mockTransaction = new MockPolymeshTransaction(); + const run = mockTransaction.run; + + mockVenue.modify.mockResolvedValue(mockTransaction); + + await processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.DryRun, signer: 'Alice' } + ); + + expect(run).not.toHaveBeenCalled(); + }); + }); + + describe('with offline', () => { + it('should handle offline process mode', async () => { + const mockVenue = new MockVenue(); + + const mockTransaction = new MockPolymeshTransaction(); + const run = mockTransaction.run; + + mockVenue.modify.mockResolvedValue(mockTransaction); + + await processTransaction( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVenue.modify as any, + {}, + {}, + { processMode: ProcessMode.Offline, signer: 'Alice' } + ); + + expect(run).not.toHaveBeenCalled(); + }); + }); }); describe('prepareProcedure', () => { diff --git a/src/transactions/transactions.util.ts b/src/transactions/transactions.util.ts index 881f5e46..a23cd029 100644 --- a/src/transactions/transactions.util.ts +++ b/src/transactions/transactions.util.ts @@ -1,20 +1,22 @@ -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { BigNumber } from '@polymeshassociation/polymesh-private-sdk'; import { + ConfidentialProcedureMethod, ErrorCode, Fees, GenericPolymeshTransaction, NoArgsProcedureMethod, PayingAccountType, - ProcedureMethod, ProcedureOpts, + TransactionPayload, TransactionStatus, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; import { isPolymeshError, isPolymeshTransaction, isPolymeshTransactionBatch, -} from '@polymeshassociation/polymesh-sdk/utils'; +} from '@polymeshassociation/polymesh-private-sdk/utils'; +import { TransactionOptionsDto } from '~/polymesh-rest-api/src/common/dto/transaction-options.dto'; import { AppError, AppInternalError, @@ -23,9 +25,10 @@ import { AppUnprocessableError, AppValidationError, isAppError, -} from '~/common/errors'; -import { BatchTransactionModel } from '~/common/models/batch-transaction.model'; -import { TransactionModel } from '~/common/models/transaction.model'; +} from '~/polymesh-rest-api/src/common/errors'; +import { BatchTransactionModel } from '~/polymesh-rest-api/src/common/models/batch-transaction.model'; +import { TransactionModel } from '~/polymesh-rest-api/src/common/models/transaction.model'; +import { ProcessMode } from '~/polymesh-rest-api/src/common/types'; export type TransactionDetails = { status: TransactionStatus; @@ -44,9 +47,14 @@ export type TransactionResult = { details: TransactionDetails; }; +export type TransactionPayloadResult = { + details: TransactionDetails; + transactionPayload: TransactionPayload; +}; + type WithArgsProcedureMethod = T extends NoArgsProcedureMethod ? never : T; -export type Method = WithArgsProcedureMethod>; +export type Method = WithArgsProcedureMethod>; /** * a helper function to handle when procedures have args and those without args @@ -75,8 +83,10 @@ export async function processTransaction< method: Method, args: MethodArgs, opts: ProcedureOpts, - dryRun = false -): Promise> { + transactionOptions: TransactionOptionsDto +): Promise | TransactionPayloadResult> { + const { processMode, metadata } = transactionOptions; + try { const procedure = await prepareProcedure(method, args, opts); @@ -84,7 +94,7 @@ export async function processTransaction< const [totalFees, result] = await Promise.all([ procedure.getTotalFees(), - dryRun ? ({} as TransformedReturnType) : procedure.run(), + processMode === 'submit' ? procedure.run() : ({} as TransformedReturnType), ]); const { @@ -103,10 +113,15 @@ export async function processTransaction< }, }; - if (dryRun) { + if (processMode === ProcessMode.DryRun) { return { details, result, transactions: [] }; } + if (processMode === ProcessMode.Offline) { + const transactionPayload = await procedure.toSignablePayload(metadata); + return { details, transactionPayload }; + } + const assembleTransactionResponse = ( transaction: GenericPolymeshTransaction ): TransactionModel | BatchTransactionModel => { @@ -120,7 +135,7 @@ export async function processTransaction< transactionTags: transactions.map(({ tag }) => tag), }; } else { - throw new Error( + throw new AppInternalError( 'Unsupported transaction details received. Please report this issue to the Polymesh team' ); } @@ -158,6 +173,15 @@ export function handleSdkError(err: unknown): AppError { if (isPolymeshError(err)) { const { message, code } = err; + + // catch address not present error from the signing manager + if ( + code === ErrorCode.General && + message.includes('not part of the Signing Manager attached to the SDK') + ) { + throw new AppValidationError(message); + } + switch (code) { case ErrorCode.NoDataChange: case ErrorCode.ValidationError: diff --git a/src/transactions/types.ts b/src/transactions/types.ts index f4bf085f..7c45b082 100644 --- a/src/transactions/types.ts +++ b/src/transactions/types.ts @@ -1,6 +1,6 @@ import { PolymeshTransaction, PolymeshTransactionBatch, -} from '@polymeshassociation/polymesh-sdk/types'; +} from '@polymeshassociation/polymesh-private-sdk/types'; export type Transaction = PolymeshTransaction | PolymeshTransactionBatch; diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts deleted file mode 100644 index 4106ed14..00000000 --- a/src/users/dto/create-user.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, Length } from 'class-validator'; - -export class CreateUserDto { - @ApiProperty({ - description: 'The unique name of the user', - example: 'Alice', - type: 'string', - }) - @IsString() - @Length(3, 127) - readonly name: string; -} diff --git a/src/users/model/user.model.ts b/src/users/model/user.model.ts deleted file mode 100644 index cda4a1a9..00000000 --- a/src/users/model/user.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ - -import { ApiProperty } from '@nestjs/swagger'; - -export class UserModel { - @ApiProperty({ - type: 'string', - description: 'Name of the user', - example: 'Alice', - }) - readonly name: string; - - @ApiProperty({ - type: 'string', - description: - 'The internal ID of the user. The exact format depends on the Datastore being used', - example: 'ce97d1ec-2d77-463c-bbde-e077e055858c', - }) - readonly id: string; - - constructor(model: UserModel) { - Object.assign(this, model); - } -} diff --git a/src/users/repo/user.repo.suite.ts b/src/users/repo/user.repo.suite.ts deleted file mode 100644 index 21c47b90..00000000 --- a/src/users/repo/user.repo.suite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AppConflictError, AppNotFoundError } from '~/common/errors'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -const name = 'Alice'; - -export const testUsersRepo = async (usersRepo: UsersRepo): Promise => { - let user: UserModel; - - describe('method: createUser', () => { - it('should create a user', async () => { - user = await usersRepo.createUser({ name }); - expect(user).toMatchSnapshot(); - }); - - it('should throw ExistsError if user exists with given name', () => { - const expectedError = new AppConflictError(name, UsersRepo.type); - - return expect(usersRepo.createUser({ name })).rejects.toThrowError(expectedError); - }); - }); - - describe('method: findByName', () => { - it('should find the created user', async () => { - const foundUser = await usersRepo.findByName(name); - expect(foundUser).toMatchSnapshot(); - }); - - it('should throw NotFoundError if the user does not exist', async () => { - const unknownName = 'unknownName'; - const expectedError = new AppNotFoundError(unknownName, UsersRepo.type); - return expect(usersRepo.findByName(unknownName)).rejects.toThrowError(expectedError); - }); - }); -}; diff --git a/src/users/repo/user.repo.ts b/src/users/repo/user.repo.ts deleted file mode 100644 index 225c7823..00000000 --- a/src/users/repo/user.repo.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { testUsersRepo } from '~/users/repo/user.repo.suite'; - -export abstract class UsersRepo { - public static type = 'User'; - - public abstract findByName(name: string): Promise; - public abstract createUser(params: CreateUserDto): Promise; - - /** - * a set of tests implementers should pass - */ - public static async test(repo: UsersRepo): Promise { - return testUsersRepo(repo); - } -} diff --git a/src/users/user.consts.ts b/src/users/user.consts.ts deleted file mode 100644 index 00433495..00000000 --- a/src/users/user.consts.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { UserModel } from '~/users/model/user.model'; - -export const defaultUser = new UserModel({ - id: '-1', - name: 'DefaultUser', -}); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts deleted file mode 100644 index d386bb8d..00000000 --- a/src/users/users.controller.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { testValues } from '~/test-utils/consts'; -import { mockUserServiceProvider } from '~/test-utils/service-mocks'; -import { UsersController } from '~/users/users.controller'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; -describe('UsersController', () => { - let controller: UsersController; - let mockUsersService: DeepMocked; - - beforeEach(async () => { - mockUsersService = mockUserServiceProvider.useValue as DeepMocked; - - const module: TestingModule = await Test.createTestingModule({ - controllers: [UsersController], - providers: [mockUserServiceProvider], - }).compile(); - - controller = module.get(UsersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('createUser', () => { - const params = { name: user.name }; - it('should call the service and return the result', () => { - when(mockUsersService.createUser).calledWith(params).mockResolvedValue(user); - - return expect(controller.createUser(params)).resolves.toEqual(user); - }); - }); -}); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts deleted file mode 100644 index 81abaf8a..00000000 --- a/src/users/users.controller.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersService } from '~/users/users.service'; - -@ApiTags('auth') -@Controller('users') -export class UsersController { - constructor(private readonly usersService: UsersService) {} - - @ApiOperation({ - summary: 'Create a new REST API user', - description: 'This endpoint creates a new REST API user', - }) - @ApiOkResponse({ - description: 'The newly created user', - type: UserModel, - }) - @Post('/create') - async createUser(@Body() params: CreateUserDto): Promise { - return this.usersService.createUser(params); - } -} diff --git a/src/users/users.module.ts b/src/users/users.module.ts deleted file mode 100644 index b53ef7a7..00000000 --- a/src/users/users.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DatastoreModule } from '~/datastore/datastore.module'; -import { UsersController } from '~/users/users.controller'; -import { UsersService } from '~/users/users.service'; - -/** - * responsible for the REST API's users - */ -@Module({ - imports: [DatastoreModule.registerAsync()], - providers: [UsersService], - exports: [UsersService], - controllers: [UsersController], -}) -export class UsersModule {} diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts deleted file mode 100644 index 30b3dd10..00000000 --- a/src/users/users.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { when } from 'jest-when'; - -import { testValues } from '~/test-utils/consts'; -import { mockUserRepoProvider } from '~/test-utils/repo-mocks'; -import { UsersRepo } from '~/users/repo/user.repo'; -import { UsersService } from '~/users/users.service'; - -const { user } = testValues; - -describe('UsersService', () => { - let service: UsersService; - let mockUsersRepo: DeepMocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [mockUserRepoProvider, UsersService], - }).compile(); - - mockUsersRepo = mockUserRepoProvider.useValue as DeepMocked; - - service = module.get(UsersService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('method: getByName', () => { - it('should return the User', async () => { - when(mockUsersRepo.findByName).calledWith(user.name).mockResolvedValue(user); - - const foundUser = await service.getByName(user.name); - - expect(foundUser).toEqual(user); - }); - }); - - describe('method: createUser', () => { - const params = { name: user.name }; - - it('should create and return a User', async () => { - when(mockUsersRepo.createUser).calledWith(params).mockResolvedValue(user); - - const createdUser = await service.createUser(params); - - expect(createdUser).toEqual(user); - }); - }); -}); diff --git a/src/users/users.service.ts b/src/users/users.service.ts deleted file mode 100644 index 22991ee7..00000000 --- a/src/users/users.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { CreateUserDto } from '~/users/dto/create-user.dto'; -import { UserModel } from '~/users/model/user.model'; -import { UsersRepo } from '~/users/repo/user.repo'; - -@Injectable() -export class UsersService { - constructor(private readonly userRepo: UsersRepo) {} - - public async getByName(name: string): Promise { - return this.userRepo.findByName(name); - } - - public async createUser(params: CreateUserDto): Promise { - const user = await this.userRepo.createUser(params); - return user; - } -} diff --git a/tsconfig.json b/tsconfig.json index bd915838..90cbb560 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,17 @@ { "compilerOptions": { - "outDir": "dist", + "outDir": "dist", "baseUrl": ".", "paths": { - "~/*": ["src/*"], + "~/app.module": ["src/app.module"], + "~/confidential*": ["src/confidential*"], + "~/extended*": ["src/extended*"], + "~/middleware/*": ["src/confidential-middleware/*"], + "~/test-utils/*": ["src/test-utils/*"], + "~/polymesh/*": ["src/polymesh/*"], + "~/transactions/*": ["src/transactions/*"], + "~/polymesh-rest-api/*": ["src/polymesh-rest-api/*"], + "~/*": ["src/polymesh-rest-api/src/*"] }, "plugins": [ { @@ -27,5 +35,5 @@ "skipLibCheck": true, "lib": ["es2017", "dom"], "typeRoots": ["./node_modules/@types", "./src/typings"] - }, + } } diff --git a/yarn.lock b/yarn.lock index 9e71bb3f..6e1f1b65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,18 +72,19 @@ rxjs "7.8.1" "@apollo/client@^3.8.1": - version "3.8.7" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.7.tgz#090b1518f513503b9a6a690ee3eaec49529822e1" - integrity sha512-DnQtFkQrCyxHTSa9gR84YRLmU/al6HeXcLZazVe+VxKBmx/Hj4rV8xWtzfWYX5ijartsqDR7SJgV037MATEecA== + version "3.10.1" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.10.1.tgz#4c8eec28fcce25b96f27c1f1e443ec5c676e4de0" + integrity sha512-QNacQBZzJla5UQ/LLBXJWM7/1v1C5cfpMQPAFjW4hg4T54wHWbg4Dr+Dp6N+hy/ygu8tepdM+/y/5VFLZhovlQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" - "@wry/context" "^0.7.3" + "@wry/caches" "^1.0.0" "@wry/equality" "^0.5.6" - "@wry/trie" "^0.4.3" + "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.17.5" + optimism "^0.18.0" prop-types "^15.7.2" + rehackt "^0.1.0" response-iterator "^0.2.6" symbol-observable "^4.0.0" ts-invariant "^0.10.3" @@ -98,6 +99,14 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.14.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.22.9": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" @@ -266,11 +275,25 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.0.tgz#b6d6e29058ca369127b0eeca2a1c4b5794f1b6b9" integrity sha512-0v7oNOjr6YT9Z2RAOTv4T9aP+ubfx4Q/OhVtAet7PFDt0t9Oy6Jn+/rfC6b8HJ5zEqrQCiMxJfgtHpmIminmJQ== +"@babel/parser@^7.14.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563" + integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ== + "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" @@ -390,7 +413,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15", "@babel/template@^7.3.3": +"@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== @@ -399,6 +422,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.3.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + "@babel/traverse@^7.23.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -423,6 +455,15 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.14.5", "@babel/types@^7.3.0": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.5.tgz#48d730a00c95109fa4393352705954d74fb5b602" + integrity sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -432,15 +473,6 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.3.0": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.5.tgz#48d730a00c95109fa4393352705954d74fb5b602" - integrity sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w== - dependencies: - "@babel/helper-string-parser" "^7.23.4" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1168,6 +1200,13 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@^1.3.0", "@noble/curves@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== + dependencies: + "@noble/hashes" "1.4.0" + "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" @@ -1178,6 +1217,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.3": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1587,12 +1631,12 @@ tslib "^2.5.3" "@polkadot/keyring@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.5.1.tgz#2f38504aa915f54bbd265f3793a6be55010eb1f5" - integrity sha512-u6b+Q7wI6WY/vwmJS9uUHy/5hKZ226nTlVNmxjkj9GvrRsQvUSwS94163yHPJwiZJiIv5xK5m0rwCMyoYu+wjA== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.2.tgz#6067e6294fee23728b008ac116e7e9db05cecb9b" + integrity sha512-O3Q7GVmRYm8q7HuB3S0+Yf/q/EB2egKRRU3fv9b3B7V+A52tKzA+vIwEmNVaD1g5FKW9oB97rmpggs0zaKFqHw== dependencies: - "@polkadot/util" "12.5.1" - "@polkadot/util-crypto" "12.5.1" + "@polkadot/util" "12.6.2" + "@polkadot/util-crypto" "12.6.2" tslib "^2.6.2" "@polkadot/networks@12.4.2": @@ -1604,13 +1648,22 @@ "@substrate/ss58-registry" "^1.43.0" tslib "^2.6.2" -"@polkadot/networks@12.5.1", "@polkadot/networks@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.5.1.tgz#685c69d24d78a64f4e750609af22678d57fe1192" - integrity sha512-PP6UUdzz6iHHZH4q96cUEhTcydHj16+61sqeaYEJSF6Q9iY+5WVWQ26+rdjmre/EBdrMQkSS/CKy73mO5z/JkQ== +"@polkadot/networks@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.1.tgz#eb0b1fb9e04fbaba066d44df4ff18b0567ca5fcc" + integrity sha512-pzyirxTYAnsx+6kyLYcUk26e4TLz3cX6p2KhTgAVW77YnpGX5VTKTbYykyXC8fXFd/migeQsLaa2raFN47mwoA== dependencies: - "@polkadot/util" "12.5.1" - "@substrate/ss58-registry" "^1.43.0" + "@polkadot/util" "12.6.1" + "@substrate/ss58-registry" "^1.44.0" + tslib "^2.6.2" + +"@polkadot/networks@12.6.2", "@polkadot/networks@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.2.tgz#791779fee1d86cc5b6cd371858eea9b7c3f8720d" + integrity sha512-1oWtZm1IvPWqvMrldVH6NI2gBoCndl5GEwx7lAuQWGr7eNL+6Bdc5K3Z9T0MzFvDGoi2/CBqjX9dRKo39pDC/w== + dependencies: + "@polkadot/util" "12.6.2" + "@substrate/ss58-registry" "^1.44.0" tslib "^2.6.2" "@polkadot/rpc-augment@10.9.1": @@ -1718,7 +1771,7 @@ rxjs "^7.8.1" tslib "^2.5.3" -"@polkadot/util-crypto@12.4.2", "@polkadot/util-crypto@^12.4.2": +"@polkadot/util-crypto@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.4.2.tgz#e19258dab5f2d4fe49f2d074d36d33a445e50b74" integrity sha512-JP7OrEKYx35P3wWc2Iu9F6BfYMIkywXik908zQqPxwoQhr8uDLP1Qoyu9Sws+hE97Yz1O4jBVvryS2le0yusog== @@ -1734,23 +1787,39 @@ "@scure/base" "1.1.1" tslib "^2.6.2" -"@polkadot/util-crypto@12.5.1", "@polkadot/util-crypto@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.5.1.tgz#1753b23abfb9d72db950399ef65b0cbe5bef9f2f" - integrity sha512-Y8ORbMcsM/VOqSG3DgqutRGQ8XXK+X9M3C8oOEI2Tji65ZsXbh9Yh+ryPLM0oBp/9vqOXjkLgZJbbVuQceOw0A== +"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz#d2d51010e8e8ca88951b7d864add797dad18bbfc" + integrity sha512-FEWI/dJ7wDMNN1WOzZAjQoIcCP/3vz3wvAp5QQm+lOrzOLj0iDmaIGIcBkz8HVm3ErfSe/uKP0KS4jgV/ib+Mg== + dependencies: + "@noble/curves" "^1.3.0" + "@noble/hashes" "^1.3.3" + "@polkadot/networks" "12.6.2" + "@polkadot/util" "12.6.2" + "@polkadot/wasm-crypto" "^7.3.2" + "@polkadot/wasm-util" "^7.3.2" + "@polkadot/x-bigint" "12.6.2" + "@polkadot/x-randomvalues" "12.6.2" + "@scure/base" "^1.1.5" + tslib "^2.6.2" + +"@polkadot/util-crypto@^12.4.2": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.1.tgz#f1e354569fb039822db5e57297296e22af575af8" + integrity sha512-2ezWFLmdgeDXqB9NAUdgpp3s2rQztNrZLY+y0SJYNOG4ch+PyodTW/qSksnOrVGVdRhZ5OESRE9xvo9LYV5UAw== dependencies: "@noble/curves" "^1.2.0" "@noble/hashes" "^1.3.2" - "@polkadot/networks" "12.5.1" - "@polkadot/util" "12.5.1" - "@polkadot/wasm-crypto" "^7.2.2" - "@polkadot/wasm-util" "^7.2.2" - "@polkadot/x-bigint" "12.5.1" - "@polkadot/x-randomvalues" "12.5.1" + "@polkadot/networks" "12.6.1" + "@polkadot/util" "12.6.1" + "@polkadot/wasm-crypto" "^7.3.1" + "@polkadot/wasm-util" "^7.3.1" + "@polkadot/x-bigint" "12.6.1" + "@polkadot/x-randomvalues" "12.6.1" "@scure/base" "^1.1.3" tslib "^2.6.2" -"@polkadot/util@12.4.2", "@polkadot/util@^12.4.2": +"@polkadot/util@12.4.2": version "12.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.4.2.tgz#65759f4b366c2a787fd21abacab8cf8ab1aebbf9" integrity sha512-NcTCbnIzMb/3TvJNEbaiu/9EvYIBuzDwZfqQ4hzL0GAptkF8aDkKMDCfQ/j3FI38rR+VTPQHNky9fvWglGKGRw== @@ -1763,71 +1832,137 @@ bn.js "^5.2.1" tslib "^2.6.2" -"@polkadot/util@12.5.1", "@polkadot/util@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.5.1.tgz#f4e7415600b013d3b69527aa88904acf085be3f5" - integrity sha512-fDBZL7D4/baMG09Qowseo884m3QBzErGkRWNBId1UjWR99kyex+cIY9fOSzmuQxo6nLdJlLHw1Nz2caN3+Bq0A== +"@polkadot/util@12.6.1", "@polkadot/util@^12.4.2": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.1.tgz#477b8e2c601e8aae0662670ed33da46f1b335e5a" + integrity sha512-10ra3VfXtK8ZSnWI7zjhvRrhupg3rd4iFC3zCaXmRpOU+AmfIoCFVEmuUuC66gyXiz2/g6k5E6j0lWQCOProSQ== dependencies: - "@polkadot/x-bigint" "12.5.1" - "@polkadot/x-global" "12.5.1" - "@polkadot/x-textdecoder" "12.5.1" - "@polkadot/x-textencoder" "12.5.1" - "@types/bn.js" "^5.1.1" + "@polkadot/x-bigint" "12.6.1" + "@polkadot/x-global" "12.6.1" + "@polkadot/x-textdecoder" "12.6.1" + "@polkadot/x-textencoder" "12.6.1" + "@types/bn.js" "^5.1.5" bn.js "^5.2.1" tslib "^2.6.2" -"@polkadot/wasm-bridge@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.2.2.tgz#957b82b17927fe080729e8930b5b5c554f77b8df" - integrity sha512-CgNENd65DVYtackOVXXRA0D1RPoCv5+77IdBCf7kNqu6LeAnR4nfTI6qjaApUdN1xRweUsQjSH7tu7VjkMOA0A== +"@polkadot/util@12.6.2", "@polkadot/util@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.2.tgz#9396eff491221e1f0fd28feac55fc16ecd61a8dc" + integrity sha512-l8TubR7CLEY47240uki0TQzFvtnxFIO7uI/0GoWzpYD/O62EIAMRsuY01N4DuwgKq2ZWD59WhzsLYmA5K6ksdw== dependencies: - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" + "@polkadot/x-bigint" "12.6.2" + "@polkadot/x-global" "12.6.2" + "@polkadot/x-textdecoder" "12.6.2" + "@polkadot/x-textencoder" "12.6.2" + "@types/bn.js" "^5.1.5" + bn.js "^5.2.1" + tslib "^2.6.2" -"@polkadot/wasm-crypto-asmjs@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.2.2.tgz#25243a4d5d8d997761141b616623cacff4329f13" - integrity sha512-wKg+cpsWQCTSVhjlHuNeB/184rxKqY3vaklacbLOMbUXieIfuDBav5PJdzS3yeiVE60TpYaHW4iX/5OYHS82gg== +"@polkadot/wasm-bridge@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.1.tgz#8438363aa98296f8be949321ca1d3a4cbcc4fc49" + integrity sha512-wPtDkGaOQx5BUIYP+kJv5aV3BnCQ+HXr36khGKYrRQAMBrG+ybCNPOTVXDQnSbraPQRSw7fSIJmiQpEmFsIz0w== dependencies: - tslib "^2.6.1" + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" -"@polkadot/wasm-crypto-init@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.2.2.tgz#ffd105b87fc1b679c06c85c0848183c27bc539e3" - integrity sha512-vD4iPIp9x+SssUIWUenxWLPw4BVIwhXHNMpsV81egK990tvpyIxL205/EF5QRb1mKn8WfWcNFm5tYwwh9NdnnA== +"@polkadot/wasm-bridge@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.3.2.tgz#e1b01906b19e06cbca3d94f10f5666f2ae0baadc" + integrity sha512-AJEXChcf/nKXd5Q/YLEV5dXQMle3UNT7jcXYmIffZAo/KI394a+/24PaISyQjoNC0fkzS1Q8T5pnGGHmXiVz2g== dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" -"@polkadot/wasm-crypto-wasm@7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.2.2.tgz#9e49a1565bda2bc830708693b491b37ad8a2144d" - integrity sha512-3efoIB6jA3Hhv6k0YIBwCtlC8gCSWCk+R296yIXRLLr3cGN415KM/PO/d1JIXYI64lbrRzWRmZRhllw3jf6Atg== +"@polkadot/wasm-crypto-asmjs@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.1.tgz#8322a554635bcc689eb3a944c87ea64061b6ba81" + integrity sha512-pTUOCIP0nUc4tjzdG1vtEBztKEWde4DBEZm7NaxBLvwNUxsbYhLKYvuhASEyEIz0ZyE4rOBWEmRF4Buic8oO+g== dependencies: - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" + tslib "^2.6.2" -"@polkadot/wasm-crypto@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" - integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== +"@polkadot/wasm-crypto-asmjs@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.3.2.tgz#c6d41bc4b48b5359d57a24ca3066d239f2d70a34" + integrity sha512-QP5eiUqUFur/2UoF2KKKYJcesc71fXhQFLT3D4ZjG28Mfk2ZPI0QNRUfpcxVQmIUpV5USHg4geCBNuCYsMm20Q== dependencies: - "@polkadot/wasm-bridge" "7.2.2" - "@polkadot/wasm-crypto-asmjs" "7.2.2" - "@polkadot/wasm-crypto-init" "7.2.2" - "@polkadot/wasm-crypto-wasm" "7.2.2" - "@polkadot/wasm-util" "7.2.2" - tslib "^2.6.1" + tslib "^2.6.2" -"@polkadot/wasm-util@7.2.2", "@polkadot/wasm-util@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" - integrity sha512-N/25960ifCc56sBlJZ2h5UBpEPvxBmMLgwYsl7CUuT+ea2LuJW9Xh8VHDN/guYXwmm92/KvuendYkEUykpm/JQ== +"@polkadot/wasm-crypto-init@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.1.tgz#5a140f9e2746ce3009dbcc4d05827e0703fd344d" + integrity sha512-Fx15ItLcxCe7uJCWZVXhFbsrXqHUKAp9KGYQFKBRK7r1C2va4Y7qnirjwkxoMHQcunusLe2KdbrD+YJuzh4wlA== + dependencies: + "@polkadot/wasm-bridge" "7.3.1" + "@polkadot/wasm-crypto-asmjs" "7.3.1" + "@polkadot/wasm-crypto-wasm" "7.3.1" + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + +"@polkadot/wasm-crypto-init@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.3.2.tgz#7e1fe79ba978fb0a4a0f74a92d976299d38bc4b8" + 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" + +"@polkadot/wasm-crypto-wasm@7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.1.tgz#8f0906ab5dd11fa706db4c3547304b0e1d99f671" + integrity sha512-hBMRwrBLCfVsFHSdnwwIxEPshoZdW/dHehYRxMSpUdmqOxtD1gnjocXGE1KZUYGX675+EFuR+Ch6OoTKFJxwTA== + dependencies: + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + +"@polkadot/wasm-crypto-wasm@7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.3.2.tgz#44e08ed5cf6499ce4a3aa7247071a5d01f6a74f4" + integrity sha512-15wd0EMv9IXs5Abp1ZKpKKAVyZPhATIAHfKsyoWCEFDLSOA0/K0QGOxzrAlsrdUkiKZOq7uzSIgIDgW8okx2Mw== + dependencies: + "@polkadot/wasm-util" "7.3.2" + tslib "^2.6.2" + +"@polkadot/wasm-crypto@^7.2.2", "@polkadot/wasm-crypto@^7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.2.tgz#61bbcd9e591500705c8c591e6aff7654bdc8afc9" + 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" + +"@polkadot/wasm-crypto@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.3.1.tgz#178e43ab68385c90d40f53590d3fdb59ee1aa5f4" + integrity sha512-BSK0YyCN4ohjtwbiHG71fgf+7ufgfLrHxjn7pKsvXhyeiEVuDhbDreNcpUf3eGOJ5tNk75aSbKGF4a3EJGIiNA== + dependencies: + "@polkadot/wasm-bridge" "7.3.1" + "@polkadot/wasm-crypto-asmjs" "7.3.1" + "@polkadot/wasm-crypto-init" "7.3.1" + "@polkadot/wasm-crypto-wasm" "7.3.1" + "@polkadot/wasm-util" "7.3.1" + tslib "^2.6.2" + +"@polkadot/wasm-util@7.3.1", "@polkadot/wasm-util@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.1.tgz#047fbce91e9bdd944d46bea8f636d2fdc268fba2" + integrity sha512-0m6ozYwBrJgnGl6QvS37ZiGRu4FFPPEtMYEVssfo1Tz4skHJlByWaHWhRNoNCVFAKiGEBu+rfx5HAQMAhoPkvg== + dependencies: + tslib "^2.6.2" + +"@polkadot/wasm-util@7.3.2", "@polkadot/wasm-util@^7.2.2", "@polkadot/wasm-util@^7.3.2": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.3.2.tgz#4fe6370d2b029679b41a5c02cd7ebf42f9b28de1" + integrity sha512-bmD+Dxo1lTZyZNxbyPE380wd82QsX+43mgCm40boyKrRppXEyQmWT98v/Poc7chLuskYb6X8IQ6lvvK2bGR4Tg== dependencies: - tslib "^2.6.1" + tslib "^2.6.2" "@polkadot/x-bigint@12.4.2": version "12.4.2" @@ -1837,20 +1972,28 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-bigint@12.5.1", "@polkadot/x-bigint@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.5.1.tgz#0a6a3a34fae51468e7b02b42e0ff0747fd88a80a" - integrity sha512-Fw39eoN9v0sqxSzfSC5awaDVdzojIiE7d1hRSQgVSrES+8whWvtbYMR0qwbVhTuW7DvogHmye41P9xKMlXZysg== +"@polkadot/x-bigint@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.1.tgz#82b6a3639e1bc1195b2858482f0421b403641b80" + integrity sha512-YlABeVIlgYQZJ4ZpW/+akFGGxw5jMGt4g5vaP7EumlORGneJHzzWJYDmI5v2y7j1zvC9ofOle7z4tRmtN/QDew== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + +"@polkadot/x-bigint@12.6.2", "@polkadot/x-bigint@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.2.tgz#59b7a615f205ae65e1ac67194aefde94d3344580" + integrity sha512-HSIk60uFPX4GOFZSnIF7VYJz7WZA7tpFJsne7SzxOooRwMTWEtw3fUpFy5cYYOeLh17/kHH1Y7SVcuxzVLc74Q== + dependencies: + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" "@polkadot/x-fetch@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.5.1.tgz#41532d1324cef56a28c31490ac81062d487b16fb" - integrity sha512-Bc019lOKCoQJrthiS+H3LwCahGtl5tNnb2HK7xe3DBQIUx9r2HsF/uEngNfMRUFkUYg5TPCLFbEWU8NIREBS1A== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.6.2.tgz#b1bca028db90263bafbad2636c18d838d842d439" + integrity sha512-8wM/Z9JJPWN1pzSpU7XxTI1ldj/AfC8hKioBlUahZ8gUiJaOF7K9XEFCrCDLis/A1BoOu7Ne6WMx/vsJJIbDWw== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.2" node-fetch "^3.3.2" tslib "^2.6.2" @@ -1861,10 +2004,17 @@ dependencies: tslib "^2.6.2" -"@polkadot/x-global@12.5.1", "@polkadot/x-global@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.5.1.tgz#947bb90e0c46c853ffe216dd6dcb6847d5c18a98" - integrity sha512-6K0YtWEg0eXInDOihU5aSzeb1t9TiDdX9ZuRly+58ALSqw5kPZYmQLbzE1d8HWzyXRXK+YH65GtLzfMGqfYHmw== +"@polkadot/x-global@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.1.tgz#1a00ae466e344539bdee57eb7b1dd4e4d5b1dc95" + integrity sha512-w5t19HIdBPuyu7X/AiCyH2DsKqxBF0KpF4Ymolnx8PfcSIgnq9ZOmgs74McPR6FgEmeEkr9uNKujZrsfURi1ug== + dependencies: + tslib "^2.6.2" + +"@polkadot/x-global@12.6.2", "@polkadot/x-global@^12.3.1": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.2.tgz#31d4de1c3d4c44e4be3219555a6d91091decc4ec" + integrity sha512-a8d6m+PW98jmsYDtAWp88qS4dl8DyqUBsd0S+WgyfSMtpEXu6v9nXDgPZgwF5xdDvXhm+P0ZfVkVTnIGrScb5g== dependencies: tslib "^2.6.2" @@ -1876,12 +2026,20 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-randomvalues@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.5.1.tgz#b30c6fa8749f5776f1d8a78b6edddb9b0f9c2853" - integrity sha512-UsMb1d+77EPNjW78BpHjZLIm4TaIpfqq89OhZP/6gDIoS2V9iE/AK3jOWKm1G7Y2F8XIoX1qzQpuMakjfagFoQ== +"@polkadot/x-randomvalues@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.1.tgz#f0ad7afa5b0bac123b634ac19d6625cd301a9307" + integrity sha512-1uVKlfYYbgIgGV5v1Dgn960cGovenWm5pmg+aTMeUGXVYiJwRD2zOpLyC1i/tP454iA74j74pmWb8Nkn0tJZUQ== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + +"@polkadot/x-randomvalues@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz#13fe3619368b8bf5cb73781554859b5ff9d900a2" + integrity sha512-Vr8uG7rH2IcNJwtyf5ebdODMcr0XjoCpUbI91Zv6AlKVYOGKZlKLYJHIwpTaKKB+7KPWyQrk4Mlym/rS7v9feg== + dependencies: + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" "@polkadot/x-textdecoder@12.4.2": @@ -1892,12 +2050,20 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-textdecoder@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.5.1.tgz#8d89d2b5efbffb2550a48f8afb4a834e1d8d4f6e" - integrity sha512-j2YZGWfwhMC8nHW3BXq10fAPY02ObLL/qoTjCMJ1Cmc/OGq18Ep7k9cXXbjFAq3wf3tUUewt/u/hStKCk3IvfQ== +"@polkadot/x-textdecoder@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.1.tgz#ee6e9a0f1819204aa60e0ef5a576e8b222501123" + integrity sha512-IasodJeV1f2Nr/VtA207+LXCQEqYcG8y9qB/EQcRsrEP58NbwwxM5Z2obV0lSjJOxRTJ4/OlhUwnLHwcbIp6+g== + dependencies: + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + +"@polkadot/x-textdecoder@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.2.tgz#b86da0f8e8178f1ca31a7158257e92aea90b10e4" + integrity sha512-M1Bir7tYvNappfpFWXOJcnxUhBUFWkUFIdJSyH0zs5LmFtFdbKAeiDXxSp2Swp5ddOZdZgPac294/o2TnQKN1w== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" "@polkadot/x-textencoder@12.4.2": @@ -1908,55 +2074,87 @@ "@polkadot/x-global" "12.4.2" tslib "^2.6.2" -"@polkadot/x-textencoder@12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.5.1.tgz#9104e37a60068df2fbf57c81a7ce48669430c76c" - integrity sha512-1JNNpOGb4wD+c7zFuOqjibl49LPnHNr4rj4s3WflLUIZvOMY6euoDuN3ISjQSHCLlVSoH0sOCWA3qXZU4bCTDQ== +"@polkadot/x-textencoder@12.6.1": + version "12.6.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.1.tgz#b39d4afb50c8bc2ff6add9f20cfc2338abff90d4" + integrity sha512-sTq/+tXqBhGe01a1rjieSHFh3y935vuRgtahVgVJZnfqh5SmLPgSN5tTPxZWzyx7gHIfotle8laTJbJarv7V1A== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.1" + tslib "^2.6.2" + +"@polkadot/x-textencoder@12.6.2": + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.2.tgz#81d23bd904a2c36137a395c865c5fefa21abfb44" + integrity sha512-4N+3UVCpI489tUJ6cv3uf0PjOHvgGp9Dl+SZRLgFGt9mvxnvpW/7+XBADRMtlG4xi5gaRK7bgl5bmY6OMDsNdw== + dependencies: + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" "@polkadot/x-ws@^12.3.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.5.1.tgz#ff9fc78ef701e18d765443779ab95296a406138c" - integrity sha512-efNMhB3Lh6pW2iTipMkqwrjpuUtb3EwR/jYZftiIGo5tDPB7rqoMOp9s6KRFJEIUfZkLnMUtbkZ5fHzUJaCjmQ== + version "12.6.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.6.2.tgz#b99094d8e53a03be1de903d13ba59adaaabc767a" + integrity sha512-cGZWo7K5eRRQCRl2LrcyCYsrc3lRbTlixZh3AzgU8uX4wASVGRlNWi/Hf4TtHNe1ExCDmxabJzdIsABIfrr7xw== dependencies: - "@polkadot/x-global" "12.5.1" + "@polkadot/x-global" "12.6.2" tslib "^2.6.2" - ws "^8.14.1" + ws "^8.15.1" "@polymeshassociation/fireblocks-signing-manager@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.3.0.tgz#fbcabb0803e0f50aa95211047664423be093b3b8" - integrity sha512-8ApmdQpQ2rGgqGP6nm2SP5XR22hSQnF5idy5rOKRyU3UUgHWCsdnUgZKiw9+rtSPVxFzDjeVUcya2ljqD+DmOg== + version "2.4.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/fireblocks-signing-manager/-/fireblocks-signing-manager-2.4.0.tgz#950fe46caf09d605f50eddbece2c8be4993e5ae2" + integrity sha512-go6wS34qiTU4LW9uuqVNSV7Bdi0nbbq0LN7kD6jjp6QJgBUuN6XsmNDcwa17CU/6LrSXRN94cb/HvJdntFfEGw== dependencies: "@polkadot/util" "^12.4.2" "@polkadot/util-crypto" "^12.4.2" - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" fireblocks-sdk "^2.5.3" "@polymeshassociation/hashicorp-vault-signing-manager@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/hashicorp-vault-signing-manager/-/hashicorp-vault-signing-manager-3.1.0.tgz#0a547ecee10fd7bd8105ae00ab0153d805d0bee5" - integrity sha512-/AZxM28jbeNuZE21AjIU4lhBdZH6/Dkaq+rrgX4juP43pwv/mdhED8d/RSC5eviHDBFr/VpDu68ryB8uvOdLpQ== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/hashicorp-vault-signing-manager/-/hashicorp-vault-signing-manager-3.2.0.tgz#621cca97b95f959752a1ed54c3b222ea97c7492b" + integrity sha512-HtHz/q/8O6D6/YYwpmh0qh2WdC+opMM1+Gu99ZPUfBstshgh5OY7oCfAH699V/BA67O9Ai5UGPf8L9AIxTGrBA== dependencies: "@polkadot/util" "^12.4.2" "@polkadot/util-crypto" "^12.4.2" - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" cross-fetch "^3.1.5" lodash "^4.17.21" "@polymeshassociation/local-signing-manager@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/local-signing-manager/-/local-signing-manager-3.1.0.tgz#0bf12b10d8bc76b0c1a0a3832ed8bd85578cec3a" - integrity sha512-T6RPqnw7D0KJpsI7J1Ix+S6VMyFBEAIJKpYZUJ16a2nmYL4DzlCWY/aHhyubaZn4jY5d1j5vmJTLYsavKENbLw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/local-signing-manager/-/local-signing-manager-3.2.0.tgz#7c08d811d428bd1e78c7c6ad92dbc966481a34cf" + integrity sha512-gQx08eK8E43mo9KDtIJFQpNMAWq1eTGL2qgQe6IKEiadDpIfSJ9WeCg/mXXrOobC9jxQyi1+pGRQY6wUllDBXA== dependencies: - "@polymeshassociation/signing-manager-types" "^3.1.0" + "@polymeshassociation/signing-manager-types" "^3.2.0" + +"@polymeshassociation/polymesh-private-sdk@1.2.0-alpha.2": + version "1.2.0-alpha.2" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-private-sdk/-/polymesh-private-sdk-1.2.0-alpha.2.tgz#359f7dfd116f22aa3ae233554acfe24408b3310a" + integrity sha512-o/idh9HCd/J8OnWStj81CdXlDl6vfwVaQQ/qZNRi7ByKGG5Ax0+yAGfP7oFhywOFO1jThg5+MNu19wU/G4QECw== + dependencies: + "@apollo/client" "^3.8.1" + "@noble/curves" "^1.4.0" + "@polkadot/api" "10.9.1" + "@polkadot/util" "12.4.2" + "@polkadot/util-crypto" "12.4.2" + "@polymeshassociation/polymesh-sdk" "24.2.1" + bignumber.js "9.0.1" + bluebird "^3.7.2" + cross-fetch "^4.0.0" + dayjs "1.11.9" + graphql "^16.8.0" + graphql-tag "2.12.6" + iso-7064 "^1.1.0" + json-stable-stringify "^1.0.2" + lodash "^4.17.21" + patch-package "^8.0.0" + semver "^7.5.4" + websocket "^1.0.34" -"@polymeshassociation/polymesh-sdk@^23.0.0": - version "23.0.0" - resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-23.0.0.tgz#1d73ac26d6117b9cf9543e7705695992cefd4305" - integrity sha512-rq6fnRN9gLMGrG8hz+gvtDJFloYKw8WlgoXzDZxeVSiGgGg3YSVWRge8WzfeTotHumS2qbfkWJK2HgFU6V5foA== +"@polymeshassociation/polymesh-sdk@24.2.1": + version "24.2.1" + resolved "https://registry.yarnpkg.com/@polymeshassociation/polymesh-sdk/-/polymesh-sdk-24.2.1.tgz#3fd4c8da94e39b80e2778bdc3c62b17348d6c331" + integrity sha512-mKFqSEC98akG+F/50alDLXnOD/VrkKdf5pXRM6KRY0OCJxOV0JYfEk23NApHx/OCgzu9CiTHElgBMjoRywAp3A== dependencies: "@apollo/client" "^3.8.1" "@polkadot/api" "10.9.1" @@ -1980,6 +2178,11 @@ resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.1.0.tgz#645afd036af1666579be8b6cf6f7bf15390183e2" integrity sha512-gLhToq1vRXo+Tx9wvpFGeZyjwSFkmXlgEtVgKuh1uRlAyezrCG2uJB+tBE7Nx8IquuiCisbEpG/A8UHkwgN4Cg== +"@polymeshassociation/signing-manager-types@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@polymeshassociation/signing-manager-types/-/signing-manager-types-3.2.0.tgz#a02089aae88968bc7a3d20a19a34b1a84361a191" + integrity sha512-+xJdrxhOyfY0Noq8s9vLsfJKCMU2R3cH0MetWL2aoX/DLmm2p8gX28EtaGBsHNoiZJLGol4NnLR0fphyVsXS0Q== + "@prettier/eslint@npm:prettier-eslint@^15.0.1", prettier-eslint@15.0.1: version "15.0.1" resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-15.0.1.tgz#2543a43e9acec2a9767ad6458165ce81f353db9c" @@ -2010,6 +2213,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== +"@scure/base@^1.1.5": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + "@semantic-release/changelog@^6.0.1": version "6.0.3" resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-6.0.3.tgz#6195630ecbeccad174461de727d5f975abc23eeb" @@ -2050,20 +2258,6 @@ lodash "^4.17.4" parse-json "^5.0.0" -"@semantic-release/git@^10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@semantic-release/git/-/git-10.0.1.tgz#c646e55d67fae623875bf3a06a634dd434904498" - integrity sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w== - dependencies: - "@semantic-release/error" "^3.0.0" - aggregate-error "^3.0.0" - debug "^4.0.0" - dir-glob "^3.0.0" - execa "^5.0.0" - lodash "^4.17.4" - micromatch "^4.0.0" - p-reduce "^2.0.0" - "@semantic-release/github@^8.0.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@semantic-release/github/-/github-8.1.0.tgz#c31fc5852d32975648445804d1984cd96e72c4d0" @@ -2178,9 +2372,14 @@ smoldot "1.0.4" "@substrate/ss58-registry@^1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.43.0.tgz#93108e45cb7ef6d82560c153e3692c2aa1c711b3" - integrity sha512-USEkXA46P9sqClL7PZv0QFsit4S8Im97wchKG0/H/9q3AT/S76r40UHfCr4Un7eBJPE23f7fU9BZ0ITpP9MCsA== + version "1.47.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.47.0.tgz#99b11fd3c16657f5eae483b3df7c545ca756d1fc" + integrity sha512-6kuIJedRcisUJS2pgksEH2jZf3hfSIVzqtFzs/AyjTW3ETbMg5q1Bb7VWa0WYaT6dTrEXp/6UoXM5B9pSIUmcw== + +"@substrate/ss58-registry@^1.44.0": + version "1.44.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.44.0.tgz#54f214e2a44f450b7bbc9252891c1879a54e0606" + integrity sha512-7lQ/7mMCzVNSEfDS4BCqnRnKCFKpcOaPrxMeGTXHX1YQzM/m2BBHjbK2C3dJvjv7GYxMiaTq/HdWQj1xS6ss+A== "@tootallnate/once@2": version "2.0.0" @@ -2247,10 +2446,10 @@ dependencies: "@babel/types" "^7.3.0" -"@types/bn.js@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" - integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== +"@types/bn.js@^5.1.1", "@types/bn.js@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== dependencies: "@types/node" "*" @@ -2410,9 +2609,11 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "20.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" - integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + version "20.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.2.tgz#32a5e8228357f57714ad28d52229ab04880c2814" + integrity sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw== + dependencies: + undici-types "~5.26.4" "@types/node@20.4.7": version "20.4.7" @@ -2765,7 +2966,14 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" -"@wry/context@^0.7.0", "@wry/context@^0.7.3": +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": version "0.7.4" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== @@ -2786,6 +2994,13 @@ dependencies: tslib "^2.3.0" +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -4120,11 +4335,11 @@ cron@2.4.1: luxon "^3.2.1" cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== dependencies: - node-fetch "2.6.7" + node-fetch "^2.6.12" cross-fetch@^4.0.0: version "4.0.0" @@ -4152,13 +4367,13 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== dependencies: - es5-ext "^0.10.50" - type "^1.0.1" + es5-ext "^0.10.64" + type "^2.7.2" dargs@^7.0.0: version "7.0.0" @@ -4201,7 +4416,7 @@ debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, de dependencies: ms "2.1.2" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4581,13 +4796,14 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3: @@ -4600,12 +4816,12 @@ es6-iterator@^2.0.3: es6-symbol "^3.1.1" es6-symbol@^3.1.1, es6-symbol@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== dependencies: - d "^1.0.1" - ext "^1.1.2" + d "^1.0.2" + ext "^1.7.0" escalade@^3.1.1: version "3.1.1" @@ -4822,6 +5038,16 @@ eslint@^8.21.0, eslint@^8.48.0, eslint@^8.7.0: strip-ansi "^6.0.1" text-table "^0.2.0" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -4870,6 +5096,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -4999,7 +5233,7 @@ express@4.18.2: utils-merge "1.0.1" vary "~1.1.2" -ext@^1.1.2: +ext@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== @@ -5192,7 +5426,12 @@ flatted@^3.2.7: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.4, follow-redirects@^1.15.0: +follow-redirects@^1.14.4: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -7761,9 +8000,9 @@ next-tick@^1.1.0: integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== nock@^13.3.1: - version "13.3.8" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.8.tgz#7adf3c66f678b02ef0a78d5697ae8bc2ebde0142" - integrity sha512-96yVFal0c/W1lG7mmfRe7eO+hovrhJYd2obzzOZ90f6fjpeU/XNvd9cYHZKZAQJumDfhXgoTpkpJ9pvMj+hqHw== + version "13.5.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" + integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7786,13 +8025,6 @@ node-emoji@1.11.0, node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -7810,9 +8042,9 @@ node-fetch@^3.3.2: formdata-polyfill "^4.0.10" node-gyp-build@^4.3.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" - integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== node-gyp@^9.0.0, node-gyp@^9.1.0: version "9.4.0" @@ -8199,11 +8431,12 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -optimism@^0.17.5: - version "0.17.5" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.17.5.tgz#a4c78b3ad12c58623abedbebb4f2f2c19b8e8816" - integrity sha512-TEcp8ZwK1RczmvMnvktxHSF2tKgMWjJ71xEFGX5ApLh67VsMSTy1ZUlipJw8W+KaqgOmQ+4pqwkeivY89j+4Vw== +optimism@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" + integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== dependencies: + "@wry/caches" "^1.0.0" "@wry/context" "^0.7.0" "@wry/trie" "^0.4.3" tslib "^2.3.0" @@ -9082,6 +9315,11 @@ registry-auth-token@^5.0.0: dependencies: "@pnpm/npm-conf" "^2.1.0" +rehackt@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" + integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== + repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -9189,6 +9427,22 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rhea-promise@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rhea-promise/-/rhea-promise-3.0.1.tgz#4e18d89cc989b6a287941db92b2a47ed9908c139" + integrity sha512-Fcqgml7lgoyi7fH1ClsSyFr/xwToijEN3rULFgrIcL+7EHeduxkWogFxNHjFzHf2YGScAckJDaDxS1RdlTUQYw== + dependencies: + debug "^3.1.0" + rhea "^3.0.0" + tslib "^2.2.0" + +rhea@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rhea/-/rhea-3.0.2.tgz#3882ec45ed7620936c8c807833d17d84a5724ac7" + integrity sha512-0G1ZNM9yWin8VLvTxyISKH6KfR6gl1TW/1+5yMKPf2r1efhkzTLze09iFtT2vpDjuWIVtSmXz8r18lk/dO8qwQ== + dependencies: + debug "^4.3.3" + rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -9344,7 +9598,7 @@ semver-regex@^3.1.2: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.5.4, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -9361,7 +9615,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -10155,7 +10409,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -10237,11 +10491,6 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - type@^2.7.2: version "2.7.2" resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" @@ -10361,6 +10610,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -10543,14 +10797,14 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: defaults "^1.0.3" web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== webpack-node-externals@3.0.0: version "3.0.0" @@ -10714,10 +10968,10 @@ write-file-atomic@^4.0.0, write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.14.1, ws@^8.8.1: - version "8.14.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@^8.15.1, ws@^8.8.1: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" @@ -10760,9 +11014,9 @@ yaml@^1.10.0: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.2.2: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + version "2.4.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" + integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1"