├── .dockerignore ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Makefile ├── README.md ├── code_style_scheme.xml ├── codecov.yml ├── config ├── internal.default.json └── public.default.json ├── data ├── apps │ ├── kik.json │ ├── smp3.json │ ├── smpl.json │ └── test.json ├── offers │ ├── offer_contents.json │ ├── offers.json │ ├── spend_offer_contents.json │ └── spend_offers.json ├── translations │ ├── pt-BR.csv │ └── test_pt-BR.csv └── version_rules │ └── image.json ├── docker-compose.deps.yaml ├── docker-compose.tests.yaml ├── docker-compose.yaml ├── examples ├── get_order.sh ├── kik-1.pem ├── offers.sh ├── open_order.sh ├── orders.sh ├── reg.sh ├── smpl-1-priv.pem ├── smpl-1.pem ├── smpl-default-priv.pem ├── smpl-default.pem ├── submit_earn_order.sh ├── test-es256_0-priv.pem ├── test-es256_0.pem ├── test-rs512_0-priv.pem └── test-rs512_0.pem ├── hooks ├── build └── push ├── jwt ├── private_keys │ ├── README.md │ └── rs512_0-priv.pem └── public_keys │ ├── README.md │ └── rs512_0.pem ├── operational ├── README.md ├── create_keys.sh └── get_keys.sh ├── package-lock.json ├── package.json ├── scripts ├── lib │ └── express-bearer-token.d.ts └── src │ ├── adapt_translation_csv.ts │ ├── admin │ ├── app.ts │ ├── config.ts │ ├── index.ts │ ├── migration.ts │ ├── routes.ts │ ├── services.ts │ ├── translations.ts │ └── wrapper.html │ ├── analytics │ ├── events │ │ ├── common.ts │ │ ├── earn_transaction_broadcast_to_blockchain_failed.ts │ │ ├── earn_transaction_broadcast_to_blockchain_submitted.ts │ │ ├── earn_transaction_broadcast_to_blockchain_succeeded.ts │ │ ├── restore_request_failed.ts │ │ ├── spend_order_payment_confirmed.ts │ │ ├── spend_transaction_broadcast_to_blockchain_failed.ts │ │ ├── spend_transaction_broadcast_to_blockchain_submitted.ts │ │ ├── stellar_account_creation_failed.ts │ │ ├── stellar_account_creation_succeeded.ts │ │ ├── user_login_server_failed.ts │ │ ├── user_login_server_requested.ts │ │ ├── user_login_server_succeeded.ts │ │ ├── user_logout_server_requested.ts │ │ ├── user_registration_failed.ts │ │ ├── user_registration_requested.ts │ │ ├── user_registration_succeeded.ts │ │ └── wallet_address_update_succeeded.ts │ └── index.ts │ ├── client.ts │ ├── client.v1.ts │ ├── config.ts │ ├── create_data │ ├── offers.ts │ └── transform_offer_content_schema.ts │ ├── errors.ts │ ├── internal │ ├── app.ts │ ├── config.ts │ ├── index.ts │ ├── jwt.ts │ ├── middleware.ts │ ├── routes.ts │ └── services.ts │ ├── logging.ts │ ├── manage_db_data.ts │ ├── metrics.ts │ ├── middleware.ts │ ├── mock_client.ts │ ├── models │ ├── applications.ts │ ├── index.ts │ ├── offers.ts │ ├── orders.ts │ ├── translations.ts │ └── users.ts │ ├── node-console.ts │ ├── public │ ├── app.ts │ ├── auth.ts │ ├── config.ts │ ├── index.ts │ ├── jwt.ts │ ├── middleware.ts │ ├── routes │ │ ├── config.ts │ │ ├── index.ts │ │ ├── offers.ts │ │ ├── orders.ts │ │ └── users.ts │ └── services │ │ ├── applications.ts │ │ ├── index.ts │ │ ├── internal_service.ts │ │ ├── migration.ts │ │ ├── native_offers.ts │ │ ├── native_offers.v1.ts │ │ ├── offer_contents.ts │ │ ├── offers.ts │ │ ├── orders.ts │ │ ├── orders.v1.ts │ │ ├── payment.ts │ │ └── users.ts │ ├── redis.ts │ ├── repl-client.ts │ ├── server.ts │ ├── tests │ ├── config.spec.ts │ ├── helpers.ts │ ├── init_tests.ts │ ├── services │ │ ├── index.spec.ts │ │ ├── orders.spec.ts │ │ ├── orders.v1.spec.ts │ │ ├── users.spec.ts │ │ └── users.v1.spec.ts │ ├── translations.spec.ts │ └── utils.spec.ts │ └── utils │ ├── axios_client.ts │ ├── cache.ts │ ├── migration.ts │ ├── path.ts │ ├── rate_limit.ts │ └── utils.ts ├── tests ├── config.json ├── jwt │ ├── private_keys │ │ ├── es256_0-priv.pem │ │ └── rs512_0-priv.pem │ └── public_keys │ │ ├── es256_0.pem │ │ └── rs512_0.pem └── wait-for ├── tsconfig.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | scripts/bin 4 | docker-compose.* 5 | Dockerfile* 6 | .secrets* 7 | hooks 8 | examples 9 | coverage 10 | .travis.yml 11 | .git* 12 | data 13 | database.sqlite 14 | .idea/ 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kinecosystem/kin-ecosystem-sdk-backend 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | **KIN welcomes all contributions**. We're building a product that will hopefully make the world a fairer place, 4 | as such we could not build it without the efforts of many people, 5 | so please don't be shy and share your help with us. Even the tiniest contribution can make a difference! 6 | 7 | These guidelines briefly explain how to contribute in an effective manner, making sure to keep high 8 | quality standards and making it easier for your contributions to make through. 9 | 10 | There are many ways to contribute, from writing tutorials, improving the [documentation](https://partners.kinecosystem.com), asking and answering questions on [stackoverflow](https://stackoverflow.com/questions/tagged/kin), submitting bug reports and feature requests to writing code. 11 | 12 | Kin has many public [Github repositories](https://github.com/kinecosystem) for an arrary of products. Before openning an issue, please make sure you are in the right reposity. 13 | 14 | If you have any product questions please join [our community on reddit](https://www.reddit.com/r/KinFoundation/). 15 | 16 | Note that we have a code of conduct, which we ask you follow in all your interactions with the project. 17 | 18 | ## How to report a bug 19 | 20 | If you find a security vulnerability, do NOT open an issue. Email security@kinecosystem.com instead. 21 | 22 | When filing an issue, make sure to answer these questions: 23 | 1. What's your current environment? i.e. operating system, processor architecture, node version 24 | 1. What did you do? 25 | 1. What did you expect to see? 26 | 1. What did you see instead? 27 | 28 | ## Getting Started Contributing Code 29 | 30 | 1. Create your own fork of the code 31 | 1. Do the changes in your fork 32 | 1. Be sure you have followed the code style for the project 33 | 1. Make sure you add unit tests to any new code you wrote 34 | 1. Send a pull request 35 | 36 | ## Code review process 37 | 38 | The core team looks at Pull Requests on a regular basis in a bi-weekly triage meeting. After feedback has been given we expect responses within two weeks. 39 | 40 | ## Code Style 41 | 42 | We use TSLint to enfore code style. Please be sure to check the configuration and use it in any code changes. 43 | 44 | ## Code of Conduct 45 | 46 | ### Our Pledge 47 | 48 | In the interest of fostering an open and welcoming environment, we as 49 | contributors and maintainers pledge to making participation in our project and 50 | our community a harassment-free experience for everyone, regardless of age, body 51 | size, disability, ethnicity, gender identity and expression, level of experience, 52 | nationality, personal appearance, race, religion, or sexual identity and 53 | orientation. 54 | 55 | ### Our Standards 56 | 57 | Examples of behavior that contributes to creating a positive environment 58 | include: 59 | 60 | * Using welcoming and inclusive language 61 | * Being respectful of differing viewpoints and experiences 62 | * Gracefully accepting constructive criticism 63 | * Focusing on what is best for the community 64 | * Showing empathy towards other community members 65 | 66 | Examples of unacceptable behavior by participants include: 67 | 68 | * The use of sexualized language or imagery and unwelcome sexual attention or 69 | advances 70 | * Trolling, insulting/derogatory comments, and personal or political attacks 71 | * Public or private harassment 72 | * Publishing others' private information, such as a physical or electronic 73 | address, without explicit permission 74 | * Other conduct which could reasonably be considered inappropriate in a 75 | professional setting 76 | 77 | ### Our Responsibilities 78 | 79 | Project maintainers are responsible for clarifying the standards of acceptable 80 | behavior and are expected to take appropriate and fair corrective action in 81 | response to any instances of unacceptable behavior. 82 | 83 | Project maintainers have the right and responsibility to remove, edit, or 84 | reject comments, commits, code, wiki edits, issues, and other contributions 85 | that are not aligned to this Code of Conduct, or to ban temporarily or 86 | permanently any contributor for other behaviors that they deem inappropriate, 87 | threatening, offensive, or harmful. 88 | 89 | ### Scope 90 | 91 | This Code of Conduct applies both within project spaces and in public spaces 92 | when an individual is representing the project or its community. Examples of 93 | representing a project or community include using an official project e-mail 94 | address, posting via an official social media account, or acting as an appointed 95 | representative at an online or offline event. Representation of a project may be 96 | further defined and clarified by project maintainers. 97 | 98 | ### Enforcement 99 | 100 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 101 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 102 | complaints will be reviewed and investigated and will result in a response that 103 | is deemed necessary and appropriate to the circumstances. The project team is 104 | obligated to maintain confidentiality with regard to the reporter of an incident. 105 | Further details of specific enforcement policies may be posted separately. 106 | 107 | Project maintainers who do not follow or enforce the Code of Conduct in good 108 | faith may face temporary or permanent repercussions as determined by other 109 | members of the project's leadership. 110 | 111 | ### Attribution 112 | 113 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 114 | available at [http://contributor-covenant.org/version/1/4][version] 115 | 116 | [homepage]: http://contributor-covenant.org 117 | [version]: http://contributor-covenant.org/version/1/4/ 118 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] linked to a JIRA issue 2 | - [ ] description of the change 3 | - [ ] tests for the newly added code 4 | - [ ] if this is a breaking change, contains backward support code 5 | - [ ] if this requires client changes or partner updates, updated dependendants 6 | - [ ] were metrics and alerts added 7 | - [ ] should I change documentation 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | scripts/bin 4 | scripts/src/**/*.js 5 | scripts/src/**/*.d.ts 6 | scripts/src/**/*.js.map 7 | tests/src/**/*.d.ts 8 | tests/src/**/*.js 9 | tests/src/**/*.js.map 10 | database.sqlite 11 | .tags 12 | .DS_Store 13 | examples/kik* 14 | .secrets* 15 | jwt/**/*.pem 16 | examples/*.pem 17 | coverage/* 18 | bin/* 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | services: docker 4 | 5 | node_js: 6 | - "11" 7 | 8 | before_script: 9 | # - make generate-funding-address # won't work for KIN3 as it's missing whitelisting step 10 | - mkdir -p ./secrets/ && 11 | echo export STELLAR_BASE_SEED=${STELLAR_BASE_SEED} STELLAR_ADDRESS=${STELLAR_ADDRESS} > ./secrets/.secrets 12 | - make create-jwt-keys 13 | - make build-image 14 | - make build db 15 | - NODEMON_WATCH_DIRS="-w /no_such_dir" make up 16 | 17 | script: 18 | - make test 19 | - ./node_modules/codecov/bin/codecov 20 | - make clear-db db 21 | - make test-system-docker 22 | 23 | after_failure: 24 | - make logs 25 | 26 | branches: 27 | only: 28 | - master 29 | 30 | deploy: 31 | provider: script 32 | script: echo "pushing docker image" # && docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" && make push-image 33 | on: 34 | branch: master 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11-alpine 2 | 3 | WORKDIR /opt/app 4 | 5 | # copy requirements 6 | COPY package*.json ./ 7 | 8 | # install build tools 9 | RUN apk add -qU --no-cache -t .fetch-deps git make python g++ \ 10 | && npm install -g npm@latest \ 11 | && npm i \ 12 | && apk del -q .fetch-deps 13 | 14 | # copy the code 15 | COPY . . 16 | 17 | # transpile typescript 18 | RUN npm run transpile 19 | 20 | # set build meta data 21 | ARG BUILD_COMMIT 22 | ARG BUILD_TIMESTAMP 23 | 24 | ENV BUILD_COMMIT $BUILD_COMMIT 25 | ENV BUILD_TIMESTAMP $BUILD_TIMESTAMP 26 | 27 | EXPOSE 80 28 | HEALTHCHECK --interval=1m --timeout=5s --retries=3 CMD wget localhost/status -q -O - > /dev/null 2>&1 29 | 30 | # run the api server 31 | CMD [ "npm", "run", "start" ] 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | trap 'kill %1' SIGINT; make run-internal & bash -c 'sleep 1 && make run' 3 | 4 | split: 5 | tmux new-session 'make run-internal' \; split-window 'sleep 1 && make run' \; 6 | 7 | install: 8 | npm i 9 | 10 | build: 11 | npm run build 12 | 13 | run: 14 | npm run start 15 | 16 | run-internal: 17 | npm run start-internal 18 | 19 | run-admin: 20 | npm run start-admin 21 | 22 | test: 23 | npm run transpile 24 | npm test 25 | 26 | test-system: 27 | npm run test-system 28 | 29 | console: 30 | node --experimental-repl-await ./scripts/bin/node-console.js 31 | 32 | 33 | # will connect to localhost:3000 via socket (can be used to connect to prod with ssh tunnel or any environment) 34 | console-admin: 35 | npm run remote-repl 36 | 37 | 38 | 39 | # "make down up" will not work sometimes, adding sleep after "down" fixes it 40 | sleep: 41 | sleep 0.5 42 | # docker targets 43 | revision := $(shell git rev-parse --short HEAD) 44 | image := "kinecosystem/marketplace-server" 45 | 46 | build-image: create-jwt-keys 47 | docker build -t ${image} -f Dockerfile \ 48 | --build-arg BUILD_COMMIT="${revision}" \ 49 | --build-arg BUILD_TIMESTAMP="$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")" . 50 | docker tag ${image} ${image}:${revision} 51 | 52 | push-image: 53 | docker push ${image}:latest 54 | docker push ${image}:${revision} 55 | 56 | pull: 57 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml pull 58 | 59 | up: up-dev 60 | up-dev: db-docker 61 | . ./secrets/.secrets && docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml up -d 62 | 63 | logs: 64 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml logs 65 | 66 | down: 67 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml down 68 | 69 | psql: 70 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm psql 71 | 72 | redis-cli: 73 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm redis-cli 74 | 75 | db: db-docker 76 | db-docker: 77 | . ./secrets/.secrets && docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm create-db 78 | 79 | clear-db: 80 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm psql -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;" 81 | 82 | clear-redis: 83 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm redis-cli del cursor 84 | 85 | test-system-docker: 86 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm test-system 87 | 88 | generate-funding-address: 89 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run generate-funding-address 90 | 91 | console-docker: 92 | docker-compose -f docker-compose.yaml -f docker-compose.deps.yaml -f docker-compose.tests.yaml run --rm marketplace-console 93 | 94 | create-jwt-keys: 95 | ./operational/create_keys.sh . 96 | 97 | clean-source: 98 | find ./scripts/src -name "*.d.ts" -exec rm {} \; 99 | find ./scripts/src -name "*.js" -exec rm {} \; 100 | find ./scripts/src -name "*.js.map" -exec rm {} \; 101 | 102 | find ./tests/src -name "*.d.ts" -exec rm {} \; 103 | find ./tests/src -name "*.js.map" -exec rm {} \; 104 | 105 | .PHONY: build-image push-image up down psql db-docker test-system-docker generate-funding-address test run build install db all split run-internal test-system 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kin Ecosystem Marketplace Server 2 | ![](https://travis-ci.org/kinfoundation/marketplace-server.svg?branch=master) 3 | 4 | ## Disclaimer 5 | Any data that appears in the repo does not reflect real partnerships or product integrations. We use real company names and products for the sole sake of mocking data to populate our SDK client. 6 | 7 | Documentation 8 | 9 | ### Install/Run 10 | Clone this repo, and then in a terminal: 11 | ```bash 12 | marketplace-server> npm i 13 | marketplace-server> npm run restart 14 | ``` 15 | 16 | ### Development 17 | Please make sure that you follow the code conventions which are described/enforced by the IDE and tslint. 18 | In any jetbrains based IDE (webstorm, pycharm, intellij, etc): 19 | 20 | - Code style 21 | 1. Go to the Preferences > Editor > Code Style 22 | 2. Click the small gears icon besides the Scheme drop down box 23 | 3. Import Scheme > IntelliJ IDEA code style XML 24 | 4. Select the [code_style_scheme.xml](code_style_scheme.xml) file in the root of this project. 25 | 26 | - TSLint 27 | 1. Go to Preferences > Languages & Frameworks > TypeScript > TSLint 28 | 2. Check the **Enable** box 29 | 3. Make sure that the **Search for tslint.json** options is selected under **Configuration file**. 30 | 31 | 32 | ### Testing 33 | 34 | First compile the source: 35 | ``` 36 | make build 37 | ``` 38 | then create the DB: 39 | ``` 40 | make db 41 | ``` 42 | Then run the tests: 43 | ``` 44 | make test 45 | ``` 46 | 47 | ### Running in Docker 48 | To run and test using docker follow the instructions bellow: 49 | 50 | #### Setup 51 | *Download docker + docker-compose for your environment.* 52 | 53 | If you **DON'T** have a wallet with XLM and KIN: 54 | Run the following command to generate a `secrets/.secrets` file with a pre-funded wallet: 55 | ``` 56 | make generate-funding-address 57 | ``` 58 | Note that this command will overwrite any existing file `secrets/.secrets`. 59 | 60 | If you have a wallet with XLM and KIN: 61 | You need to have a stellar account with funds and create a `secrets/.secrets` file locally with the following content: 62 | ``` 63 | export STELLAR_BASE_SEED=SXXX 64 | export STELLAR_ADDRESS=GXXX 65 | ``` 66 | 67 | ##### Create JWT encryption keys 68 | ``` 69 | make create-jwt-keys: 70 | ``` 71 | will create the dir `jwt/` with random encryption keys. You can add other keys if you'd like. the keys in the public_keys dir will be exported via `/v1/config` call. 72 | 73 | #### Run docker servers and system tests 74 | Run the following command: 75 | ``` 76 | make up # start all services 77 | ``` 78 | 79 | And in a separate shell: 80 | ``` 81 | make test-system-docker # run tests 82 | ``` 83 | 84 | To stop the services 85 | ``` 86 | make down 87 | ``` 88 | 89 | #### Run with mounted code for development 90 | You will need to install the dependencies and build the code locally using: 91 | ``` 92 | make install build 93 | ``` 94 | Then when you want to run your local version, instead of `make up`, run: 95 | ``` 96 | make up-dev 97 | ``` 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /code_style_scheme.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 12 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 85 | 86 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | 6 | status: 7 | project: yes 8 | patch: no 9 | changes: no 10 | -------------------------------------------------------------------------------- /config/internal.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3001, 3 | "loggers": [ 4 | { 5 | "name": "console", 6 | "type": "console", 7 | "format": "pretty-json" 8 | } 9 | ], 10 | "db": { 11 | "type": "postgres", 12 | "host": "localhost", 13 | "port": 25432, 14 | "username": "user", 15 | "password": "pass", 16 | "database": "ecosystem", 17 | "synchronize": true, 18 | "logging": false 19 | }, 20 | "payment_service": "http://localhost:5000", 21 | "internal_service": "http://localhost:3001", 22 | "webview": "https://s3.amazonaws.com/assets.kinecosystemtest.com/web-offers/cards-based/index.html", 23 | "jwt_keys_dir": "jwt", 24 | "bi_service": "https://kin-bi.appspot.com/eco_", 25 | "statsd": { 26 | "host": "localhost", 27 | "port": 8125 28 | }, 29 | "cache_ttl": { 30 | "default": 0 31 | }, 32 | "initial_hourly_migration": 100, 33 | "initial_minute_migration": 10 34 | } 35 | -------------------------------------------------------------------------------- /config/public.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "loggers": [ 4 | { 5 | "name": "console", 6 | "type": "console", 7 | "format": "pretty-json" 8 | } 9 | ], 10 | "assets_base": "https://s3.amazonaws.com/kinmarketplace-assets/version1/", 11 | "db": { 12 | "type": "postgres", 13 | "host": "localhost", 14 | "port": 25432, 15 | "username": "user", 16 | "password": "pass", 17 | "database": "ecosystem", 18 | "synchronize": true, 19 | "logging": false 20 | }, 21 | "redis": "mock", 22 | "statsd": { 23 | "host": "localhost", 24 | "port": 8125 25 | }, 26 | "payment_service": "http://localhost:5000", 27 | "internal_service": "http://localhost:3001", 28 | "webview": "https://s3.amazonaws.com/assets.kinecosystemtest.com/web-offers/cards-based/index.html", 29 | "ecosystem_service": "http://localhost:3000", 30 | "environment_name": "local", 31 | "bi_service": "https://kin-bi.appspot.com/eco_", 32 | "cache_ttl": { 33 | "default": 0, 34 | "application": 10 35 | }, 36 | 37 | "migration_service": "https://migration-service.kinecosystemtest.com" 38 | } 39 | -------------------------------------------------------------------------------- /data/apps/kik.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "Akikikikik", 3 | "jwt_public_keys": { 4 | "1": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQv8mribvXGMwDWJTjasn2EQsUxCV\nPtabrOcgAsBXp//sEYnTDj8WdY7OFoMcQ79aMyO630lHSBNBYWIvqO2+LQ==\n-----END PUBLIC KEY-----\n", 5 | "test_key": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE5ppEUgBzRQpccf9arl/txo8t6hDpGPnK\n1SxVTpadjrlCi4YmKM4xVEzyYSsOQHvWA6I70gT0DyVxSk64zsY0MA==\n-----END PUBLIC KEY-----" 6 | }, 7 | "config": { 8 | "max_user_wallets": null, 9 | "daily_earn_offers": 4, 10 | "sign_in_types": ["jwt"], 11 | "blockchain_version": "3", 12 | "limits": { 13 | "hourly_registration": 200000, 14 | "minute_registration": 10000, 15 | "hourly_total_earn": 5000000, 16 | "minute_total_earn": 85000, 17 | "daily_user_earn": 5000 18 | } 19 | }, 20 | "app_id": "kik", 21 | "name": "Kik Messenger" 22 | } 23 | -------------------------------------------------------------------------------- /data/apps/smp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "A28hNcn2wp77QyaM8kB2C", 3 | "jwt_public_keys": { 4 | "1": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\nUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\nHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\no2kQ+X5xK9cipRgEKwIDAQAB\n-----END PUBLIC KEY-----\n", 5 | "rs512_1": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClUIAyLEwOGJd7DNcxk+NuzcWq\nNW1Bnmb67UQpVjvdCM8Q4EdQZZL0Ac0ZKjzqLANhroiIfXbnXd3in5KOIK1Apq9c\nZKDLBVVTrbJXoveOy+TpnIReZm62KTbS7Vi/fvg5xAoY3ta3zgbiZ7z/p607D6mX\n0KVSnqwC0jvnMxKtFwIDAQAB\n-----END PUBLIC KEY-----", 6 | "default": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVi3rSAFNLSs4jUVRL6YjoVJ0XbgQnlpr\nEMr0XmzlPCImOzH8rC+5Mzfto1XXwn5NkMpd3RW0Qy3t9A0ZxijEkw==\n-----END PUBLIC KEY-----\n" 7 | }, 8 | "config": { 9 | "max_user_wallets": 5, 10 | "daily_earn_offers": 80, 11 | "sign_in_types": [ 12 | "jwt", 13 | "whitelist" 14 | ], 15 | "blockchain_version": "3", 16 | "bulk_user_creation_allowed": 51, 17 | "limits": { 18 | "hourly_registration": 200000, 19 | "minute_registration": 10000, 20 | "hourly_total_earn": 5000000, 21 | "minute_total_earn": 85000, 22 | "daily_user_earn": 5000 23 | } 24 | }, 25 | "app_id": "smp3", 26 | "name": "Sample App KIN3" 27 | } 28 | -------------------------------------------------------------------------------- /data/apps/smpl.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "A28hNcn2wp77QyaM8kB2C", 3 | "jwt_public_keys": { 4 | "1": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\nUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\nHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\no2kQ+X5xK9cipRgEKwIDAQAB\n-----END PUBLIC KEY-----\n", 5 | "rs512_1": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClUIAyLEwOGJd7DNcxk+NuzcWq\nNW1Bnmb67UQpVjvdCM8Q4EdQZZL0Ac0ZKjzqLANhroiIfXbnXd3in5KOIK1Apq9c\nZKDLBVVTrbJXoveOy+TpnIReZm62KTbS7Vi/fvg5xAoY3ta3zgbiZ7z/p607D6mX\n0KVSnqwC0jvnMxKtFwIDAQAB\n-----END PUBLIC KEY-----", 6 | "default": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVi3rSAFNLSs4jUVRL6YjoVJ0XbgQnlpr\nEMr0XmzlPCImOzH8rC+5Mzfto1XXwn5NkMpd3RW0Qy3t9A0ZxijEkw==\n-----END PUBLIC KEY-----\n" 7 | }, 8 | "config": { 9 | "max_user_wallets": 50, 10 | "daily_earn_offers": 100, 11 | "sign_in_types": [ 12 | "jwt", 13 | "whitelist" 14 | ], 15 | "blockchain_version": "2", 16 | "bulk_user_creation_allowed": 51, 17 | "limits": { 18 | "hourly_registration": 200000, 19 | "minute_registration": 10000, 20 | "hourly_total_earn": 5000000, 21 | "minute_total_earn": 85000, 22 | "daily_user_earn": 50000 23 | } 24 | }, 25 | "app_id": "smpl", 26 | "name": "Sample App" 27 | } 28 | -------------------------------------------------------------------------------- /data/apps/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "AyINT44OAKagkSav2vzMz", 3 | "jwt_public_keys": { 4 | "rs512_0": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCgOMeb0heN3k9NinbsWpRo3WEl\nwGnoP9UCpKsGLbZAzkgE64fP/thj4CH6xnoRsObl4LFdeh9cekXooS19/A+Q9M0k\nxlrURq8qvogJPSFERi6S7vzMM1LxGmOcgwLLSp4AGZ5A0GkjFXhiZU1hOfb2WmkK\neVTyx22Df6Ox6ulslwIDAQAB\n-----END PUBLIC KEY-----", 5 | "es256_0": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE5ppEUgBzRQpccf9arl/txo8t6hDpGPnK\n1SxVTpadjrlCi4YmKM4xVEzyYSsOQHvWA6I70gT0DyVxSk64zsY0MA==\n-----END PUBLIC KEY-----" 6 | }, 7 | "config": { 8 | "max_user_wallets": null, 9 | "daily_earn_offers": 10, 10 | "sign_in_types": ["jwt", "whitelist"], 11 | "blockchain_version": "3", 12 | "limits": { 13 | "hourly_registration": 200000, 14 | "minute_registration": 10000, 15 | "hourly_total_earn": 5000000, 16 | "minute_total_earn": 85000, 17 | "daily_user_earn": 50000 18 | } 19 | }, 20 | "app_id": "test", 21 | "name": "Test App" 22 | } 23 | -------------------------------------------------------------------------------- /data/translations/test_pt-BR.csv: -------------------------------------------------------------------------------- 1 | Type,Key,Default,Translation,Character Limit 2 | poll,offer:OKKmC7OHkK2GztnaD3VF3:title,Favorites,Favoritos,14 3 | poll,offer:OKKmC7OHkK2GztnaD3VF3:description,Let us know!,Avise-nos!,18 4 | poll,offer:OKKmC7OHkK2GztnaD3VF3:orderTitle,Poll,Enquete,8 5 | poll,offer:OKKmC7OHkK2GztnaD3VF3:orderDescription,Completed,Concluído,24 6 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[0].title,Choose your favorite city,Escolha sua cidade preferida,38 7 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[0].question.answers[0],San Francisco,São Francisco,22 8 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[0].question.answers[1],New York City,Cidade de Nova York,22 9 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[0].question.answers[2],Miami,Miami,22 10 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[0].question.answers[3],Austin,Austin,22 11 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[1].title,Choose your favorite flower,Escolha sua flor preferida,38 12 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[1].question.answers[0],Rose,Rosa,22 13 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[1].question.answers[1],Daffodil,Narciso,22 14 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[1].question.answers[2],Petunia,Petúnia,22 15 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[1].question.answers[3],Daisy,Margarida,22 16 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[2].title,Choose your favorite brand of candy,Escolha sua marca preferida de doces,38 17 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[2].question.answers[0],Nestle,Nestlé,22 18 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[2].question.answers[1],Hershey,Hershey’s,22 19 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[2].question.answers[2],Wonka,Wonka,22 20 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[2].question.answers[3],Wrigley,Wrigley,22 21 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[3].title,Choose your favorite movie monster,Escolha seu monstro de filme preferido,38 22 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[3].question.answers[0],Vampire,Vampiro,22 23 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[3].question.answers[1],Frankenstein,Frankenstein,22 24 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[3].question.answers[2],Godzilla,Godzilla,22 25 | poll,offer_contents:OKKmC7OHkK2GztnaD3VF3:content:pages[3].question.answers[3],Mummy,Múmia,22 26 | -------------------------------------------------------------------------------- /data/version_rules/image.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comparator": ">=1.0.0", 4 | "data": { 5 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/tell_us_more.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_1.png", 6 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/favorites.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_2.png", 7 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/take_a_survaey.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_3.png", 8 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/do_you_like.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_4.png", 9 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/sport.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_5.png", 10 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/movies.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_6.png", 11 | "https://cdn.kinecosystem.com/thumbnails/offers/earn-cover-images-v2/answer_poll.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_7.png", 12 | "https://cdn.kinecosystem.com/thumbnails/offers/quiz_5.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_8.png", 13 | "https://cdn.kinecosystem.com/thumbnails/offers/quiz_2.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_9.png", 14 | "https://cdn.kinecosystem.com/thumbnails/offers/quiz_4.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_9.png", 15 | "https://cdn.kinecosystem.com/thumbnails/offers/quiz_1.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_10.png", 16 | "https://cdn.kinecosystem.com/thumbnails/offers/quiz_3.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/Generic_10.png", 17 | "https://cdn.kinecosystem.com/thumbnails/offers/kin_tutorial.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/kin_logo.png", 18 | "https://cdn.kinecosystem.com/thumbnails/offers/earn_offer3.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/kin_logo.png", 19 | "https://cdn.kinecosystem.com/thumbnails/offers/swelly_kin_tut.png": "https://cdn.kinecosystem.com/thumbnails/offers/222x222/kin_swelly_tutorial.png" 20 | } 21 | } 22 | ] 23 | 24 | 25 | -------------------------------------------------------------------------------- /docker-compose.deps.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | payment-web: 5 | image: kinecosystem/payment-service:2d5d8a6 6 | ports: 7 | - 80 8 | links: 9 | - redis 10 | environment: 11 | &payment_env_vars 12 | APP_DEBUG: 'False' 13 | APP_PORT: 80 14 | APP_HOST: '0.0.0.0' 15 | APP_NAME: 'payment-service' 16 | APP_REDIS: redis://redis:6379/0 17 | STELLAR_INITIAL_XLM_AMOUNT: 2 18 | STELLAR_BASE_SEED: ${STELLAR_BASE_SEED} 19 | STELLAR_HORIZON_URL: 'https://horizon-playground.kininfrastructure.com' 20 | STELLAR_NETWORK: 'Kin Playground Network ; June 2018' 21 | STELLAR_KIN_ISSUER_ADDRESS: 'GBC3SG6NGTSZ2OMH3FFGB7UVRQWILW367U4GSOOF4TFSZONV42UJXUH7' 22 | STELLAR_KIN_TOKEN_NAME: 'KIN' 23 | 24 | payment-worker: 25 | image: kinecosystem/payment-service:2d5d8a6 26 | command: pipenv run python worker.py 27 | links: 28 | - redis 29 | environment: 30 | <<: *payment_env_vars 31 | CHANNEL_SALT: some_salt 32 | MAX_CHANNELS: 1 33 | 34 | payment-watcher: 35 | image: kinecosystem/payment-service:2d5d8a6 36 | command: pipenv run python watcher.py 37 | links: 38 | - redis 39 | environment: 40 | <<: *payment_env_vars 41 | 42 | payment-web-v3: 43 | image: kinecosystem/payment-service-v3:b56f930 44 | ports: 45 | - 80 46 | links: 47 | - redis 48 | environment: 49 | &payment3_env_vars 50 | APP_DEBUG: 'False' 51 | APP_PORT: 80 52 | APP_HOST: '0.0.0.0' 53 | APP_NAME: 'payment3-service' 54 | APP_REDIS: redis://redis:6379/1 55 | STELLAR_INITIAL_XLM_AMOUNT: 2 56 | STELLAR_BASE_SEED: ${STELLAR_BASE_SEED} 57 | STELLAR_HORIZON_URL: 'https://horizon-testnet.kininfrastructure.com' 58 | STELLAR_NETWORK: 'Kin Testnet ; December 2018' 59 | 60 | payment-worker-v3: 61 | image: kinecosystem/payment-service-v3:b56f930 62 | command: pipenv run python worker.py 63 | links: 64 | - redis 65 | environment: 66 | <<: *payment3_env_vars 67 | CHANNEL_SALT: some_salt 68 | MAX_CHANNELS: 1 69 | 70 | payment-watcher-v3: 71 | image: kinecosystem/payment-service-v3:b56f930 72 | command: pipenv run python watcher.py 73 | links: 74 | - redis 75 | environment: 76 | <<: *payment3_env_vars 77 | 78 | migration-service: 79 | image: kinecosystem/migration-server:1911c1c 80 | restart: on-failure 81 | ports: 82 | - 8000 83 | environment: 84 | MAIN_SEED: ${STELLAR_BASE_SEED} 85 | PROXY_SALT: 'low_sodium' 86 | CHANNEL_COUNT: 5 87 | KIN_ISSUER: 'GBC3SG6NGTSZ2OMH3FFGB7UVRQWILW367U4GSOOF4TFSZONV42UJXUH7' 88 | OLD_HORIZON: 'https://horizon-playground.kininfrastructure.com' 89 | NEW_HORIZON: 'https://horizon-testnet.kininfrastructure.com' 90 | NEW_PASSPHRASE: 'Kin Testnet ; December 2018' 91 | APP_ID: 'mgsv' 92 | DEBUG: 'TRUE' 93 | 94 | redis: 95 | image: redis:4.0 96 | ports: 97 | - 6379 98 | 99 | jwt-service: 100 | image: kinecosystem/jwt-service:1eaf6e5 101 | ports: 102 | - 80 103 | environment: 104 | APP_DEBUG: 'False' 105 | APP_PORT: 80 106 | APP_HOST: '0.0.0.0' 107 | APP_NAME: 'smpl' 108 | APP_MARKETPLACE_SERVICE: 'marketplace-public' 109 | 110 | jwt-service-v3: 111 | image: kinecosystem/jwt-service:1eaf6e5 112 | ports: 113 | - 80 114 | environment: 115 | APP_DEBUG: 'False' 116 | APP_PORT: 80 117 | APP_HOST: '0.0.0.0' 118 | APP_NAME: 'smp3' 119 | APP_MARKETPLACE_SERVICE: 'marketplace-public' 120 | 121 | bi-service: 122 | image: mendhak/http-https-echo 123 | ports: 124 | - 80 125 | - 443 126 | -------------------------------------------------------------------------------- /docker-compose.tests.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | psql: 5 | image: postgres:10.4 6 | links: 7 | - postgres 8 | entrypoint: psql -h postgres -U user -d ecosystem 9 | environment: 10 | PGPASSWORD: pass 11 | 12 | redis-cli: 13 | image: redis:4.0 14 | links: 15 | - redis 16 | entrypoint: redis-cli -h redis -p 6379 17 | 18 | create-db: 19 | image: kinecosystem/marketplace-server:latest 20 | volumes: 21 | - .:/opt/app 22 | links: 23 | - postgres 24 | command: npm run manage-db-data -- --apps-dir data/apps --offers-dir data/offers --trans-file data/translations/pt-BR.csv --trans-lang pt-BR --rules-dir data/version_rules --app-list ALL --create-db 25 | environment: 26 | APP_DB_TYPE: postgres 27 | APP_DB_USERNAME: user 28 | APP_DB_PASSWORD: pass 29 | APP_DB_PORT: 5432 30 | APP_DB_HOST: postgres 31 | APP_DB_DATABASE: ecosystem 32 | STELLAR_ADDRESS: ${STELLAR_ADDRESS} 33 | 34 | marketplace-console: 35 | image: kinecosystem/marketplace-server:latest 36 | volumes: 37 | - .:/opt/app 38 | links: 39 | - postgres 40 | command: node --experimental-repl-await ./scripts/bin/node-console.js 41 | environment: 42 | APP_DB_TYPE: postgres 43 | APP_DB_USERNAME: user 44 | APP_DB_PASSWORD: pass 45 | APP_DB_PORT: 5432 46 | APP_DB_HOST: postgres 47 | APP_DB_DATABASE: ecosystem 48 | STELLAR_ADDRESS: ${STELLAR_ADDRESS} 49 | 50 | test-system: 51 | image: kinecosystem/marketplace-server:latest 52 | volumes: 53 | - .:/opt/app 54 | links: 55 | - marketplace-public 56 | - jwt-service 57 | - marketplace-admin 58 | environment: 59 | MARKETPLACE_BASE: http://marketplace-public 60 | JWT_SERVICE_BASE: http://jwt-service 61 | JWT_SERVICE_BASE_V3: http://jwt-service-v3 62 | MIGRATION_SERVICE: http://migration-service:8000 63 | ADMIN_BASE: http://marketplace-admin 64 | command: npm run test-system 65 | 66 | generate-funding-address: 67 | image: kinecosystem/payment-service:236500b 68 | command: pipenv run python generate_funding_address.py 69 | volumes: 70 | - ./secrets:/secrets 71 | environment: 72 | OUTPUT_DIR: /secrets 73 | KIN_FAUCET: 'http://faucet-playground.kininfrastructure.com' 74 | XLM_FAUCET: 'http://friendbot-playground.kininfrastructure.com' 75 | STELLAR_HORIZON_URL: 'https://horizon-playground.kininfrastructure.com' 76 | STELLAR_NETWORK: 'Kin Playground Network ; June 2018' 77 | STELLAR_KIN_ISSUER_ADDRESS: 'GBC3SG6NGTSZ2OMH3FFGB7UVRQWILW367U4GSOOF4TFSZONV42UJXUH7' 78 | STELLAR_KIN_TOKEN_NAME: 'KIN' 79 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | marketplace-public: 6 | image: kinecosystem/marketplace-server:latest 7 | command: ./tests/wait-for -t 60 marketplace-internal:80 -- npm run start 8 | restart: "no" 9 | volumes: 10 | - .:/opt/app 11 | ports: 12 | - 3000:80 13 | links: 14 | - redis 15 | - postgres 16 | - payment-web 17 | - bi-service 18 | environment: 19 | &app_env_vars 20 | APP_DEBUG: 'True' 21 | APP_PORT: 80 22 | APP_HOST: '0.0.0.0' 23 | APP_NAME: 'marketplace-public' 24 | APP_REDIS: redis://redis:6379/0 25 | APP_PAYMENT_SERVICE: http://payment-web 26 | APP_PAYMENT_SERVICE_V3: http://payment-web-v3 27 | APP_INTERNAL_SERVICE: http://marketplace-internal 28 | APP_BI_SERVICE: http://bi-service 29 | APP_DB_TYPE: postgres 30 | APP_DB_USERNAME: user 31 | APP_DB_PASSWORD: pass 32 | APP_DB_PORT: 5432 33 | APP_DB_HOST: postgres 34 | APP_DB_DATABASE: ecosystem 35 | NODEMON_WATCH_DIRS: ${NODEMON_WATCH_DIRS} 36 | CACHE_TTL: '{ "default": 0, "application": 10 }' 37 | MIGRATION_SERVICE: http://migration-service:8000 38 | NUM_PROCESSES: 1 39 | 40 | marketplace-admin: 41 | image: kinecosystem/marketplace-server:latest 42 | restart: "no" 43 | command: ./tests/wait-for -t 60 marketplace-public:80 -- npm run start-admin 44 | volumes: 45 | - .:/opt/app 46 | ports: 47 | - 3002:80 48 | - 3003:3003 49 | links: 50 | - redis 51 | - payment-web 52 | - postgres 53 | - marketplace-public # so I can wait for it 54 | environment: 55 | <<: *app_env_vars 56 | APP_NAME: 'marketplace-admin' 57 | 58 | marketplace-internal: 59 | image: kinecosystem/marketplace-server:latest 60 | restart: "no" 61 | command: ./tests/wait-for -t 60 postgres:5432 -- npm run start-internal 62 | volumes: 63 | - .:/opt/app 64 | ports: 65 | - 3001:80 66 | links: 67 | - redis 68 | - payment-web 69 | - postgres 70 | - marketplace-public # so I can wait for it 71 | environment: 72 | <<: *app_env_vars 73 | APP_NAME: 'marketplace-internal' 74 | APP_JWT_KEYS_DIR: /opt/app/jwt 75 | 76 | postgres: 77 | image: postgres:10.4 78 | ports: 79 | - 25432:5432 80 | environment: 81 | POSTGRES_USER: user 82 | POSTGRES_PASSWORD: pass 83 | POSTGRES_DB: ecosystem 84 | -------------------------------------------------------------------------------- /examples/get_order.sh: -------------------------------------------------------------------------------- 1 | curl -XGET -H 'Authorization: Bearer 7VwMBEsFuj' -H 'X-REQUEST-ID: 1234' "localhost:3000/v1/orders/TWrT0pb8C0N" 2 | -------------------------------------------------------------------------------- /examples/kik-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVi3rSAFNLSs4jUVRL6YjoVJ0XbgQnlpr 3 | EMr0XmzlPCImOzH8rC+5Mzfto1XXwn5NkMpd3RW0Qy3t9A0ZxijEkw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /examples/offers.sh: -------------------------------------------------------------------------------- 1 | curl -H 'Authorization: Bearer s1WxZ1KL17' -H 'X-REQUEST-ID: 1234' "localhost:3000/v1/orders" 2 | -------------------------------------------------------------------------------- /examples/open_order.sh: -------------------------------------------------------------------------------- 1 | curl -XPOST -H 'Authorization: Bearer mIVw62gm5C' -H 'X-REQUEST-ID: 1234' "localhost:3000/v1/offers/OfG71lBD14Y/orders" 2 | -------------------------------------------------------------------------------- /examples/orders.sh: -------------------------------------------------------------------------------- 1 | curl -H 'Authorization: Bearer mIVw62gm5C' -H 'X-REQUEST-ID: 1234' "localhost:3000/v1/orders" 2 | -------------------------------------------------------------------------------- /examples/reg.sh: -------------------------------------------------------------------------------- 1 | curl -XPOST -d'{"sign_in_type": "whitelist", "user_id": "new_user4", "device_id": "my_device", "app_id": "kik", "public_address": "GDNI5XYHLGZMLDNJMX7W67NBD3743AMK7SN5BBNAEYSCBD6WIW763F2H"}' -H 'X-REQUEST-ID: 1234' -H 'content-type: application/json' "localhost:3000/v1/users" 2 | -------------------------------------------------------------------------------- /examples/smpl-1-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw 3 | 33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW 4 | +jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB 5 | AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS 6 | 3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp 7 | uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE 8 | 2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0 9 | GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K 10 | Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY 11 | 6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5 12 | fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523 13 | Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP 14 | FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/smpl-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 3 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 4 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 5 | o2kQ+X5xK9cipRgEKwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /examples/smpl-default-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHQCAQEEIOK5/iY/Yd9ddPkqLK6fbAQ49c+zT7faViwDSfjqAr/ioAcGBSuBBAAK 3 | oUQDQgAEVi3rSAFNLSs4jUVRL6YjoVJ0XbgQnlprEMr0XmzlPCImOzH8rC+5Mzft 4 | o1XXwn5NkMpd3RW0Qy3t9A0ZxijEkw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/smpl-default.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVi3rSAFNLSs4jUVRL6YjoVJ0XbgQnlpr 3 | EMr0XmzlPCImOzH8rC+5Mzfto1XXwn5NkMpd3RW0Qy3t9A0ZxijEkw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /examples/submit_earn_order.sh: -------------------------------------------------------------------------------- 1 | curl -XPOST -d'{"a": "b"}' -H 'Authorization: Bearer mIVw62gm5C' -H 'X-REQUEST-ID: 1234' "localhost:3000/v1/orders/TBtCs7cvazK" 2 | -------------------------------------------------------------------------------- /examples/test-es256_0-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHQCAQEEIEru2jLI5tOuVzCPnfB7iLeqYPLz+pF8xCFFljVBCZ0JoAcGBSuBBAAK 3 | oUQDQgAE5ppEUgBzRQpccf9arl/txo8t6hDpGPnK1SxVTpadjrlCi4YmKM4xVEzy 4 | YSsOQHvWA6I70gT0DyVxSk64zsY0MA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/test-es256_0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE5ppEUgBzRQpccf9arl/txo8t6hDpGPnK 3 | 1SxVTpadjrlCi4YmKM4xVEzyYSsOQHvWA6I70gT0DyVxSk64zsY0MA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /examples/test-rs512_0-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQCgOMeb0heN3k9NinbsWpRo3WElwGnoP9UCpKsGLbZAzkgE64fP 3 | /thj4CH6xnoRsObl4LFdeh9cekXooS19/A+Q9M0kxlrURq8qvogJPSFERi6S7vzM 4 | M1LxGmOcgwLLSp4AGZ5A0GkjFXhiZU1hOfb2WmkKeVTyx22Df6Ox6ulslwIDAQAB 5 | AoGBAIWxm248iwcIgCf16HIkyz4NrGix/C2eRgJkoYkQT8oX1ySz3KoEv0/n0LLP 6 | N0DX2nPrYFTd+JlUNk19YNaFo/5BGk0ewUpHaDvNgS52W2rSkxwO+C0K14yrFnjy 7 | rFTxytGexBVHyxV6VxYvB/2ZAAjw4C/fn7RIbj5cVsS9/xGBAkEAzloDnbM/uoF6 8 | JjioslMLhdgRBgZ8sEjvfk17pExVXF3Lg7o6sMqK9wN5QdI6CDBKDdpYAndHCxxC 9 | Pd75WKcO4QJBAMbFdAzAl5ZObMrrW3N+To6FKHW8emWcHtPE/hBkzWeBljhVDF5w 10 | wjTP0YtgWsSesfy6IsTW0WG7eoZczRJbwncCQQCjcf0HPmGuErxz5dEJXmn0HA5v 11 | 3VeKMlswiaLzolrCjLCUqD+wpN2phiDXl/LLZaRikJ3BZkTpcfquEx0hsUFBAkAY 12 | 9IlRYIynjkkSYOImaeEq+4TM41DqmOM16zGKlV1EdXyKrgLTiIyZXM4OFZbPwKzP 13 | 8f6Tf/ThtEv5uoT0nRiZAkBnWFvJ7RPFcbsKJzNfciakbpi0Hn9W1m+PTa2OfvQH 14 | KaZ/jti09jWmU9sPwS2xxOAgx9w0kVViwWaZuypBNrTc 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/test-rs512_0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCgOMeb0heN3k9NinbsWpRo3WEl 3 | wGnoP9UCpKsGLbZAzkgE64fP/thj4CH6xnoRsObl4LFdeh9cekXooS19/A+Q9M0k 4 | xlrURq8qvogJPSFERi6S7vzMM1LxGmOcgwLLSp4AGZ5A0GkjFXhiZU1hOfb2WmkK 5 | eVTyx22Df6Ox6ulslwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # File needs to be called /hooks/build relative to the Dockerfile. 4 | # $IMAGE_NAME var is injected into the build so the tag is correct. 5 | 6 | make build-image 7 | -------------------------------------------------------------------------------- /hooks/push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # File needs to be called /hooks/build relative to the Dockerfile. 4 | # $IMAGE_NAME var is injected into the build so the tag is correct. 5 | 6 | make push-image 7 | -------------------------------------------------------------------------------- /jwt/private_keys/README.md: -------------------------------------------------------------------------------- 1 | here goes the jwt private keys 2 | -------------------------------------------------------------------------------- /jwt/private_keys/rs512_0-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw 3 | 33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW 4 | +jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB 5 | AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS 6 | 3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp 7 | uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE 8 | 2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0 9 | GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K 10 | Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY 11 | 6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5 12 | fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523 13 | Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP 14 | FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw== 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /jwt/public_keys/README.md: -------------------------------------------------------------------------------- 1 | here goes the jwt public keys 2 | -------------------------------------------------------------------------------- /jwt/public_keys/rs512_0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 3 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 4 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 5 | o2kQ+X5xK9cipRgEKwIDAQAB 6 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /operational/README.md: -------------------------------------------------------------------------------- 1 | # Operational scripts and (hopefully) deploy scripts 2 | 3 | For example: 4 | * create_keys.sh - on a machine with write permissions to AWS, create 10 ES256 keys and write them to AWS parameter store 5 | * get_keys.sh - on the deployed machine, run this to read keys from AWS parameter store and place them under /opt/marketplace-server/keys 6 | -------------------------------------------------------------------------------- /operational/create_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # usage: program [PUBLISH] 3 | # example: program /tmp/out 4 | # example: program /tmp/out publish 5 | 6 | 7 | if [ $# -ge 1 ] 8 | then 9 | DIR=$1 10 | else 11 | DIR=. 12 | fi 13 | 14 | 15 | if [ $# -ge 2 ] && [ $2 == publish ] 16 | then 17 | PUBLISH=1 18 | else 19 | PUBLISH=0 20 | fi 21 | 22 | mkdir -p $DIR/jwt/private_keys 23 | mkdir -p $DIR/jwt/public_keys 24 | 25 | rm -f $DIR/jwt/private_keys/es256_* 26 | rm -f $DIR/jwt/public_keys/es256_* 27 | 28 | for i in `seq 1 10`; do 29 | uuid=$(uuidgen) 30 | 31 | pub=es256_$uuid.pem 32 | priv=es256_$uuid-priv.pem 33 | 34 | openssl ecparam -name secp256k1 -genkey -noout -out $DIR/jwt/private_keys/$priv 35 | openssl ec -in $DIR/jwt/private_keys/$priv -pubout -out $DIR/jwt/public_keys/$pub 36 | 37 | if [ $PUBLISH -eq 1 ] 38 | then 39 | aws ssm put-parameter --name prod-jwt-$pub --type "String" --overwrite --value "$(cat $DIR/jwt/public_keys/$pub)" 40 | aws ssm put-parameter --name prod-jwt-$priv --type "SecureString" --overwrite --value "$(cat $DIR/jwt/private_keys/$priv)" 41 | fi 42 | done 43 | -------------------------------------------------------------------------------- /operational/get_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## reads all public/ private keys starting with 'prod-jwt' and ending with 'pem' and writes them to a dir, removing the 'prod-' part from the filename 3 | 4 | if [ $# == 1 ] && [ $1 == private ] 5 | then 6 | PUBLIC_FLAG= 7 | DIR=jwt/private_keys 8 | else 9 | PUBLIC_FLAG=-v 10 | DIR=jwt/public_keys 11 | fi 12 | 13 | keys=`aws --region=us-east-1 ssm describe-parameters | jq -r '.Parameters[].Name' | grep 'prod-jwt-.*pem' | grep $PUBLIC_FLAG -- '-priv.pem'` 14 | 15 | # empty out current dir items 16 | rm -f $DIR/*.pem 17 | 18 | for key in $keys; do 19 | # write the keys with the same name as the parameter name stripping off the "prod-jwt-" prefix 20 | mkdir -p $DIR 21 | aws --region=us-east-1 ssm get-parameters --names $key --with-decryption | jq -r '.Parameters[] | select(.Name == "'$key'") | .Value' > /opt/marketplace-server/$DIR/${key:9} 22 | done 23 | 24 | echo wrote keys to $DIR 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marketplace-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/kinfoundation/marketplace-server" 9 | }, 10 | "dependencies": { 11 | "argparse": "^1.0.10", 12 | "axios": "^0.18.1", 13 | "axios-retry": "^3.1.1", 14 | "body-parser": "^1.18.2", 15 | "cookie-parser": "^1.4.3", 16 | "csv-parse": "^4.4.6", 17 | "export-to-csv": "^0.1.2", 18 | "express": "^4.16.2", 19 | "express-async-errors": "^2.1.2", 20 | "express-bearer-token": "^2.1.1", 21 | "express-http-context": "^1.2.0", 22 | "hot-shots": "^5.9.1", 23 | "http-status-codes": "^1.3.0", 24 | "jsonwebtoken": "^8.1.1", 25 | "moment": "^2.20.1", 26 | "nodemon": "^1.18.8", 27 | "pg": "7.4.3", 28 | "pg-hstore": "2.3.2", 29 | "redis": "^2.8.0", 30 | "redis-mock": "^0.21.0", 31 | "redlock": "^3.1.2", 32 | "reflect-metadata": "^0.1.12", 33 | "semver": "^6.0.0", 34 | "source-map-support": "^0.5.4", 35 | "to": "^0.2.9", 36 | "typeorm": "^0.1.12", 37 | "update": "^0.7.4", 38 | "uuid": "^3.2.1", 39 | "winston": "^2.4.0" 40 | }, 41 | "devDependencies": { 42 | "@kinecosystem/kin.js": "^2.0.1", 43 | "@kinecosystem/kin.js-v1": "^1.1.1", 44 | "@types/argparse": "^1.0.35", 45 | "@types/body-parser": "^1.16.8", 46 | "@types/csv-parse": "^1.1.11", 47 | "@types/expect": "^1.20.3", 48 | "@types/express": "^4.11.0", 49 | "@types/jest": "^23.3.10", 50 | "@types/jsonwebtoken": "^7.2.5", 51 | "@types/node": "^11.0.0", 52 | "@types/redis": "^2.8.6", 53 | "@types/redis-mock": "^0.17.0", 54 | "@types/redlock": "^3.0.1", 55 | "@types/semver": "^5.5.0", 56 | "@types/sequelize": "^4.27.4", 57 | "@types/stellar-sdk": "^0.10.4", 58 | "@types/supertest": "^2.0.4", 59 | "@types/type-is": "^1.6.2", 60 | "@types/uuid": "^3.4.4", 61 | "codecov": "^3.1.0", 62 | "expect": "^24.1.0", 63 | "jest": "^23.6.0", 64 | "npm-run-all": "^4.0.1", 65 | "rimraf": "2.6.1", 66 | "supertest": "^3.0.0", 67 | "ts-jest": "^23.10.5", 68 | "tslint": "^5.9.1", 69 | "tslint-eslint-rules": "^4.1.1", 70 | "typescript": "^2.8" 71 | }, 72 | "scripts": { 73 | "clean": "rimraf scripts/bin", 74 | "transpile": "tsc -p .", 75 | "lint": "./node_modules/.bin/tslint -p .", 76 | "build": "npm-run-all clean lint transpile", 77 | "start": "nodemon $NODEMON_WATCH_DIRS scripts/bin/public/index", 78 | "start-internal": "nodemon $NODEMON_WATCH_DIRS scripts/bin/internal/index", 79 | "start-admin": "nodemon $NODEMON_WATCH_DIRS scripts/bin/admin/index", 80 | "start-marketplace-public": "node scripts/bin/public/index", 81 | "start-marketplace-internal": "node scripts/bin/internal/index", 82 | "start-marketplace-admin": "node scripts/bin/admin/index", 83 | "manage-db-data": "node scripts/bin/manage_db_data", 84 | "restart": "npm-run-all build start", 85 | "remote-repl": "node ./scripts/bin/repl-client.js localhost:3003", 86 | "test-system": "node scripts/bin/mock_client", 87 | "test": "jest --forceExit --runInBand --detectOpenHandles" 88 | }, 89 | "author": "", 90 | "license": "ISC", 91 | "jest": { 92 | "moduleFileExtensions": [ 93 | "ts", 94 | "tsx", 95 | "js" 96 | ], 97 | "roots": [ 98 | "" 99 | ], 100 | "transform": { 101 | "^.+\\.(ts|tsx)$": "ts-jest" 102 | }, 103 | "testMatch": [ 104 | "/scripts/src/tests/**.spec.ts" 105 | ], 106 | "setupFiles": [ 107 | "/scripts/src/tests/init_tests.ts" 108 | ], 109 | "coverageDirectory": "/coverage/", 110 | "collectCoverageFrom": [ 111 | "/scripts/src/**/*.ts", 112 | "!/scripts/src/analytics/**", 113 | "!/scripts/src/admin/**", 114 | "!/scripts/src/create_data/**", 115 | "!/scripts/src/mock_client.ts", 116 | "!/scripts/src/manage_db_data.ts", 117 | "!/scripts/src/adapt_translation_csv.ts", 118 | "!/scripts/src/node-console.ts", 119 | "!/scripts/src/tests/**", 120 | "!/scripts/src/**/*.d.ts" 121 | ], 122 | "collectCoverage": true 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /scripts/lib/express-bearer-token.d.ts: -------------------------------------------------------------------------------- 1 | declare module "express-bearer-token"; 2 | -------------------------------------------------------------------------------- /scripts/src/adapt_translation_csv.ts: -------------------------------------------------------------------------------- 1 | import csvParse = require("csv-parse/lib/sync"); 2 | import { ExportToCsv, Options as ExportCsvOptions } from "export-to-csv"; 3 | 4 | import { readFileSync, writeFile } from "fs"; 5 | 6 | import { path } from "./utils/path"; 7 | import { CsvParse, TranslationData } from "./admin/translations"; 8 | 9 | function getOfferTranslation(inputCsv: TranslationData) { 10 | return inputCsv.reduce((dict, [type, key, defaultStr, translation]) => { 11 | if (!translation || !translation.length) { 12 | return dict; 13 | } 14 | dict[defaultStr] = translation; 15 | return dict; 16 | }, {} as { [defaultStr: string]: string }); 17 | } 18 | 19 | async function addTranslationTo(csv: TranslationData, fromDict: { [defaultStr: string]: string }) { 20 | return csv.map(([type, key, defaultStr, __, charLimit]) => { 21 | return { 22 | Type: type, 23 | Key: key, 24 | Default: defaultStr, 25 | Translation: fromDict[defaultStr] || defaultStr, 26 | "Character Limit": charLimit, 27 | }; 28 | }); 29 | } 30 | 31 | function writeCsvDataToFile(data: any[], fileName: string, resolve: (value?: any) => void, reject: (reason?: any) => void) { 32 | const options = { 33 | fieldSeparator: ",", 34 | quoteStrings: "\"", 35 | decimalseparator: ".", 36 | showLabels: true, 37 | showTitle: false, 38 | useBom: true, 39 | useKeysAsHeaders: true, 40 | }; 41 | 42 | const csvExporter = new ExportToCsv(options); 43 | writeFile(fileName, csvExporter.generateCsv(data, true), err => { 44 | if (err) { 45 | console.error("Error:", err); 46 | reject(err); 47 | return; 48 | } 49 | resolve(fileName); 50 | }); 51 | } 52 | 53 | export async function processFile(translationFile: string, fileToTranslate: string, saveAs: string | null = null, rowOffSet: number = 1) { 54 | /*** 55 | * Takes a template (CSV) file (fileToTranslate) generated by admin/translations.writeCsvTemplateToFile and a translation file (translationFile) 56 | * and adapts the translations to the offers in the template, Saves the to the file specified with SaveAs 57 | ***/ 58 | return new Promise(async (resolve, reject) => { 59 | if (!translationFile || !fileToTranslate) { 60 | console.error("Both input and output file are required"); 61 | reject("Both input and output file are required"); 62 | return; 63 | } 64 | const translationCsv = readFileSync(path(translationFile)); 65 | const outputCsv = readFileSync(path(fileToTranslate)); 66 | const parsedTranslationCsv = (csvParse as CsvParse)(translationCsv); 67 | const parsedCsvToTranslate = (csvParse as CsvParse)(outputCsv); 68 | parsedTranslationCsv.splice(0, rowOffSet); 69 | parsedCsvToTranslate.splice(0, rowOffSet); 70 | const offerToTranslationDict = getOfferTranslation(parsedTranslationCsv); 71 | const translatedData = await addTranslationTo(parsedCsvToTranslate, offerToTranslationDict); 72 | if (!saveAs) { 73 | const segments = fileToTranslate.split("."); 74 | segments[0] = segments[0] + "-translated"; 75 | saveAs = segments.join("."); 76 | } 77 | // console.log("translatedData", translatedData); 78 | writeCsvDataToFile(translatedData, saveAs, resolve, reject); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /scripts/src/admin/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import "express-async-errors"; // handle async/await errors in middleware 3 | 4 | import { getConfig } from "./config"; 5 | import { initLogger } from "../logging"; 6 | import { init as initMigration } from "./migration"; 7 | 8 | const config = getConfig(); 9 | const logger = initLogger(...config.loggers!); 10 | import { createRoutes } from "./routes"; 11 | import { init as initModels } from "../models/index"; 12 | import { notFoundHandler, generalErrorHandler } from "../middleware"; 13 | 14 | function createApp() { 15 | const app = express(); 16 | app.set("port", getConfig().port); 17 | 18 | const bodyParser = require("body-parser"); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | 22 | return app; 23 | } 24 | 25 | export const app: express.Express = createApp(); 26 | 27 | // routes 28 | createRoutes(app); 29 | 30 | // catch 404 31 | app.use(notFoundHandler); 32 | // catch errors 33 | app.use(generalErrorHandler); 34 | 35 | async function init() { 36 | await initMigration(); 37 | await initModels(); 38 | } 39 | 40 | // initializing db and models 41 | init().then(msg => { 42 | logger.info("init admin"); 43 | }); 44 | -------------------------------------------------------------------------------- /scripts/src/admin/config.ts: -------------------------------------------------------------------------------- 1 | import { Config as BaseConfig, getConfig as baseGetConfig, init as baseInit } from "../config"; 2 | 3 | export interface Config extends BaseConfig { 4 | } 5 | 6 | export function getConfig(): Config { 7 | return baseGetConfig(); 8 | } 9 | 10 | function init(): void { 11 | baseInit(`config/internal.default.json`); 12 | } 13 | 14 | init(); 15 | -------------------------------------------------------------------------------- /scripts/src/admin/index.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as net from "net"; 3 | 4 | import { getConfig } from "./config"; 5 | 6 | import { app } from "./app"; 7 | 8 | import { onError, onListening } from "../server"; 9 | import { start as nodeConsoleStart } from "../node-console"; 10 | 11 | const server = http.createServer(app); 12 | server.listen(getConfig().port); 13 | server.on("error", onError); 14 | server.on("listening", onListening(server)); 15 | 16 | const remoteReplServer = net.createServer(socket => { 17 | console.log("new connection, environment_name:", process.env.environment_name); 18 | nodeConsoleStart(socket, process.env.environment_name === "production" ? "Your on production!!!" : ""); 19 | }); 20 | 21 | remoteReplServer.listen(3003); 22 | remoteReplServer.on("error", onError); 23 | remoteReplServer.on("listening", onListening(remoteReplServer)); 24 | 25 | module.exports = { server, remoteReplServer }; 26 | -------------------------------------------------------------------------------- /scripts/src/admin/migration.ts: -------------------------------------------------------------------------------- 1 | // for admin use only 2 | import { getAxiosClient } from "../utils/axios_client"; 3 | import { WalletResponse } from "../utils/migration"; 4 | import { BlockchainConfig, getBlockchainConfig } from "../public/services/payment"; 5 | 6 | const httpClient = getAxiosClient({ timeout: 3000 }); 7 | let BLOCKCHAIN: BlockchainConfig; 8 | let BLOCKCHAIN3: BlockchainConfig; 9 | 10 | export async function init() { 11 | BLOCKCHAIN = await getBlockchainConfig("2"); 12 | BLOCKCHAIN3 = await getBlockchainConfig("3"); 13 | } 14 | 15 | export async function getKin2Balance(walletAddress: string): Promise { 16 | try { 17 | const res = await httpClient.get(`${ BLOCKCHAIN.horizon_url }/accounts/${ walletAddress }`); 18 | for (const balance of res.data.balances) { 19 | if (balance.asset_issuer === BLOCKCHAIN.asset_issuer && 20 | balance.asset_code === BLOCKCHAIN.asset_code) { 21 | return parseFloat(balance.balance); 22 | } 23 | } 24 | return 0; // no balance is zero balance 25 | } catch (e) { 26 | return null; 27 | } 28 | } 29 | 30 | export async function getKin3Balance(walletAddress: string): Promise { 31 | try { 32 | 33 | const res = await httpClient.get(`${ BLOCKCHAIN3.horizon_url }/accounts/${ walletAddress }`); 34 | for (const balance of res.data.balances) { 35 | if (balance.asset_type === "native") { 36 | return parseFloat(balance.balance); 37 | } 38 | } 39 | return 0; // no balance is zero balance 40 | } catch (e) { 41 | return null; 42 | } 43 | } 44 | 45 | // returns true if migration service says this address is burned 46 | export async function isBurned(walletAddress: string): Promise { 47 | try { 48 | const res = await httpClient.get(`${ BLOCKCHAIN.horizon_url }/accounts/${ walletAddress }`); 49 | for (const signer of res.data.signers) { 50 | if (signer.weight > 0) { 51 | return false; 52 | } 53 | } 54 | return true; 55 | } catch (e) { 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/src/admin/routes.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | import { promisify } from "util"; 4 | 5 | import { Express, Request, RequestHandler, Response, Router } from "express"; 6 | import { getDefaultLogger as logger } from "../logging"; 7 | 8 | import { 9 | addMigrationUser, 10 | changeAppOffer, 11 | changeOffer, 12 | fuzzySearch, 13 | getApplication, 14 | getApplicationOffers, 15 | getApplications, 16 | getApplicationUserData, 17 | getApplicationUsers, getMigrationStatus, 18 | getOffer, 19 | getOffers, 20 | getOrder, 21 | getOrders, 22 | getPollResults, 23 | getUserData, 24 | getUserOffers, 25 | getWallet, 26 | getWalletPayments, 27 | retryOrder, 28 | retryUserWallet, 29 | updateAppConfig 30 | } from "./services"; 31 | 32 | import { statusHandler } from "../middleware"; 33 | import { getConfig } from "./config"; 34 | 35 | const readFile = promisify(fs.readFile); 36 | 37 | function jsonResponse(func: (body: any, params: any, query: any) => Promise): RequestHandler { 38 | return async function(req: Request, res: Response) { 39 | const content = await func(req.body, req.params, req.query); 40 | res.status(200).json(content); 41 | } as any as RequestHandler; 42 | } 43 | 44 | function wrapService(func: (params: any, query: any) => Promise): RequestHandler { 45 | const genericServiceTemplatePath = "../../src/admin/wrapper.html"; 46 | return async function(req: Request, res: Response) { 47 | const content = await func(req.params, req.query); 48 | const html = (await readFile(path.join(__dirname, genericServiceTemplatePath), { encoding: "utf8" })) 49 | .replace("${ content }", content) 50 | .replace("${ webview }", getConfig().webview) 51 | .replace("${ now }", Date.now().toString()) 52 | .replace(/\$isProduction\$/g, (process.env.environment_name === "production").toString()); 53 | 54 | res.status(200).send(html); 55 | } as any as RequestHandler; 56 | } 57 | 58 | export async function index(params: { app_id: string }, query: any): Promise { 59 | return ``; 65 | } 66 | 67 | export function createRoutes(app: Express, pathPrefix?: string) { 68 | const router = Router(); 69 | router 70 | .get("/applications", wrapService(getApplications)) 71 | .get("/applications/:app_id", wrapService(getApplication)) 72 | .get("/applications/:app_id/offers", wrapService(getApplicationOffers)) 73 | .get("/applications/:app_id/users", wrapService(getApplicationUsers)) 74 | .get("/offers", wrapService(getOffers)) 75 | .get("/orders", wrapService(getOrders)) 76 | .get("/offers/:offer_id", wrapService(getOffer)) 77 | .get("/polls/:offer_id", wrapService(getPollResults)) 78 | .get("/users/:user_id", wrapService(getUserData)) 79 | .get("/users/:user_id/offers", wrapService(getUserOffers)) 80 | .get("/applications/:app_id/users/:app_user_id", wrapService(getApplicationUserData)) 81 | .get("/orders/:order_id", wrapService(getOrder)) 82 | .get("/fuzzy/:some_id", wrapService(fuzzySearch)) 83 | .get("/wallets/:wallet_address", wrapService(getWallet)) 84 | .get("/wallets/:wallet_address/payments", wrapService(getWalletPayments)) 85 | .get("/", wrapService(index)) 86 | // retries 87 | .get("/orders/:order_id/retry", wrapService(retryOrder)) 88 | .get("/users/:user_id/wallets/:wallet/retry", wrapService(retryUserWallet)) 89 | // change data 90 | .post("/applications/:app_id/offers/:offer_id", jsonResponse(changeAppOffer)) 91 | .post("/offers/:offer_id", jsonResponse(changeOffer)) 92 | 93 | .put("/applications/:application_id/config", updateAppConfig) 94 | .post("/migration/users", jsonResponse(addMigrationUser)) 95 | .get("/migration/wallets/:wallet_address/status", jsonResponse(getMigrationStatus)) 96 | 97 | ; 98 | 99 | app.use("", router); 100 | app.get("/status", statusHandler); 101 | logger().info("created routes"); 102 | } 103 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/common.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | import { getConfig } from "../../config"; 3 | 4 | /** 5 | * common properties for all events 6 | */ 7 | export type Common = { 8 | schema_version: string; 9 | event_id: string; 10 | version: string; 11 | device_id: string | null; 12 | user_id: string; 13 | timestamp: string; 14 | platform: "iOS" | "Android" | "Web" | "Server"; 15 | }; 16 | 17 | export function create(userId: string, deviceId?: string): Common { 18 | return { 19 | schema_version: "e98699d6f5dd88a66fc3d31e368a090e7312d7a6", 20 | event_id: uuid(), 21 | version: getConfig().commit!, 22 | device_id: deviceId || null, 23 | user_id: userId, 24 | timestamp: Date.now().toString(), 25 | platform: "Server" 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/earn_transaction_broadcast_to_blockchain_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server fails to create earn transaction on blockchain 12 | */ 13 | export interface EarnTransactionBroadcastToBlockchainFailed extends EventData { 14 | event_name: "earn_transaction_broadcast_to_blockchain_failed"; 15 | event_type: "log"; 16 | common: Common; 17 | error_reason: string; 18 | offer_id: string; 19 | order_id: string; 20 | } 21 | 22 | export function create(user_id: string, error_reason: string, offer_id: string, order_id: string): Event { 23 | return new Event({ 24 | event_name: "earn_transaction_broadcast_to_blockchain_failed", 25 | event_type: "log", 26 | common: createCommon(user_id), 27 | error_reason, 28 | offer_id, 29 | order_id 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/earn_transaction_broadcast_to_blockchain_submitted.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server submits earn transaction to blockchain 12 | */ 13 | export interface EarnTransactionBroadcastToBlockchainSubmitted extends EventData { 14 | event_name: "earn_transaction_broadcast_to_blockchain_submitted"; 15 | event_type: "log"; 16 | common: Common; 17 | offer_id: string; 18 | order_id: string; 19 | } 20 | 21 | export function create(user_id: string, device_id: string, offer_id: string, order_id: string): Event { 22 | return new Event({ 23 | event_name: "earn_transaction_broadcast_to_blockchain_submitted", 24 | event_type: "log", 25 | common: createCommon(user_id, device_id), 26 | offer_id, 27 | order_id 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/earn_transaction_broadcast_to_blockchain_succeeded.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server submits earn transaction to blockchain and get confirmation 12 | */ 13 | export interface EarnTransactionBroadcastToBlockchainSucceeded extends EventData { 14 | event_name: "earn_transaction_broadcast_to_blockchain_succeeded"; 15 | event_type: "log"; 16 | common: Common; 17 | transaction_id: string; 18 | offer_id: string; 19 | order_id: string; 20 | } 21 | 22 | export function create(user_id: string, transaction_id: string, offer_id: string, order_id: string): Event { 23 | return new Event({ 24 | event_name: "earn_transaction_broadcast_to_blockchain_succeeded", 25 | event_type: "log", 26 | common: createCommon(user_id), 27 | transaction_id, 28 | offer_id, 29 | order_id 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/restore_request_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * blocking cross app wallet restore 12 | */ 13 | export interface RestoreRequestFailed extends EventData { 14 | event_name: "restore_request_failed"; 15 | event_type: "log"; 16 | common: Common; 17 | error_reason: string; 18 | } 19 | 20 | export function create(user_id: string, device_id: string, error_reason: string): Event { 21 | return new Event({ 22 | event_name: "restore_request_failed", 23 | event_type: "log", 24 | common: createCommon(user_id, device_id), 25 | error_reason 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/spend_order_payment_confirmed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server tracks the OrderID on the blockchain 12 | */ 13 | export interface SpendOrderPaymentConfirmed extends EventData { 14 | event_name: "spend_order_payment_confirmed"; 15 | event_type: "log"; 16 | common: Common; 17 | transaction_id: string; 18 | offer_id: string; 19 | order_id: string; 20 | is_native: boolean; 21 | origin: "marketplace" | "external"; 22 | } 23 | 24 | export function create(user_id: string, transaction_id: string, offer_id: string, order_id: string, is_native: boolean, origin: "marketplace" | "external"): Event { 25 | return new Event({ 26 | event_name: "spend_order_payment_confirmed", 27 | event_type: "log", 28 | common: createCommon(user_id), 29 | transaction_id, 30 | offer_id, 31 | order_id, 32 | is_native, 33 | origin 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/spend_transaction_broadcast_to_blockchain_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server fails to create spend transaction on blockchain 12 | */ 13 | export interface SpendTransactionBroadcastToBlockchainFailed extends EventData { 14 | event_name: "spend_transaction_broadcast_to_blockchain_failed"; 15 | event_type: "log"; 16 | common: Common; 17 | error_reason: string; 18 | offer_id: string; 19 | order_id: string; 20 | } 21 | 22 | export function create(user_id: string, error_reason: string, offer_id: string, order_id: string): Event { 23 | return new Event({ 24 | event_name: "spend_transaction_broadcast_to_blockchain_failed", 25 | event_type: "log", 26 | common: createCommon(user_id), 27 | error_reason, 28 | offer_id, 29 | order_id 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/spend_transaction_broadcast_to_blockchain_submitted.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Server submits spend transaction to blockchain 12 | */ 13 | export interface SpendTransactionBroadcastToBlockchainSubmitted extends EventData { 14 | event_name: "spend_transaction_broadcast_to_blockchain_submitted"; 15 | event_type: "log"; 16 | common: Common; 17 | offer_id: string; 18 | order_id: string; 19 | } 20 | 21 | export function create(user_id: string, device_id: string, offer_id: string, order_id: string): Event { 22 | return new Event({ 23 | event_name: "spend_transaction_broadcast_to_blockchain_submitted", 24 | event_type: "log", 25 | common: createCommon(user_id, device_id), 26 | offer_id, 27 | order_id 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/stellar_account_creation_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Phase 1 - stellar account creation failed 12 | */ 13 | export interface StellarAccountCreationFailed extends EventData { 14 | event_name: "stellar_account_creation_failed"; 15 | event_type: "log"; 16 | common: Common; 17 | error_reason: string; 18 | } 19 | 20 | export function create(user_id: string, error_reason: string): Event { 21 | return new Event({ 22 | event_name: "stellar_account_creation_failed", 23 | event_type: "log", 24 | common: createCommon(user_id), 25 | error_reason 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/stellar_account_creation_succeeded.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Phase 1 - stellar account creation succeeded 12 | */ 13 | export interface StellarAccountCreationSucceeded extends EventData { 14 | event_name: "stellar_account_creation_succeeded"; 15 | event_type: "log"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string): Event { 20 | return new Event({ 21 | event_name: "stellar_account_creation_succeeded", 22 | event_type: "log", 23 | common: createCommon(user_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_login_server_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * User login failed 12 | */ 13 | export interface UserLoginServerFailed extends EventData { 14 | event_name: "user_login_server_failed"; 15 | event_type: "business"; 16 | common: Common; 17 | error_reason: string; 18 | } 19 | 20 | export function create(user_id: string, error_reason: string): Event { 21 | return new Event({ 22 | event_name: "user_login_server_failed", 23 | event_type: "business", 24 | common: createCommon(user_id), 25 | error_reason 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_login_server_requested.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * when a user login with a different device /user_id , and this device is currently logout 12 | */ 13 | export interface UserLoginServerRequested extends EventData { 14 | event_name: "user_login_server_requested"; 15 | event_type: "business"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "user_login_server_requested", 22 | event_type: "business", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_login_server_succeeded.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * User login success 12 | */ 13 | export interface UserLoginServerSucceeded extends EventData { 14 | event_name: "user_login_server_succeeded"; 15 | event_type: "business"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "user_login_server_succeeded", 22 | event_type: "business", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_logout_server_requested.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * User logout per device_id and user_id combination 12 | */ 13 | export interface UserLogoutServerRequested extends EventData { 14 | event_name: "user_logout_server_requested"; 15 | event_type: "business"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "user_logout_server_requested", 22 | event_type: "business", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_registration_failed.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * New user - registration failure 12 | */ 13 | export interface UserRegistrationFailed extends EventData { 14 | event_name: "user_registration_failed"; 15 | event_type: "business"; 16 | common: Common; 17 | error_reason: string; 18 | } 19 | 20 | export function create(user_id: string, device_id: string, error_reason: string): Event { 21 | return new Event({ 22 | event_name: "user_registration_failed", 23 | event_type: "business", 24 | common: createCommon(user_id, device_id), 25 | error_reason 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_registration_requested.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Only for NEW users - only once per user_id 12 | */ 13 | export interface UserRegistrationRequested extends EventData { 14 | event_name: "user_registration_requested"; 15 | event_type: "business"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "user_registration_requested", 22 | event_type: "business", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/user_registration_succeeded.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * New user - registration success 12 | */ 13 | export interface UserRegistrationSucceeded extends EventData { 14 | event_name: "user_registration_succeeded"; 15 | event_type: "business"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "user_registration_succeeded", 22 | event_type: "business", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/events/wallet_address_update_succeeded.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventData } from "../index"; 2 | import { Common, create as createCommon } from "./common"; 3 | 4 | /** 5 | * This file was automatically generated by json-schema-to-typescript. 6 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 7 | * and run json-schema-to-typescript to regenerate this file. 8 | */ 9 | 10 | /** 11 | * Wallet address updated successfully 12 | */ 13 | export interface WalletAddressUpdateSucceeded extends EventData { 14 | event_name: "wallet_address_update_succeeded"; 15 | event_type: "log"; 16 | common: Common; 17 | } 18 | 19 | export function create(user_id: string, device_id: string): Event { 20 | return new Event({ 21 | event_name: "wallet_address_update_succeeded", 22 | event_type: "log", 23 | common: createCommon(user_id, device_id) 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config"; 2 | import { normalizeError } from "../utils/utils"; 3 | import { getDefaultLogger as logger } from "../logging"; 4 | import { getAxiosClient } from "../utils/axios_client"; 5 | 6 | const httpClient = getAxiosClient({ retries: 2, timeout: 500 }); 7 | 8 | export interface EventData { 9 | } 10 | 11 | export class Event { 12 | private readonly data: T; 13 | 14 | constructor(data: T) { 15 | this.data = data; 16 | } 17 | 18 | public report(): Promise { 19 | try { 20 | return httpClient.post(getConfig().bi_service, this.data) 21 | .catch(e => logger().warn(`failed to report to bi ${ normalizeError(e) }`)) as any; 22 | } catch (e) { 23 | // nothing to do 24 | logger().warn(`failed to report to bi: ${ normalizeError(e) }`); 25 | return Promise.resolve(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/src/config.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from "typeorm"; 2 | import "source-map-support/register"; 3 | 4 | import { path } from "./utils/path"; 5 | 6 | import { LogTarget } from "./logging"; 7 | 8 | export interface LimitConfig { 9 | hourly_migration: number; 10 | minute_migration: number; 11 | hourly_registration: number; 12 | minute_registration: number; 13 | hourly_user_requests: number; 14 | minute_user_requests: number; 15 | hourly_total_earn: number; 16 | minute_total_earn: number; 17 | daily_user_earn: number; 18 | } 19 | 20 | export interface Config { 21 | port?: number; 22 | host: string; 23 | loggers?: LogTarget[]; 24 | db: ConnectionOptions; 25 | redis: "mock" | string; 26 | statsd: { 27 | host: string; 28 | port: number; 29 | }; 30 | payment_service: string; 31 | payment_service_v3: string; 32 | internal_service: string; 33 | app_name?: string; 34 | commit?: string; 35 | timestamp?: string; 36 | bi_service: string; 37 | webview: string; 38 | cache_ttl: { 39 | default: number; 40 | application?: number; 41 | }; 42 | 43 | migration_service?: string; 44 | num_processes?: number; 45 | initial_hourly_migration?: number; 46 | initial_minute_migration?: number; 47 | } 48 | 49 | let config: Config; 50 | 51 | export function init(filePath: string) { 52 | if (config) { 53 | return; 54 | } 55 | config = require(path(filePath!)); 56 | config.port = Number(process.env.APP_PORT || "") || config.port; 57 | config.host = process.env.APP_HOST || config.host; 58 | (config.db as any).type = process.env.APP_DB_TYPE || config.db.type; 59 | (config.db as any).username = process.env.APP_DB_USERNAME || (config.db as any).username; 60 | (config.db as any).password = process.env.APP_DB_PASSWORD || (config.db as any).password; 61 | (config.db as any).port = Number(process.env.APP_DB_PORT) || (config.db as any).port; 62 | (config.db as any).host = process.env.APP_DB_HOST || (config.db as any).host; 63 | (config.db as any).database = process.env.APP_DB_DATABASE || (config.db as any).database; 64 | config.payment_service = process.env.APP_PAYMENT_SERVICE || config.payment_service; 65 | config.payment_service_v3 = process.env.APP_PAYMENT_SERVICE_V3 || config.payment_service_v3; 66 | config.internal_service = process.env.APP_INTERNAL_SERVICE || config.internal_service; 67 | config.bi_service = process.env.APP_BI_SERVICE || config.bi_service; 68 | config.app_name = process.env.APP_NAME || config.app_name; 69 | config.commit = process.env.BUILD_COMMIT || config.commit; 70 | config.timestamp = process.env.BUILD_TIMESTAMP || config.timestamp; 71 | config.redis = process.env.APP_REDIS || config.redis; 72 | config.statsd.host = process.env.STATSD_HOST || config.statsd.host; 73 | config.statsd.port = Number(process.env.STATSD_PORT) || config.statsd.port; 74 | config.num_processes = Number(process.env.NUM_PROCESSES) || config.num_processes; 75 | config.initial_hourly_migration = Number(process.env.INITIAL_HOURLY_MIGRATION) || config.initial_hourly_migration; 76 | config.initial_minute_migration = Number(process.env.INITIAL_MINUTE_MIGRATION) || config.initial_minute_migration; 77 | 78 | const cacheConfig = JSON.parse(process.env.CACH_TTL || "null") || config.cache_ttl || { "default": 30, "application": 10 }; // In seconds 79 | const getHandler = { 80 | get: (obj: Config["cache_ttl"], prop: keyof Config["cache_ttl"]) => { 81 | const currentValue = obj[prop]; 82 | return obj[prop] && typeof currentValue === "number" ? currentValue : cacheConfig.default; 83 | } 84 | }; 85 | config.cache_ttl = new Proxy(cacheConfig, getHandler); // This way if a cache config is accessed but not explicitly defined it will be default 86 | 87 | config.migration_service = process.env.MIGRATION_SERVICE || config.migration_service; 88 | 89 | } 90 | 91 | export function getConfig(): T { 92 | return config as T; 93 | } 94 | -------------------------------------------------------------------------------- /scripts/src/create_data/transform_offer_content_schema.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../public/config"; 2 | import { OfferContent } from "../models/offers"; 3 | import { close as closeModels, init as initModels } from "../models"; 4 | import { Page, PageType, Poll, Quiz, Tutorial } from "../public/services/offer_contents"; 5 | 6 | getConfig(); 7 | 8 | interface RewardPage { 9 | rewardText: string; 10 | rewardValue: "${amount}"; 11 | description: string; 12 | title: string; 13 | footerHtml: string; 14 | } 15 | 16 | const COMPLETE_POLL = "Finish the poll to earn"; 17 | const COMPLETE_QUIZ = "Finish the quiz to earn"; 18 | const COMPLETE_TUTORIAL = "Finish the tutorial to earn"; 19 | 20 | async function dumpOffers() { 21 | const offerContents = await OfferContent.find(); 22 | for (const content of offerContents) { 23 | if (content.contentType === "coupon") { 24 | continue; 25 | } 26 | const poll: Poll = JSON.parse(content.content); 27 | console.log(content.contentType); 28 | console.log("before:", poll); 29 | for (const page of poll.pages) { 30 | if ((page as any as Page).type === PageType.FullPageMultiChoice) { 31 | (page as any as RewardPage).rewardText = COMPLETE_POLL; 32 | (page as any as RewardPage).rewardValue = "${amount}"; 33 | // (page as any as RewardPage).description = ""; // XXX backward support 34 | } else if ((page as any as Page).type === PageType.TimedFullPageMultiChoice) { 35 | (page as any as RewardPage).rewardText = COMPLETE_QUIZ; 36 | (page as any as RewardPage).rewardValue = "${amount}"; 37 | if (!(page as any as RewardPage).title) { // to be able to re-run on same data 38 | (page as any as RewardPage).title = (page as any as RewardPage).description; 39 | } 40 | // (page as any as RewardPage).description = ""; // XXX backward support 41 | } else if ((page as any as Page).type === PageType.ImageAndText) { 42 | (page as any as RewardPage).rewardText = COMPLETE_TUTORIAL; 43 | (page as any as RewardPage).rewardValue = "${amount}"; 44 | delete (page as any as RewardPage).footerHtml; 45 | } else if ((page as any as Page).type === PageType.EarnThankYou) { 46 | (page as any as RewardPage).rewardValue = "${amount}"; 47 | // (page as any as RewardPage).description = ""; // XXX backward support 48 | } 49 | } 50 | 51 | console.log("after:", poll); 52 | content.content = JSON.stringify(poll); 53 | await content.save(); 54 | } 55 | } 56 | 57 | async function main() { 58 | await initModels(); 59 | await dumpOffers(); 60 | } 61 | 62 | main() 63 | .then(() => console.log("done.")) 64 | .catch(e => console.error(e)) 65 | .finally(closeModels); 66 | -------------------------------------------------------------------------------- /scripts/src/internal/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import "express-async-errors"; // handle async/await errors in middleware 3 | 4 | import { getConfig } from "./config"; 5 | import { initLogger } from "../logging"; 6 | 7 | const config = getConfig(); 8 | const logger = initLogger(...config.loggers!); 9 | 10 | import { createRoutes } from "./routes"; 11 | import { initPaymentCallbacks } from "./services"; 12 | import { init as initModels } from "../models/index"; 13 | import { init as initCustomMiddleware, notFoundHandler, generalErrorHandler } from "./middleware"; 14 | 15 | function createApp() { 16 | const app = express(); 17 | app.set("port", config.port); 18 | 19 | const bodyParser = require("body-parser"); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | 23 | initCustomMiddleware(app); 24 | 25 | return app; 26 | } 27 | 28 | export const app: express.Express = createApp(); 29 | 30 | // routes 31 | createRoutes(app); 32 | 33 | // catch 404 34 | app.use(notFoundHandler); 35 | // catch errors 36 | app.use(generalErrorHandler); 37 | 38 | export async function init() { 39 | // initializing db and models 40 | const msg = await initModels(); 41 | logger.info("init db", { msg }); 42 | const res = await initPaymentCallbacks(); 43 | logger.info("init payment result", { res }); 44 | } 45 | -------------------------------------------------------------------------------- /scripts/src/internal/config.ts: -------------------------------------------------------------------------------- 1 | import { Config as BaseConfig, getConfig as baseGetConfig, init as baseInit } from "../config"; 2 | 3 | export interface Config extends BaseConfig { 4 | jwt_keys_dir: string; 5 | } 6 | 7 | export function getConfig(): Config { 8 | return baseGetConfig(); 9 | } 10 | 11 | function init(): void { 12 | let path = "config/internal."; 13 | /*if (process.argv.length === 3) { 14 | path += process.argv[2]; 15 | } else { 16 | path += "default"; 17 | }*/ 18 | path += "default"; 19 | 20 | baseInit(`${ path }.json`); 21 | 22 | const config = getConfig(); 23 | config.jwt_keys_dir = process.env.APP_JWT_KEYS_DIR || config.jwt_keys_dir; 24 | } 25 | 26 | init(); 27 | -------------------------------------------------------------------------------- /scripts/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | 3 | import { getConfig } from "./config"; 4 | import { initLogger } from "../logging"; 5 | 6 | const config = getConfig(); 7 | import { app, init } from "./app"; 8 | 9 | import { onError, onListening } from "../server"; 10 | 11 | const server = http.createServer(app); 12 | 13 | init().then(() => { 14 | server.listen(config.port); 15 | server.on("error", onError); 16 | server.on("listening", onListening(server)); 17 | }).catch(e => { 18 | console.log("failed to start server: " + e); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/src/internal/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment"; 2 | import * as jsonwebtoken from "jsonwebtoken"; 3 | import { readKeysDir } from "../utils/utils"; 4 | import { getConfig } from "./config"; 5 | import * as path from "path"; 6 | 7 | const CONFIG = getConfig(); 8 | const PRIVATE_KEYS = readKeysDir(path.join(CONFIG.jwt_keys_dir, "private_keys")); 9 | export const PUBLIC_KEYS = readKeysDir(path.join(CONFIG.jwt_keys_dir, "public_keys")); 10 | 11 | function asyncJwtSign(payload: object, secretKey: string, options: object): Promise { 12 | return new Promise( 13 | (res, rej) => { 14 | jsonwebtoken.sign(payload, secretKey, options, (err, token) => { 15 | if (err) { 16 | rej(err); 17 | } else { 18 | res(token); 19 | } 20 | }); 21 | } 22 | ); 23 | } 24 | 25 | function getKeyForAlgorithm(alg: string): string { 26 | const keyid = Object.keys(PRIVATE_KEYS).find(k => PRIVATE_KEYS[k].algorithm.toUpperCase() === alg.toUpperCase()); 27 | if (!keyid) { 28 | throw Error(`key not found for algorithm ${alg}`); 29 | } 30 | return keyid; 31 | } 32 | 33 | export async function sign(subject: string, payload: object, alg?: string): Promise{ 34 | const keyid = getKeyForAlgorithm(alg || "es256"); 35 | const signWith = PRIVATE_KEYS[keyid]; 36 | return await asyncJwtSign(payload, signWith.key, { 37 | subject, 38 | keyid, 39 | issuer: "kin", 40 | algorithm: signWith.algorithm, 41 | expiresIn: moment().add(6, "hours").unix() 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/src/internal/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { logRequest, requestLogger, reportMetrics } from "../middleware"; 4 | 5 | export { notFoundHandler, generalErrorHandler, statusHandler } from "../middleware"; 6 | 7 | export function init(app: express.Express) { 8 | app.use(requestLogger); 9 | app.use(logRequest); 10 | app.use(reportMetrics); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/src/internal/routes.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router, Express, RequestHandler } from "express"; 2 | 3 | import { 4 | CompletedPayment, 5 | paymentFailed as paymentFailedService, 6 | paymentComplete as paymentCompleteService, 7 | walletCreationFailure as walletCreationFailureService, 8 | walletCreationSuccess as walletCreationSuccessService, 9 | WalletCreationSuccessData, 10 | WalletCreationFailureData, FailedPayment, 11 | markWalletBurnt as markWalletBurntService, 12 | } from "./services"; 13 | import { getDefaultLogger as logger } from "../logging"; 14 | 15 | import { statusHandler } from "./middleware"; 16 | import { PUBLIC_KEYS } from "./jwt"; 17 | 18 | export type WebHookRequestPayload = { 19 | object: "wallet" | "payment"; 20 | state: "success" | "fail"; 21 | }; 22 | 23 | export type WalletRequest = WebHookRequestPayload & { 24 | object: "wallet"; 25 | action: "create"; 26 | } & ({ 27 | value: WalletCreationSuccessData; 28 | state: "success"; 29 | } | 30 | { 31 | value: WalletCreationFailureData; 32 | state: "fail"; 33 | }); 34 | 35 | export type PaymentRequest = WebHookRequestPayload & { 36 | object: "payment"; 37 | action: "send" | "receive"; 38 | } & ({ 39 | value: CompletedPayment; 40 | state: "success"; 41 | } | 42 | { 43 | value: FailedPayment; 44 | state: "fail"; 45 | }); 46 | 47 | export type WebHookRequest = Request & { 48 | body: WalletRequest | PaymentRequest; 49 | }; 50 | 51 | export const webhookHandler = async function(req: WebHookRequest, res: Response) { 52 | const body: WalletRequest | PaymentRequest = req.body; 53 | if (body.object === "payment") { 54 | if (body.action === "send" || body.action === "receive") { 55 | if (body.state === "success") { 56 | await paymentCompleteService(body.value); 57 | } else { 58 | await paymentFailedService(body.value); 59 | } 60 | } else { 61 | logger().error(`unknown action ("${ (body as any).action }" for payment webhook)`); 62 | res.status(400).send({ status: "error", error: "what?" }); 63 | } 64 | } else if (body.object === "wallet") { 65 | if (body.action === "create") { 66 | if (body.state === "success") { 67 | await walletCreationSuccessService(body.value); 68 | } else { 69 | await walletCreationFailureService(body.value); 70 | } 71 | } else { 72 | logger().error(`unknown action ("${ (body as any).action }" for wallet webhook)`); 73 | res.status(400).send({ status: "error", error: "what?" }); 74 | } 75 | } else { 76 | logger().error(`unknown object ("${ (body as any).object }" for webhooks)`); 77 | res.status(400).send({ status: "error", error: "what?" }); 78 | } 79 | 80 | res.status(200).send({ status: "ok" }); 81 | } as any as RequestHandler; 82 | 83 | export const getJwtKeys = async function(req: WebHookRequest, res: Response) { 84 | res.status(200).send(PUBLIC_KEYS); 85 | } as any as RequestHandler; 86 | 87 | type MarkWalletBurntRequest = Request & { 88 | params: { wallet_address: string } 89 | }; 90 | 91 | const burnWallet = async function(req: MarkWalletBurntRequest, res: Response) { 92 | const walletAddress = req.params.wallet_address; 93 | logger().info("wallet address for burning " + walletAddress); 94 | await markWalletBurntService(walletAddress); 95 | res.sendStatus(204); 96 | } as any as RequestHandler; 97 | 98 | export function createRoutes(app: Express, pathPrefix?: string) { 99 | const router = Router(); 100 | router 101 | .post("/webhook", webhookHandler) 102 | .get("/jwt-keys", getJwtKeys) 103 | .put("/wallets/:wallet_address/burnt", burnWallet); 104 | 105 | app.use("/v1/internal/", router); 106 | app.get("/status", statusHandler); 107 | } 108 | -------------------------------------------------------------------------------- /scripts/src/logging.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston"; 2 | import { FileTransportOptions, GenericTextTransportOptions, GenericTransportOptions } from "winston"; 3 | import * as httpContext from "express-http-context"; 4 | import { CLIENT_SDK_VERSION_HEADER } from "./middleware"; 5 | 6 | export interface LogTarget { 7 | name: string; 8 | type: "console" | "file"; 9 | level: "debug" | "info" | "warn" | "error"; 10 | format?: "string" | "json" | "pretty-json"; // default to "string" 11 | timestamp?: boolean | (() => string | boolean); 12 | } 13 | 14 | export interface ConsoleTarget extends LogTarget { 15 | type: "console"; 16 | name: "console"; 17 | } 18 | 19 | export interface FileTarget extends LogTarget { 20 | type: "file"; 21 | file: string; 22 | } 23 | 24 | type WinstonTransportOptions = GenericTransportOptions & GenericTextTransportOptions & { stringify?: boolean }; 25 | 26 | function createTarget(target: LogTarget): winston.TransportInstance { 27 | let cls: { new(options: WinstonTransportOptions): winston.TransportInstance }; 28 | const defaults: WinstonTransportOptions = { 29 | timestamp: true, 30 | }; 31 | const options: WinstonTransportOptions = { 32 | level: target.level, 33 | timestamp: target.timestamp, 34 | }; 35 | 36 | if (target.format === "json" || target.format === "pretty-json") { 37 | options.json = true; 38 | } 39 | 40 | if (target.format === "json") { 41 | (options.stringify as boolean) = true; 42 | } 43 | 44 | switch (target.type) { 45 | case "console": 46 | defaults.level = "debug"; 47 | cls = winston.transports.Console; 48 | break; 49 | 50 | case "file": 51 | defaults.level = "error"; 52 | (options as FileTransportOptions).filename = (target as FileTarget).file; 53 | cls = winston.transports.File; 54 | break; 55 | 56 | default: 57 | throw new Error("unsupported log target type: " + target.type); 58 | } 59 | 60 | return new cls(mergeOptions(defaults, options)); 61 | } 62 | 63 | type OptionsKey = keyof WinstonTransportOptions; 64 | 65 | function mergeOptions(defaults: WinstonTransportOptions, options: WinstonTransportOptions): WinstonTransportOptions { 66 | const result = Object.assign({}, defaults); 67 | 68 | (Object.keys(options) as OptionsKey[]) 69 | .filter(key => options[key] !== null && options[key] !== undefined) 70 | .forEach(key => result[key] = options[key]); 71 | 72 | return result; 73 | } 74 | 75 | function getLogContext() { 76 | const reqId = httpContext.get("reqId"); 77 | const userId = httpContext.get("userId"); 78 | const deviceId = httpContext.get("deviceId"); 79 | const appId = httpContext.get("appId"); 80 | const clientSdkVersion = httpContext.get(CLIENT_SDK_VERSION_HEADER); 81 | return { req_id: reqId, user_id: userId, device_id: deviceId, app_id: appId, client_sdk_version: clientSdkVersion }; 82 | } 83 | 84 | export interface BasicLogger { 85 | error(message: string, options?: object): void; 86 | 87 | warn(message: string, options?: object): void; 88 | 89 | verbose(message: string, options?: object): void; 90 | 91 | info(message: string, options?: object): void; 92 | 93 | debug(message: string, options?: object): void; 94 | } 95 | 96 | let defaultLogger: BasicLogger; 97 | 98 | export function initLogger(...targets: LogTarget[]): BasicLogger { 99 | if (defaultLogger) { 100 | return defaultLogger; 101 | } 102 | 103 | const winstonLogger = new winston.Logger({ 104 | transports: targets.map(target => createTarget(target)) 105 | }); 106 | 107 | defaultLogger = { 108 | error: (message: string, options?: object) => { 109 | winstonLogger.error(message, { ...options, ...getLogContext() }); 110 | }, 111 | warn: (message: string, options?: object) => { 112 | winstonLogger.warn(message, { ...options, ...getLogContext() }); 113 | }, 114 | verbose: (message: string, options?: object) => { 115 | winstonLogger.verbose(message, { ...options, ...getLogContext() }); 116 | }, 117 | info: (message: string, options?: object) => { 118 | winstonLogger.info(message, { ...options, ...getLogContext() }); 119 | }, 120 | debug: (message: string, options?: object) => { 121 | winstonLogger.debug(message, { ...options, ...getLogContext() }); 122 | } 123 | }; 124 | 125 | return defaultLogger; 126 | } 127 | 128 | export function getDefaultLogger(): BasicLogger { 129 | return defaultLogger; 130 | } 131 | -------------------------------------------------------------------------------- /scripts/src/metrics.ts: -------------------------------------------------------------------------------- 1 | import { StatsD } from "hot-shots"; 2 | import { getConfig } from "./config"; 3 | 4 | import { MarketplaceError } from "./errors"; 5 | import { Order, OrderFlowType, OrderOrigin } from "./models/orders"; 6 | import { BlockchainVersion } from "./models/offers"; 7 | 8 | // XXX can add general tags to the metrics (i.e. - public/ internal, machine name etc) 9 | const statsd = new StatsD(Object.assign({ prefix: "marketplace_" }, getConfig().statsd)); 10 | 11 | export function destruct() { 12 | return new Promise(resolve => statsd.close(() => resolve())); 13 | } 14 | 15 | export function userRegister(newUser: boolean, appId: string) { 16 | statsd.increment("user_register", 1, { new_user: newUser.toString(), app_id: appId }); 17 | } 18 | 19 | export function walletAddressUpdate(appId: string, newWallet: boolean) { 20 | statsd.increment("wallet_address_update_succeeded", 1, { app_id: appId, new_wallet: newWallet.toString() }); 21 | } 22 | 23 | // no use in /scripts or /tests 24 | export function userActivate(newUser: boolean) { 25 | statsd.increment("user_activate", 1, { new_user: "true" }); 26 | } 27 | 28 | export function maxWalletsExceeded(appId: string) { 29 | statsd.increment("max_wallets_exceeded", 1, { app_id: appId }); 30 | } 31 | 32 | export function timeRequest(time: number, method: string, path: string, appId: string) { 33 | statsd.timing("request", time, { method, path, app_id: appId }); 34 | } 35 | 36 | export function createOrder(orderOrigin: OrderOrigin, offerType: OrderFlowType, appId: string) { 37 | statsd.increment("create_order", 1, { 38 | order_type: orderOrigin, 39 | offer_type: offerType, 40 | app_id: appId, 41 | }); 42 | } 43 | 44 | export function submitOrder(orderOrigin: OrderOrigin, offerType: OrderFlowType, appId: string, blockchainVersion: BlockchainVersion) { 45 | statsd.increment("submit_order", 1, { offer_type: offerType, app_id: appId, kin_version: blockchainVersion }); 46 | } 47 | 48 | export function completeOrder(orderOrigin: OrderOrigin, offerType: OrderFlowType, prevStatus: string, time: number, appId: string, blockchainVersion: BlockchainVersion) { 49 | statsd.increment("complete_order", 1, { offer_type: offerType, app_id: appId, kin_version: blockchainVersion }); 50 | // time from last status 51 | statsd.timing("complete_order_time", time, { 52 | offer_type: offerType, 53 | prev_status: prevStatus, 54 | app_id: appId, 55 | kin_version: blockchainVersion 56 | }); 57 | } 58 | 59 | export function offersReturned(numOffers: number, appId: string) { 60 | statsd.histogram("offers_returned", numOffers, { app_id: appId }); 61 | } 62 | 63 | export function reportClientError(error: MarketplaceError, appId: string) { 64 | statsd.increment("client_error", 1, 65 | { status: error.status.toString(), title: error.title, code: error.code.toString(), app_id: appId }); 66 | } 67 | 68 | export function reportServerError(method: string, path: string, appId: string) { 69 | statsd.increment("server_error", 1, { method, path, app_id: appId }); 70 | } 71 | 72 | export function reportProcessAbort(reason: string = "", appId: string = "") { 73 | statsd.increment("process_abort", 1, { system: "exit", reason, app_id: appId }); 74 | } 75 | 76 | export function bulkUserCreationRequest(appId: string, numberOfUsersRequested: number) { 77 | statsd.increment("bulk_user_creation_request", numberOfUsersRequested, { app_id: appId }); 78 | } 79 | 80 | export function bulkUserCreated(appId: string) { 81 | statsd.increment("bulk_user_creation", 1, { app_id: appId }); 82 | } 83 | 84 | type MigrationReason = "app_on_kin3" | "wallet_on_kin3" | "gradual_migration"; 85 | 86 | export function migrationRateLimitExceeded(appId: string) { 87 | statsd.increment("migration_limit_exceeded", 1, { app_id: appId }); 88 | } 89 | 90 | export function migrationTrigger(appId: string, reason: MigrationReason) { 91 | statsd.increment("migration_trigger", 1, { app_id: appId, reason }); 92 | } 93 | 94 | export function migrationInfo(appId: string, reason: MigrationReason) { 95 | statsd.increment("migration_info", 1, { app_id: appId, reason }); 96 | } 97 | 98 | export function skipMigration(appId: string) { 99 | statsd.increment("skip_migration", 1, { app_id: appId }); 100 | } 101 | 102 | export function orderFailed(order: Order) { 103 | function safeString(str: string): string { 104 | return str.replace(/\W/g, " "); 105 | } 106 | 107 | const unknownError = { error: "unknown_error", message: "unknown error", code: -1 }; 108 | const unknownUser = { id: "no_id", appId: "no_id", appUserId: "no_id", walletAddress: "no_wallet" }; 109 | const error = order.error || unknownError; 110 | const title = safeString(error.message); 111 | const type = order.isP2P() ? "p2p" : order.contexts[0].type; 112 | const appId = order.contexts[0].user.appId; 113 | let message = `## Order <${ order.id }> from ${ appId } failed: 114 | ID: <${ order.id }> | Type: ${ type } | Origin: ${ order.origin } 115 | Error: ${ title } | Code: ${ error.code } 116 | CreatedDate: ${ order.createdDate.toISOString() } | LastDate: ${ (order.currentStatusDate || order.createdDate).toISOString() }`; 117 | 118 | message += ` sender wallet: ${ order.blockchainData.sender_address }, recipient wallet: ${ order.blockchainData.recipient_address }`; 119 | order.forEachContext(context => { 120 | message += `UserId: ${ context.user.id } | UserAppId: ${ context.user.appUserId }`; 121 | }); 122 | statsd.event(title, message, 123 | { alert_type: "warning" }, 124 | { 125 | order_type: type, 126 | app_id: appId, 127 | order_id: order.id, 128 | order_origin: order.origin, 129 | type: "failed_order" 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /scripts/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as cluster from "cluster"; 3 | import * as httpContext from "express-http-context"; 4 | import * as moment from "moment"; 5 | import { performance } from "perf_hooks"; 6 | import { Request, Response } from "express-serve-static-core"; 7 | 8 | import * as metrics from "./metrics"; 9 | import { getConfig } from "./config"; 10 | import { generateId } from "./utils/utils"; 11 | import { MarketplaceError } from "./errors"; 12 | import { abort as restartServer } from "./server"; 13 | import { isAuthenticatedRequest } from "./public/auth"; 14 | import { getDefaultLogger as logger } from "./logging"; 15 | 16 | const START_TIME = (new Date()).toISOString(); 17 | 18 | const RESTART_ERROR_COUNT = 5; // Amount of errors to occur in time frame to trigger restart 19 | const RESTART_MAX_TIMEFRAME = 20; // In seconds 20 | 21 | export const CLIENT_SDK_VERSION_HEADER = "x-sdk-version"; 22 | 23 | let serverErrorTimeStamps: number[] = []; 24 | 25 | function getAppIdFromRequest(req: Request): string { 26 | return isAuthenticatedRequest(req) ? req.context.user.appId : ""; 27 | } 28 | 29 | /** 30 | * augments the request object with a request-id and a logger. 31 | * the logger should be then used when logging inside request handlers, which will then add some more info per logger 32 | */ 33 | export const requestLogger = function(req: express.Request, res: express.Response, next: express.NextFunction) { 34 | httpContext.set("reqId", req.header("x-request-id") || generateId()); 35 | next(); 36 | } as express.RequestHandler; 37 | 38 | function getWorkerId() { 39 | return cluster.worker ? cluster.worker.id : undefined; 40 | } 41 | 42 | export const logRequest = function(req: express.Request, res: express.Response, next: express.NextFunction) { 43 | const t = performance.now(); 44 | const data = Object.assign({}, req.headers); 45 | 46 | if (req.query && Object.keys(req.query).length > 0) { 47 | data.querystring = req.query; 48 | } 49 | 50 | res.on("finish", () => { 51 | logger().debug(`worker ${ getWorkerId() }: finished handling request: ${ req.method } ${ req.path }`, { status: res.statusCode, data, time: performance.now() - t }); 52 | }); 53 | 54 | next(); 55 | } as express.RequestHandler; 56 | 57 | export const setHttpContextFromRequest = function(req: express.Request, res: express.Response, next: express.NextFunction) { 58 | httpContext.set(CLIENT_SDK_VERSION_HEADER, req.header(CLIENT_SDK_VERSION_HEADER)); 59 | next(); 60 | } as express.RequestHandler; 61 | 62 | export const reportMetrics = function(req: express.Request, res: express.Response, next: express.NextFunction) { 63 | const t = performance.now(); 64 | 65 | res.on("finish", () => { 66 | const path = req.route ? req.route.path : "unknown"; 67 | metrics.timeRequest(performance.now() - t, req.method, path, getAppIdFromRequest(req)); 68 | }); 69 | 70 | next(); 71 | } as express.RequestHandler; 72 | 73 | export const notFoundHandler = function(req: Request, res: Response) { 74 | res.status(404).send({ code: 404, error: "Not found", message: "Not found" }); 75 | } as express.RequestHandler; 76 | 77 | /** 78 | * The "next" arg is needed even though it's not used, otherwise express won't understand that it's an error handler 79 | */ 80 | export function generalErrorHandler(err: any, req: Request, res: Response, next: express.NextFunction) { 81 | if (err instanceof MarketplaceError) { 82 | clientErrorHandler(err, req as express.Request, res); 83 | } else if (err.status && err.status < 500) { 84 | const mpErr = new MarketplaceError(err.status, 0, err.type || err.message, err.message); 85 | clientErrorHandler(mpErr, req as express.Request, res); 86 | } else { 87 | serverErrorHandler(err, req as express.Request, res); 88 | } 89 | } 90 | 91 | function clientErrorHandler(error: MarketplaceError, req: express.Request, res: express.Response) { 92 | logger().error(`client error (4xx)`, { error: error.toJson() }); 93 | metrics.reportClientError(error, getAppIdFromRequest(req)); 94 | 95 | // set headers from the error if any 96 | Object.keys(error.headers).forEach(key => res.setHeader(key, error.headers[key])); 97 | res.status(error.status).send(error.toJson()); 98 | } 99 | 100 | function serverErrorHandler(error: any, req: express.Request, res: express.Response) { 101 | metrics.reportServerError(req.method, req.url, getAppIdFromRequest(req)); 102 | 103 | const timestamp = moment().unix(); 104 | serverErrorTimeStamps.push(timestamp); 105 | serverErrorTimeStamps = serverErrorTimeStamps.slice(-RESTART_ERROR_COUNT); 106 | 107 | let message = `Error 108 | method: ${ req.method } 109 | path: ${ req.url } 110 | payload: ${ JSON.stringify(req.body) } 111 | `; 112 | 113 | if (error instanceof Error) { 114 | message += `message: ${ error.message } 115 | stack: ${ error.stack }`; 116 | } else { 117 | message += `message: ${ error.toString() }`; 118 | } 119 | 120 | if (serverErrorTimeStamps.length === RESTART_ERROR_COUNT) { 121 | if (timestamp - serverErrorTimeStamps[0] < RESTART_MAX_TIMEFRAME) { 122 | restartServer("too many internal errors", getAppIdFromRequest(req)); 123 | } 124 | } 125 | 126 | logger().error(`server error (5xx)`, { error: message }); 127 | 128 | res.status(500).send({ code: 500, error: error.message || "Server error", message: error.message }); 129 | } 130 | 131 | export const statusHandler = async function(req: express.Request, res: express.Response) { 132 | logger().info("status called", { context: isAuthenticatedRequest(req) ? req.context : null }); 133 | res.status(200).send( 134 | { 135 | status: "ok", 136 | app_name: getConfig().app_name, 137 | start_time: START_TIME, 138 | build: { 139 | commit: getConfig().commit, 140 | timestamp: getConfig().timestamp, 141 | } 142 | }); 143 | } as any as express.RequestHandler; 144 | -------------------------------------------------------------------------------- /scripts/src/models/applications.ts: -------------------------------------------------------------------------------- 1 | import { generateId, IdPrefix, isNothing } from "../utils/utils"; 2 | import { localCache } from "../utils/cache"; 3 | import { BaseEntity, Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; 4 | import { CreationDateModel, initializer as Initializer, register as Register } from "./index"; 5 | import { Cap, Offer, OfferType, BlockchainVersion } from "./offers"; 6 | import { Order } from "./orders"; 7 | 8 | import { LimitConfig } from "../config"; 9 | import moment = require("moment"); 10 | import { getConfig } from "../public/config"; 11 | 12 | const config = getConfig(); 13 | 14 | export type StringMap = { [key: string]: string; }; // key => value pairs 15 | export type SignInType = "jwt" | "whitelist"; 16 | export type ApplicationConfig = { 17 | max_user_wallets: number | null; 18 | daily_earn_offers: number; 19 | sign_in_types: SignInType[]; 20 | limits: LimitConfig; 21 | blockchain_version: BlockchainVersion; 22 | bulk_user_creation_allowed?: number; 23 | gradual_migration_date?: string | null; // ISO date format with TZ, i.e. 2010-12-21T10:22:33Z 24 | gradual_migration_jwt_users_limit?: number | null; // ISO date format with TZ, i.e. 2010-12-21T10:22:33Z 25 | }; 26 | 27 | @Entity({ name: "applications" }) 28 | @Initializer("apiKey", () => generateId(IdPrefix.App)) 29 | @Register 30 | export class Application extends CreationDateModel { 31 | // XXX testing purposes 32 | public static SAMPLE_API_KEY = "A28hNcn2wp77QyaM8kB2C"; 33 | 34 | public static async all(): Promise> { 35 | const cacheKey = "apps"; 36 | let apps = localCache.get(cacheKey); 37 | 38 | // if (!apps) { 39 | if (true) { 40 | apps = await Application.find(); 41 | // localCache.set(cacheKey, apps, moment.duration(config.cache_ttl.application, "seconds")); 42 | } 43 | 44 | return new Map(apps.map(app => [app.id, app]) as Array<[string, Application]>); 45 | } 46 | 47 | public static async get(id: string): Promise { 48 | return (await this.all()).get(id); 49 | } 50 | 51 | @Column({ name: "name" }) 52 | public name!: string; 53 | 54 | @Column({ name: "api_key" }) 55 | public apiKey!: string; 56 | 57 | @Column("simple-json", { name: "jwt_public_keys" }) 58 | public jwtPublicKeys!: StringMap; 59 | 60 | @Column("simple-json", { name: "wallet_addresses" }) 61 | public walletAddresses!: { recipient: string; sender: string }; 62 | 63 | @Column("simple-json", { name: "config" }) 64 | public config!: ApplicationConfig; 65 | 66 | @OneToMany(type => AppOffer, appOffer => appOffer.app) 67 | public appOffers!: AppOffer[]; 68 | 69 | public supportsSignInType(type: SignInType) { 70 | return this.config.sign_in_types.includes(type); 71 | } 72 | 73 | public allowsNewWallet(currentNumberOfWallets: number) { 74 | return this.config.max_user_wallets === null || currentNumberOfWallets < this.config.max_user_wallets; 75 | } 76 | 77 | // return true if should apply gradual migration from given date 78 | // if no date given, use now 79 | public shouldApplyGradualMigration(date?: Date): boolean { 80 | if (isNothing(this.config.gradual_migration_date)) { 81 | return false; 82 | } 83 | const compareTo = moment(date || new Date()); 84 | // given date is after the migration date - should migrate 85 | return compareTo > moment(this.config.gradual_migration_date); 86 | } 87 | } 88 | 89 | @Entity({ name: "application_offers" }) 90 | @Register 91 | export class AppOffer extends BaseEntity { 92 | public static async getAppOffers(appId: string, type: OfferType): Promise { 93 | const cacheKey = `appOffers:${ appId }:${ type }`; 94 | let appOffers = localCache.get(cacheKey); 95 | 96 | if (!appOffers) { 97 | appOffers = await AppOffer.createQueryBuilder("app_offer") 98 | .leftJoinAndSelect("app_offer.offer", "offer") 99 | .leftJoinAndSelect("app_offer.app", "app") 100 | .where("app_offer.appId = :appId", { appId }) 101 | .andWhere("offer.type = :type", { type }) 102 | .orderBy("app_offer.sortIndex", "ASC") 103 | .getMany(); 104 | localCache.set(cacheKey, appOffers); 105 | } 106 | 107 | return appOffers; 108 | } 109 | 110 | public static async generate(appId: string, offerId: string, cap: Cap, walletAddress: string): Promise { 111 | const lastAppOffer = await AppOffer.findOne({ where: { appId }, order: { sortIndex: "DESC" } }); 112 | const orderingBufferStep = 10; 113 | const lastAppOfferOrdering = (lastAppOffer && lastAppOffer.sortIndex) ? Number(lastAppOffer.sortIndex) : 0; 114 | const newAppOfferOrdering = lastAppOfferOrdering + 1 * orderingBufferStep; 115 | return await AppOffer.create({ appId, offerId, cap, walletAddress, sortIndex: newAppOfferOrdering }); 116 | } 117 | 118 | @PrimaryColumn({ name: "app_id" }) 119 | public appId!: string; 120 | 121 | @PrimaryColumn({ name: "offer_id" }) 122 | public offerId!: string; 123 | 124 | @Column("simple-json") 125 | public cap!: Cap; 126 | 127 | @Column({ name: "wallet_address" }) 128 | public walletAddress!: string; 129 | 130 | @Column({ name: "sort_index", type: "int" }) 131 | public sortIndex!: number; 132 | 133 | @ManyToOne(type => Offer, { eager: true }) 134 | @JoinColumn({ name: "offer_id" }) 135 | public readonly offer!: Offer; 136 | 137 | @ManyToOne(type => Application, app => app.appOffers) 138 | @JoinColumn({ name: "app_id" }) 139 | public readonly app!: Application; 140 | 141 | public async didExceedCap(userId: string): Promise { 142 | // const total = (await Order.countAllByOffer(this.appId, { offerId: this.offerId })).get(this.offerId) || 0; 143 | // if (total >= this.cap.total) { 144 | // return true; 145 | // } 146 | const forUser = (await Order.countAllByOffer(this.appId, { 147 | offerId: this.offerId, 148 | userId 149 | })).get(this.offerId) || 0; 150 | return forUser >= this.cap.per_user; 151 | } 152 | } 153 | 154 | @Entity({ name: "app_whitelists" }) 155 | @Index(["appId", "appUserId"], { unique: true }) 156 | @Register 157 | export class AppWhitelists extends CreationDateModel { 158 | @Column({ name: "app_id" }) 159 | public appId!: string; 160 | 161 | @Column({ name: "app_user_id" }) 162 | public appUserId!: string; 163 | } 164 | -------------------------------------------------------------------------------- /scripts/src/models/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ObjectType } from "typeorm/common/ObjectType"; 3 | import { DeepPartial } from "typeorm/common/DeepPartial"; 4 | import { BaseEntity, Column, Connection, ConnectionOptions, createConnection, PrimaryColumn } from "typeorm"; 5 | 6 | import { getConfig } from "../config"; 7 | import { generateId, IdPrefix } from "../utils/utils"; 8 | import { path } from "../utils/path"; 9 | 10 | const entities: ModelConstructor[] = []; 11 | 12 | let dbConfig: ConnectionOptions; 13 | let connection: Connection | null; 14 | let initPromise: Promise | null; 15 | 16 | export type ModelConstructor = ({ new(): Model }) | Function; 17 | export type ModelMemberInitializer = () => any; 18 | 19 | export abstract class Model extends BaseEntity { 20 | public static new(this: ObjectType, data?: DeepPartial): T { 21 | const instance = (this as typeof BaseEntity).create(data!) as T; 22 | 23 | for (const [name, initializer] of (this as typeof Model).initializers.entries()) { 24 | if (!instance[name as keyof Model]) { 25 | instance[name as keyof Model] = initializer(); 26 | } 27 | } 28 | 29 | return instance; 30 | } 31 | 32 | protected static initializers = new Map([["id", () => generateId(IdPrefix.None)]]); 33 | 34 | protected static copyInitializers(add?: { [name: string]: ModelMemberInitializer }): Map { 35 | const map = new Map(this.initializers); 36 | if (add) { 37 | Object.keys(add).forEach(name => map.set(name, add[name])); 38 | } 39 | 40 | return map; 41 | } 42 | 43 | @PrimaryColumn() 44 | public id!: string; 45 | 46 | public isNew: boolean; 47 | 48 | public constructor(isNew = false) { 49 | super(); 50 | 51 | this.isNew = isNew; 52 | } 53 | } 54 | 55 | export abstract class CreationDateModel extends Model { 56 | protected static initializers = Model.copyInitializers({ createdDate: () => new Date() }); 57 | 58 | @Column({ name: "created_date" }) 59 | public createdDate!: Date; 60 | } 61 | 62 | export function register(ctor: ModelConstructor) { 63 | entities.push(ctor); 64 | } 65 | 66 | export function initializer(propName: string, fn: ModelMemberInitializer) { 67 | return initializers({ [propName]: fn }); 68 | } 69 | 70 | export function initializers(props: { [name: string]: ModelMemberInitializer }) { 71 | // ctor is also { initializers: Map }, but it's protected 72 | return (ctor: ModelConstructor) => { 73 | const parent = Object.getPrototypeOf(ctor.prototype).constructor; 74 | if (parent.initializers === (ctor as any).initializers) { 75 | (ctor as any).initializers = new Map(parent.initializers); 76 | } 77 | 78 | Object.keys(props).forEach(name => (ctor as any).initializers.set(name, props[name])); 79 | }; 80 | } 81 | 82 | export function init(createDb?: boolean): Promise { 83 | if (initPromise) { 84 | return initPromise; 85 | } 86 | 87 | dbConfig = Object.assign({}, getConfig().db, { synchronize: !!createDb }); 88 | (dbConfig as any).entities = entities; 89 | 90 | initPromise = createConnection(dbConfig) 91 | .then(conn => { 92 | connection = conn; 93 | return createOnConnectedString(connection.options); 94 | }) 95 | .catch(error => { 96 | throw error; 97 | }); 98 | 99 | return initPromise; 100 | } 101 | 102 | export async function close(): Promise { 103 | await connection!.close(); 104 | initPromise = null; 105 | connection = null; 106 | } 107 | 108 | export type ModelFilters = Partial<{ [K in keyof T]: T[K] }>; 109 | 110 | function createOnConnectedString(options: ConnectionOptions): string { 111 | let msg = `connected to ${ options.type } server`; 112 | 113 | switch (options.type) { 114 | case "sqlite": 115 | msg += `, db file: '${ options.database }'`; 116 | } 117 | 118 | return msg; 119 | } 120 | -------------------------------------------------------------------------------- /scripts/src/models/offers.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; 2 | 3 | import { CreationDateModel, initializer as Initializer, Model, register as Register } from "./index"; 4 | import { generateId, IdPrefix } from "../utils/utils"; 5 | import { OrderMeta } from "./orders"; 6 | import { OfferTranslation } from "./translations"; 7 | import { AppOffer } from "./applications"; 8 | import { localCache } from "../utils/cache"; 9 | 10 | export type BlockchainData = { 11 | transaction_id?: string; 12 | sender_address?: string; 13 | recipient_address?: string; 14 | memo?: string; 15 | }; 16 | 17 | export type BlockchainVersion = "2" | "3"; 18 | export const BlockchainVersionValues = ["2", "3"] as BlockchainVersion[]; 19 | 20 | export type OfferMeta = { 21 | title: string; 22 | image: string; 23 | description: string; 24 | order_meta: OrderMeta; 25 | }; 26 | 27 | export type Cap = { 28 | total: number; 29 | per_user: number; 30 | }; 31 | 32 | export type OfferType = "spend" | "earn"; 33 | export type ContentType = "quiz" | "poll" | "tutorial" | "coupon"; 34 | 35 | @Entity({ name: "offer_owners" }) 36 | @Register 37 | export class OfferOwner extends Model { 38 | @Column() 39 | public name!: string; 40 | 41 | public get offers(): Promise { 42 | return Offer.find({ ownerId: this.id }); 43 | } 44 | } 45 | 46 | @Entity({ name: "offers" }) 47 | @Register 48 | @Initializer("id", () => generateId(IdPrefix.Offer)) 49 | export class Offer extends CreationDateModel { 50 | public static async get(id: string): Promise { 51 | const cacheKey = `offer:${ id }`; 52 | let offer = localCache.get(cacheKey) || undefined; 53 | 54 | if (!offer) { 55 | offer = await Offer.findOneById(id); 56 | 57 | if (offer) { 58 | localCache.set(cacheKey, offer); 59 | } 60 | } 61 | 62 | return offer; 63 | } 64 | 65 | @Column({ name: "name", unique: true }) 66 | public name!: string; 67 | 68 | @Column() 69 | public amount!: number; 70 | 71 | @Column("simple-json") 72 | public meta!: OfferMeta; 73 | 74 | @Column() 75 | public type!: OfferType; 76 | 77 | @Column({ name: "owner_id" }) 78 | public ownerId!: string; 79 | 80 | @OneToMany(type => OfferTranslation, translation => translation.offer, { 81 | cascadeInsert: true, 82 | cascadeUpdate: true 83 | }) 84 | public translations!: OfferTranslation[]; 85 | 86 | @OneToMany(type => AppOffer, appOffer => appOffer.offer) 87 | public appOffers!: AppOffer[]; 88 | } 89 | 90 | @Entity({ name: "offer_contents" }) 91 | @Register 92 | export class OfferContent extends Model { 93 | public static async get(offerId: string): Promise { 94 | return (await this.all()).get(offerId); 95 | } 96 | 97 | public static async all(): Promise> { 98 | const cacheKey = "offer_contents"; 99 | let contents = localCache.get(cacheKey); 100 | 101 | if (!contents) { 102 | contents = await OfferContent.find(); 103 | localCache.set(cacheKey, contents); 104 | } 105 | 106 | return new Map(contents.map(content => [content.offerId, content]) as Array<[string, OfferContent]>); 107 | } 108 | 109 | @PrimaryColumn({ name: "offer_id" }) 110 | public offerId!: string; 111 | 112 | @Column("simple-json") 113 | public content!: string; // should be object 114 | 115 | @Column({ name: "content_type" }) 116 | public contentType!: ContentType; 117 | } 118 | 119 | export type AssetValue = { coupon_code: string }; 120 | export type JWTValue = { jwt: string }; 121 | export type OrderValue = (JWTValue & { type: "payment_confirmation" }) | (AssetValue & { type: "coupon" }); 122 | 123 | @Entity({ name: "assets" }) 124 | @Register 125 | export class Asset extends CreationDateModel { 126 | @Column() 127 | public type!: "coupon"; 128 | 129 | @Column({ name: "offer_id" }) 130 | public offerId!: string; 131 | 132 | @Column({ name: "owner_id", nullable: true }) 133 | public ownerId?: string; // User.id 134 | 135 | @Column("simple-json") 136 | public value!: AssetValue; 137 | 138 | public asOrderValue(): OrderValue { 139 | return Object.assign({ type: this.type }, this.value); 140 | } 141 | } 142 | 143 | @Entity({ name: "poll_answers" }) 144 | @Register 145 | export class PollAnswer extends CreationDateModel { 146 | @Column({ name: "user_id" }) 147 | public userId!: string; 148 | 149 | @Column({ name: "offer_id" }) 150 | public offerId!: string; 151 | 152 | @Column({ name: "order_id" }) 153 | public orderId!: string; 154 | 155 | @Column() 156 | public content!: string; 157 | } 158 | 159 | @Entity({ name: "sdk_version_rules" }) 160 | @Register 161 | export class SdkVersionRule extends CreationDateModel { 162 | 163 | @PrimaryColumn({ name: "comparator" }) 164 | public comparator!: string; 165 | 166 | @PrimaryColumn({ name: "asset_type" }) 167 | public assetType!: string; 168 | 169 | @Column({ name: "data", type: "json" }) 170 | public data!: string; 171 | } 172 | -------------------------------------------------------------------------------- /scripts/src/models/translations.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, ObjectType, OneToMany, PrimaryColumn } from "typeorm"; 2 | import { register as Register } from "./index"; 3 | import { DeepPartial } from "typeorm/common/DeepPartial"; 4 | import { Offer } from "./offers"; 5 | 6 | export type GetTranslationsCriteria = { 7 | languages?: string[]; 8 | offerId?: string; 9 | paths?: string[]; 10 | }; 11 | 12 | const CACHE = new Map(); 13 | 14 | @Entity({ name: "offer_content_translations" }) 15 | @Register 16 | export class OfferTranslation extends BaseEntity { 17 | public static new(this: ObjectType, data?: DeepPartial): OfferTranslation { 18 | return (this as typeof BaseEntity).create(data!) as OfferTranslation; 19 | } 20 | 21 | public static async getTranslations(criteria: GetTranslationsCriteria = {}): Promise { 22 | // todo add cache 23 | const cacheKey = `${criteria.languages}:${criteria.offerId}:${criteria.paths}`; 24 | const languages = criteria.languages; 25 | const offerId = criteria.offerId; 26 | const paths = criteria.paths; 27 | 28 | if (CACHE.has(cacheKey)) { 29 | return CACHE.get(cacheKey) as OfferTranslation[]; 30 | } 31 | 32 | const query = OfferTranslation.createQueryBuilder("translations"); 33 | if (languages && languages.length > 0) { 34 | query.where("translations.language IN (:languages)", { languages }); 35 | } 36 | if (offerId) { 37 | query.andWhere("translations.offerId = :offerId", { offerId }); 38 | } 39 | if (paths && paths.length > 0) { 40 | query.andWhere("translations.path IN (:paths)", { paths }); 41 | } 42 | const results = await query.getMany(); 43 | CACHE.set(cacheKey, results); 44 | return results; 45 | } 46 | 47 | public static async getSupportedLanguages(criteria: GetTranslationsCriteria = {}): Promise<[string[], OfferTranslation[]]> { 48 | const translations = await OfferTranslation.getTranslations(criteria); 49 | const languages = new Set(translations.map(translation => translation.language)); 50 | return [Array.from(languages), translations]; 51 | } 52 | 53 | @ManyToOne(type => Offer, offer => offer.id) 54 | @JoinColumn({ name: "offer_id" }) 55 | public readonly offer!: Offer; 56 | 57 | @PrimaryColumn() 58 | public readonly context!: string; 59 | 60 | @PrimaryColumn() 61 | public readonly path!: string; 62 | 63 | @PrimaryColumn() 64 | public readonly language!: string; 65 | 66 | @Column() 67 | public readonly translation!: string; 68 | 69 | @PrimaryColumn({ name: "offer_id" }) 70 | public readonly offerId?: string; 71 | } 72 | -------------------------------------------------------------------------------- /scripts/src/node-console.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "net"; 2 | 3 | export function start(socket?: Socket, intro?: string) { 4 | const repl = require("repl"); 5 | const util = require("util"); 6 | 7 | function log(value?: any, ...args: any[]) { 8 | let print = console.log; 9 | if (socket) { 10 | // If output is not to stdout but a socket write to the socket 11 | print = (...params: any[]) => { 12 | socket.write(util.format(...params) + "\n"); 13 | }; 14 | } 15 | if (value && value.then && typeof value.then === "function") { 16 | // if FIRST argument is a promise (thenable object) then print the resolved value 17 | // we can iterate over args and handle if any are promises 18 | value.then((value: any) => log(value, ...args)); 19 | return; 20 | } 21 | if (typeof value === "function") { 22 | log(value.toString(), ...args); 23 | return; 24 | } 25 | if (Array.isArray(value) || typeof value === "object") { 26 | print(util.inspect(value, { showHidden: false, depth: 4, colors: true }), ...args); 27 | return; 28 | } 29 | print(value, ...args); 30 | } 31 | 32 | // const babel = require("@babel/core"); // We can have babel transpile in realtime to get ESNext syntax support (see reference below) 33 | 34 | const BASE_MODULE_PATH = "./"; 35 | 36 | // ***** Terminal Colors and Formats ***** // 37 | const TERMINAL_STYLE = { 38 | reset: "\x1b[0m", 39 | bright: "\x1b[1m", 40 | dim: "\x1b[2m", 41 | underscore: "\x1b[4m", 42 | blink: "\x1b[5m", 43 | reverse: "\x1b[7m", 44 | hidden: "\x1b[8m", 45 | 46 | fgblack: "\x1b[30m", 47 | fgred: "\x1b[31m", 48 | fggreen: "\x1b[32m", 49 | fgyellow: "\x1b[33m", 50 | fgblue: "\x1b[34m", 51 | fgmagenta: "\x1b[35m", 52 | fgcyan: "\x1b[36m", 53 | fgwhite: "\x1b[37m", 54 | 55 | bgblack: "\x1b[40m", 56 | bgred: "\x1b[41m", 57 | bggreen: "\x1b[42m", 58 | bgyellow: "\x1b[43m", 59 | bgblue: "\x1b[44m", 60 | bgmagenta: "\x1b[45m", 61 | bgcyan: "\x1b[46m", 62 | bgwhite: "\x1b[47m", 63 | }; 64 | // const colors = { RED: "31", GREEN: "32", YELLOW: "33", BLUE: "34", MAGENTA: "35" }; 65 | // const colorize = (color, s) => `\x1b[${color}m${s}\x1b[0m`; 66 | log("%s%sTo use the DB, initialize one of the server apps, shortcuts available: .admin, .public, .internal%s", TERMINAL_STYLE.bggreen, TERMINAL_STYLE.fgblack, TERMINAL_STYLE.reset); 67 | 68 | /*** Keeping for reference (was a little processing intensive but with a little work can be a nice feature) ***/ 69 | /*const BABEL_OPTIONS = { 70 | babelrc: false, 71 | "presets": ["es2015", { "modules": true }] 72 | }; 73 | 74 | const evaluatorFunc = function(cmd, context, filename, callback) { 75 | babel.transform(cmd, BABEL_OPTIONS, function(err, result) { 76 | log(result.code); 77 | eval(result.code); 78 | callback(null, result.code); 79 | replServer.displayPrompt(true); 80 | }); 81 | };*/ 82 | 83 | // init 84 | const replServer = repl.start({ 85 | prompt: "marketplace > ", 86 | useColors: true, 87 | useGlobal: true, 88 | terminal: true, 89 | // ignoreUndefined: true, 90 | replMode: repl.REPL_MODE_SLOPPY, 91 | ...(socket ? { input: socket, output: socket } : {}) 92 | }); 93 | 94 | if (socket) { 95 | replServer.on("exit", function() { 96 | log("REPL server exit"); 97 | socket.end(); 98 | }); 99 | socket.on("close", function close() { 100 | console.log("REPL server socket disconnected."); // we don't have a socket to use log with 101 | socket.removeListener("close", close); 102 | }); 103 | } 104 | 105 | const context = replServer.context; 106 | const r = context.r = function r(module: string, reload?: boolean) { 107 | if (reload && require.cache[require.resolve(BASE_MODULE_PATH + module)]) { 108 | delete require.cache[require.resolve(BASE_MODULE_PATH + module)]; 109 | } 110 | return require(BASE_MODULE_PATH + module); 111 | }; 112 | 113 | context.log = log; 114 | /* Require app models */ 115 | context._offers = r("models/offers"); 116 | context.Offer = context._offers.Offer; 117 | context.OfferContent = context._offers.OfferContent; 118 | context.OfferTranslation = (r("models/translations")).OfferTranslation; 119 | context._orders = r("models/orders"); 120 | context.Order = context._orders.Order; 121 | context.OrderContext = context._orders.OrderContext; 122 | context.User = (r("models/users")).User; 123 | context.AuthToken = (r("models/users")).AuthToken; 124 | context.Application = (r("models/applications")).Application; 125 | context.AppOffer = r("models/applications").AppOffer; 126 | context.utils = r("utils/utils"); 127 | 128 | /* Add shortcut REPL commands for loading the marketplace apps */ 129 | replServer.defineCommand("admin", { 130 | help: "Init the Admin App", 131 | action: r.bind(null, "admin/app") 132 | }); 133 | replServer.defineCommand("public", { 134 | help: "Init the Public App", 135 | action: r.bind(null, "public/app") 136 | }); 137 | replServer.defineCommand("internal", { 138 | help: "Init the Internal App", 139 | action: r.bind(null, "internal/app") 140 | }); 141 | } 142 | 143 | if (require.main === module) { 144 | start(); 145 | } 146 | -------------------------------------------------------------------------------- /scripts/src/public/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import "express-async-errors"; // handle async/await errors in middleware 3 | import * as typeis from "type-is"; 4 | import { initLogger } from "../logging"; 5 | import { getConfig } from "./config"; 6 | import { init as initModels } from "../models/index"; 7 | import { init as initRemoteConfig } from "./routes/config"; 8 | import { createPublicFacing, createRoutes, createV1Routes } from "./routes/index"; 9 | import { init as initMigration } from "../utils/migration"; 10 | import { generalErrorHandler, init as initCustomMiddleware, notFoundHandler } from "./middleware"; 11 | 12 | const config = getConfig(); 13 | const logger = initLogger(...config.loggers!); 14 | 15 | function createApp() { 16 | const app = express(); 17 | app.set("port", config.port); 18 | 19 | const isBigJsonRequest = (req: express.Request) => typeis(req, "application/bigjson"); 20 | 21 | const bodyParser = require("body-parser"); 22 | app.use(bodyParser.json({ limit: 52428800, type: isBigJsonRequest } )); // 50Mb limit for requests with content type: "application/bigjson" 23 | app.use(bodyParser.json({})); 24 | app.use(bodyParser.urlencoded({ extended: false })); 25 | 26 | const cookieParser = require("cookie-parser"); 27 | app.use(cookieParser()); 28 | 29 | initCustomMiddleware(app); 30 | 31 | return app; 32 | } 33 | 34 | export const app: express.Express = createApp(); 35 | 36 | // routes 37 | createRoutes(app, "/v2"); 38 | createV1Routes(app, "/v1"); 39 | createPublicFacing(app, "/partners/v1"); 40 | 41 | // catch 404 42 | app.use(notFoundHandler); 43 | // catch errors 44 | app.use(generalErrorHandler); 45 | 46 | export async function init() { 47 | // initializing db and models 48 | const msg = await initModels(); 49 | logger.debug(msg); 50 | await initRemoteConfig(); 51 | await initMigration(); 52 | } 53 | -------------------------------------------------------------------------------- /scripts/src/public/config.ts: -------------------------------------------------------------------------------- 1 | import { Config as BaseConfig, init as baseInit, getConfig as baseGetConfig, LimitConfig } from "../config"; 2 | 3 | export interface Config extends BaseConfig { 4 | assets_base: string; 5 | environment_name: string; 6 | ecosystem_service: string; 7 | internal_service: string; 8 | limits: LimitConfig; 9 | } 10 | 11 | export function getConfig(): Config { 12 | return baseGetConfig(); 13 | } 14 | 15 | function init(): void { 16 | baseInit("config/public.default.json"); 17 | } 18 | 19 | init(); 20 | -------------------------------------------------------------------------------- /scripts/src/public/index.ts: -------------------------------------------------------------------------------- 1 | // for cluster master 2 | import * as cluster from "cluster"; 3 | import * as os from "os"; 4 | 5 | // for cluster workers 6 | import * as http from "http"; 7 | import { getConfig } from "./config"; 8 | 9 | const config = getConfig(); 10 | import { app, init } from "./app"; 11 | 12 | import { onError, onListening } from "../server"; 13 | 14 | if (cluster.isMaster) { 15 | // Count the machine's CPUs 16 | const processNum = config.num_processes || os.cpus().length; 17 | // Create a worker for each CPU 18 | for (let i = 0; i < processNum; i++) { 19 | cluster.fork(); 20 | } 21 | // Listen for dying workers 22 | cluster.on("exit", worker => { 23 | // Replace the dead worker, we're not sentimental 24 | console.log(`Worker ${ worker.id } died`); 25 | cluster.fork(); 26 | }); 27 | } else { 28 | init().then(() => { 29 | const server = http.createServer(app); 30 | server.listen(config.port); 31 | server.on("error", onError); 32 | server.on("listening", onListening(server)); 33 | console.log(`Worker ${ cluster.worker.id } running`); 34 | }).catch(e => { 35 | console.log(`Worker ${ cluster.worker.id } failed: failed to start server: ${ e }`); 36 | process.exit(1); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /scripts/src/public/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment"; 2 | import { getDefaultLogger as logger } from "../logging"; 3 | import * as jsonwebtoken from "jsonwebtoken"; 4 | 5 | import { isNothing } from "../utils/utils"; 6 | import { Application } from "../models/applications"; 7 | import { 8 | BadJWTInput, 9 | InvalidJwtIssuedTime, 10 | InvalidJwtSignature, 11 | JwtKidMissing, 12 | NoSuchApp, 13 | NoSuchPublicKey 14 | } from "../errors"; 15 | 16 | export type JWTClaims = { 17 | iss: string; // issuer - the app_id 18 | exp: number; // expiration 19 | iat: number; // issued at 20 | sub: SUB; // subject 21 | }; 22 | 23 | export type JWTContent = { 24 | header: { 25 | typ: string; 26 | alg: string; 27 | kid: string; 28 | }; 29 | payload: JWTClaims & T; 30 | signature: string; 31 | }; 32 | 33 | export async function verify(token: string): Promise> { 34 | const decoded = jsonwebtoken.decode(token, { complete: true }) as JWTContent | null; 35 | if (isNothing(decoded)) { 36 | throw BadJWTInput(token); 37 | } 38 | if (decoded.header.alg.toUpperCase() !== "ES256") { 39 | logger().warn(`got JWT with wrong algorithm ${ decoded.header.alg }. ignoring`); 40 | // throw WrongJWTAlgorithm(decoded.header.alg); // TODO uncomment when we deprecate other algo support 41 | } 42 | 43 | const now = moment(); 44 | if (now.isBefore(moment.unix(decoded.payload.iat))) { 45 | throw InvalidJwtIssuedTime(decoded.payload.iat); 46 | } 47 | 48 | // if (now.isAfter(moment.unix(decoded.payload.exp))) { 49 | // throw ExpiredJwt(decoded.payload.exp); 50 | // } 51 | 52 | const appId = decoded.payload.iss; 53 | const app = await Application.get(appId); 54 | if (!app) { 55 | throw NoSuchApp(appId); 56 | } 57 | 58 | const kid = decoded.header.kid; 59 | if (isNothing(kid)) { 60 | throw JwtKidMissing(); 61 | } 62 | 63 | const publicKey = app.jwtPublicKeys[kid]; 64 | if (!publicKey) { 65 | throw NoSuchPublicKey(appId, kid); 66 | } 67 | 68 | try { 69 | await asyncJwtVerify(token, publicKey, { ignoreExpiration: true }); // throws 70 | } catch (e) { 71 | throw InvalidJwtSignature(); 72 | } 73 | 74 | return decoded; 75 | } 76 | 77 | function asyncJwtVerify(token: string, publicKey: string, options: object) { 78 | return new Promise( 79 | (res, rej) => { 80 | jsonwebtoken.verify(token, publicKey, options, (err, decoded) => { 81 | if (err || !decoded) { 82 | rej(err); 83 | } else { 84 | res(decoded); 85 | } 86 | }); 87 | } 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /scripts/src/public/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as bearerToken from "express-bearer-token"; 3 | import * as httpContext from "express-http-context"; 4 | 5 | import { logRequest, reportMetrics, requestLogger, setHttpContextFromRequest } from "../middleware"; 6 | 7 | export { notFoundHandler, generalErrorHandler, statusHandler } from "../middleware"; 8 | 9 | export function init(app: express.Express) { 10 | app.use(httpContext.middleware as express.RequestHandler); 11 | app.use(setHttpContextFromRequest); 12 | app.use(bearerToken()); 13 | app.use(requestLogger); 14 | app.use(logRequest); 15 | app.use(reportMetrics); 16 | } 17 | -------------------------------------------------------------------------------- /scripts/src/public/routes/config.ts: -------------------------------------------------------------------------------- 1 | import { KeyMap } from "../../utils/utils"; 2 | import { getConfig } from "../config"; 3 | import { NextFunction, Request, RequestHandler, Response } from "express"; 4 | import { 5 | getAppBlockchainVersion as getAppBlockchainVersionService, 6 | setAppBlockchainVersion as setAppBlockchainVersionService 7 | } from "../services/applications"; 8 | import { BlockchainConfig, getBlockchainConfig } from "../services/payment"; 9 | import { AuthenticatedRequest } from "../auth"; 10 | import { getDefaultLogger as log } from "../../logging"; 11 | import { getJwtKeys } from "../services/internal_service"; 12 | 13 | import { BlockchainVersion, BlockchainVersionValues } from "../../models/offers"; 14 | 15 | const CONFIG = getConfig(); 16 | let JWT_KEYS: KeyMap; 17 | // one time get config from payment service 18 | let BLOCKCHAIN: BlockchainConfig; 19 | let BLOCKCHAIN3: BlockchainConfig; 20 | 21 | export async function init() { 22 | BLOCKCHAIN = await getBlockchainConfig("2"); 23 | BLOCKCHAIN3 = await getBlockchainConfig("3"); 24 | JWT_KEYS = await getJwtKeys(); 25 | } 26 | 27 | export type ConfigResponse = { 28 | jwt_keys: KeyMap, 29 | blockchain: BlockchainConfig; 30 | blockchain3: BlockchainConfig; 31 | bi_service: string; 32 | webview: string; 33 | environment_name: string; 34 | ecosystem_service: string; 35 | }; 36 | 37 | export const getConfigHandler = async function(req: Request, res: Response, next: NextFunction) { 38 | const data: ConfigResponse = { 39 | jwt_keys: await getJwtKeys(), 40 | blockchain: await getBlockchainConfig("2"), 41 | blockchain3: await getBlockchainConfig("3"), 42 | bi_service: CONFIG.bi_service, 43 | webview: CONFIG.webview, 44 | environment_name: CONFIG.environment_name, 45 | ecosystem_service: CONFIG.ecosystem_service 46 | }; 47 | res.status(200).send(data); 48 | } as RequestHandler; 49 | 50 | export type GetAppBlockchainVersionRequest = Request & { 51 | params: { 52 | app_id: string; 53 | }; 54 | }; 55 | 56 | export const getAppBlockchainVersion = async function(req: GetAppBlockchainVersionRequest, res: Response) { 57 | const app_id = req.params.app_id; 58 | const data = await getAppBlockchainVersionService(app_id); 59 | res.status(200).send(data); 60 | } as any as RequestHandler; 61 | 62 | type SetAppBlockchainVersionRequest = GetAppBlockchainVersionRequest & { 63 | body: { 64 | blockchain_version: string; 65 | }; 66 | }; 67 | export const setAppBlockchainVersion = async function(req: SetAppBlockchainVersionRequest, res: Response) { 68 | const { blockchain_version } = req.body; 69 | const app_id = req.params.app_id; 70 | await setAppBlockchainVersionService(app_id, blockchain_version); 71 | res.sendStatus(204); 72 | } as any as RequestHandler; 73 | -------------------------------------------------------------------------------- /scripts/src/public/routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { statusHandler } from "../middleware"; 4 | 5 | import { getOffers } from "./offers"; 6 | import { 7 | getConfigHandler, 8 | getAppBlockchainVersion, 9 | setAppBlockchainVersion, 10 | } from "./config"; 11 | import { 12 | userInfo, 13 | myUserInfo, 14 | v1UserInfo, 15 | signInUser, 16 | updateUser, 17 | userExists, 18 | logoutUser, 19 | activateUser, 20 | v1SignInUser, 21 | v1MyUserInfo, bulkUserCreation, 22 | getUserBlockchainVersion, 23 | } from "./users"; 24 | import { 25 | cancelOrder, 26 | changeOrder, 27 | createExternalOrder, 28 | v1CreateExternalOrder, 29 | createMarketplaceOrder, 30 | createOutgoingTransferOrder, 31 | createIncomingTransferOrder, 32 | getOrder, 33 | getOrderHistory, 34 | submitOrder, 35 | } from "./orders"; 36 | import { authenticateUser } from "../auth"; 37 | import { accountStatus, addGradualMigrationUsers } from "../services/migration"; 38 | 39 | export function createRoutes(app: express.Express, pathPrefix?: string) { 40 | function prefix(path: string): string { 41 | if (!pathPrefix) { 42 | return path; 43 | } 44 | return `${ pathPrefix }/${ path }`; 45 | } 46 | 47 | app.get(prefix("offers/"), authenticateUser, getOffers); 48 | 49 | app.post(prefix("offers/external/orders"), authenticateUser, createExternalOrder); 50 | app.post(prefix("offers/:offer_id/orders"), authenticateUser, createMarketplaceOrder); 51 | 52 | app.post(prefix("transfers/outgoing/orders"), authenticateUser, createOutgoingTransferOrder); 53 | app.post(prefix("transfers/incoming/orders"), authenticateUser, createIncomingTransferOrder); 54 | 55 | app.get(prefix("orders/"), authenticateUser, getOrderHistory); 56 | app.get(prefix("orders/:order_id"), authenticateUser, getOrder); 57 | app.post(prefix("orders/:order_id"), authenticateUser, submitOrder); 58 | app.delete(prefix("orders/:order_id"), authenticateUser, cancelOrder); 59 | app.patch(prefix("orders/:order_id"), authenticateUser, changeOrder); 60 | 61 | app.post(prefix("users/me/activate"), authenticateUser, activateUser); 62 | app.get(prefix("users/exists"), authenticateUser, userExists); 63 | app.get(prefix("users/me"), authenticateUser, myUserInfo); 64 | app.get(prefix("users/:user_id"), authenticateUser, userInfo); 65 | 66 | app.patch(prefix("users/me"), authenticateUser, updateUser); 67 | app.delete(prefix("users/me/session"), authenticateUser, logoutUser); 68 | app.post(prefix("users/"), signInUser); 69 | app.get(prefix("users/:wallet_address/blockchain_version"), getUserBlockchainVersion); 70 | 71 | app.get(prefix("config/"), getConfigHandler); 72 | 73 | app.get(prefix("applications/:app_id/blockchain_version"), getAppBlockchainVersion); 74 | if (process.env.environment_name !== "production") { 75 | app.put(prefix("applications/:app_id/blockchain_version"), setAppBlockchainVersion); 76 | } 77 | app.post(prefix("applications/:app_id/migration/users"), addGradualMigrationUsers); 78 | app.get(prefix("migration/info/:app_id/:public_address"), accountStatus); 79 | 80 | app.get("/status", statusHandler); 81 | } 82 | 83 | export function createV1Routes(app: express.Express, pathPrefix?: string) { 84 | function prefix(path: string): string { 85 | if (!pathPrefix) { 86 | return path; 87 | } 88 | return `${ pathPrefix }/${ path }`; 89 | } 90 | 91 | app.get(prefix("offers/"), authenticateUser, getOffers); 92 | 93 | app.post(prefix("offers/external/orders"), authenticateUser, v1CreateExternalOrder); 94 | app.post(prefix("offers/:offer_id/orders"), authenticateUser, createMarketplaceOrder); 95 | 96 | app.get(prefix("orders/"), authenticateUser, getOrderHistory); 97 | app.get(prefix("orders/:order_id"), authenticateUser, getOrder); 98 | app.post(prefix("orders/:order_id"), authenticateUser, submitOrder); 99 | app.delete(prefix("orders/:order_id"), authenticateUser, cancelOrder); 100 | app.patch(prefix("orders/:order_id"), authenticateUser, changeOrder); 101 | 102 | app.post(prefix("users/me/activate"), authenticateUser, activateUser); 103 | app.get(prefix("users/exists"), authenticateUser, userExists); 104 | app.get(prefix("users/me"), authenticateUser, v1MyUserInfo); 105 | app.get(prefix("users/:user_id"), authenticateUser, v1UserInfo); 106 | 107 | app.patch(prefix("users/"), authenticateUser, updateUser); // deprecated, use users/me 108 | app.patch(prefix("users/me"), authenticateUser, updateUser); 109 | app.delete(prefix("users/me/session"), authenticateUser, logoutUser); 110 | app.post(prefix("users/"), v1SignInUser); // this is different than the new version 111 | 112 | app.get(prefix("config/"), getConfigHandler); 113 | } 114 | 115 | export function createPublicFacing(app: express.Express, pathPrefix?: string) { 116 | function prefix(path: string): string { 117 | if (!pathPrefix) { 118 | return path; 119 | } 120 | return `${ pathPrefix }/${ path }`; 121 | } 122 | 123 | app.post(prefix("users/bulk"), bulkUserCreation); 124 | } 125 | -------------------------------------------------------------------------------- /scripts/src/public/routes/offers.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, RequestHandler } from "express"; 2 | 3 | import { AuthenticatedRequest } from "../auth"; 4 | import { OfferType } from "../../models/offers"; 5 | 6 | import { getOffers as getOffersService } from "../services/offers"; 7 | 8 | /** 9 | * Return a list of offers 10 | */ 11 | export type GetOffersRequest = AuthenticatedRequest & { 12 | query: { 13 | type: OfferType 14 | } 15 | }; 16 | export const getOffers = async function(req: GetOffersRequest, res: Response, next: NextFunction) { 17 | try { 18 | const data = await getOffersService(req.context.user.id, req.context.user.appId, req.query, req.acceptsLanguages.bind(req)); 19 | res.status(200).send(data); 20 | } catch (err) { 21 | next(err); 22 | } 23 | } as RequestHandler; 24 | -------------------------------------------------------------------------------- /scripts/src/public/services/applications.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultLogger as logger } from "../../logging"; 2 | 3 | import { verify as verifyJwt } from "../jwt"; 4 | import { InvalidApiKey, MissingField, NoSuchApp } from "../../errors"; 5 | import { Application, AppWhitelists } from "../../models/applications"; 6 | import { BlockchainVersion } from "../../models/offers"; 7 | import moment = require("moment"); 8 | 9 | export type RegisterPayload = { 10 | user_id: string; 11 | api_key: string; 12 | device_id: string; 13 | }; 14 | export type SignInContext = { 15 | appId: string; 16 | deviceId: string; 17 | appUserId: string; 18 | }; 19 | 20 | export type V1RegisterPayload = { 21 | user_id: string; 22 | api_key: string; 23 | }; 24 | export type V1SignInContext = { 25 | appId: string; 26 | appUserId: string; 27 | }; 28 | 29 | export async function validateRegisterJWT(jwt: string): Promise { 30 | const decoded = await verifyJwt, "register">(jwt); 31 | 32 | // payload.user_id field is mandatory 33 | if (!decoded.payload.user_id) { 34 | throw MissingField("user_id"); 35 | } 36 | if (!decoded.payload.device_id) { 37 | throw MissingField("device_id"); 38 | } 39 | 40 | const appId = decoded.payload.iss; 41 | const appUserId = decoded.payload.user_id; 42 | const deviceId = decoded.payload.device_id; 43 | 44 | return { appUserId, appId, deviceId }; 45 | } 46 | 47 | export async function v1ValidateRegisterJWT(jwt: string): Promise { 48 | const decoded = await verifyJwt, "register">(jwt); 49 | 50 | // payload.user_id field is mandatory 51 | if (!decoded.payload.user_id) { 52 | throw MissingField("user_id"); 53 | } 54 | 55 | const appId = decoded.payload.iss; 56 | const appUserId = decoded.payload.user_id; 57 | 58 | return { appUserId, appId }; 59 | } 60 | 61 | export async function validateWhitelist(appUserId: string, deviceId: string, apiKey: string): Promise { 62 | // check if apiKey matches appId 63 | const app = await Application.findOne({ apiKey }); 64 | if (!app) { 65 | throw InvalidApiKey(apiKey); 66 | } 67 | 68 | // check if userId is whitelisted in app 69 | logger().info(`checking if ${ appUserId } is whitelisted for ${ app.id }`); 70 | const result = await AppWhitelists.findOne({ appUserId, appId: app.id }); 71 | if (result) { 72 | return { appUserId, deviceId, appId: app.id }; 73 | } 74 | // XXX raise an exception 75 | logger().warn(`user ${ appUserId } not found in whitelist for app ${ app.id }`); 76 | 77 | return { appUserId, deviceId, appId: app.id }; 78 | } 79 | 80 | export async function v1ValidateWhitelist(appUserId: string, apiKey: string): Promise { 81 | // check if apiKey matches appId 82 | const app = await Application.findOne({ apiKey }); 83 | if (!app) { 84 | throw InvalidApiKey(apiKey); 85 | } 86 | 87 | // check if userId is whitelisted in app 88 | logger().info(`checking if ${ appUserId } is whitelisted for ${ app.id }`); 89 | const result = await AppWhitelists.findOne({ appUserId, appId: app.id }); 90 | if (result) { 91 | return { appUserId, appId: app.id }; 92 | } 93 | // XXX raise an exception 94 | logger().warn(`user ${ appUserId } not found in whitelist for app ${ app.id }`); 95 | 96 | return { appUserId, appId: app.id }; 97 | } 98 | 99 | export async function getAppBlockchainVersion(app_id: string): Promise { 100 | const app = await Application.get(app_id); 101 | if (!app) { 102 | throw NoSuchApp(app_id); 103 | } 104 | return app.config.blockchain_version; 105 | } 106 | 107 | export async function setAppBlockchainVersion(app_id: string, blockchain_version: BlockchainVersion | "gradual"): Promise { 108 | const app = await Application.get(app_id); 109 | if (!app) { 110 | throw NoSuchApp(app_id); 111 | } 112 | 113 | if (blockchain_version === "gradual") { 114 | app.config.blockchain_version = "2"; 115 | app.config.gradual_migration_date = moment().toISOString(); 116 | } else { 117 | app.config.blockchain_version = blockchain_version; 118 | app.config.gradual_migration_date = null; 119 | } 120 | await Application.createQueryBuilder() 121 | .update("applications") 122 | .set({ config: app.config }) 123 | .where("id = :id", { id: app_id }) 124 | .execute(); 125 | } 126 | -------------------------------------------------------------------------------- /scripts/src/public/services/index.ts: -------------------------------------------------------------------------------- 1 | export interface Paging { 2 | cursors: { 3 | after?: string; 4 | before?: string; 5 | }; 6 | previous?: string; 7 | next?: string; 8 | } 9 | -------------------------------------------------------------------------------- /scripts/src/public/services/internal_service.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config"; 2 | import { KeyMap } from "../../utils/utils"; 3 | import { getAxiosClient } from "../../utils/axios_client"; 4 | 5 | const config = getConfig(); 6 | const httpClient = getAxiosClient({ timeout: 500 }); 7 | 8 | export async function getJwtKeys(): Promise { 9 | const res = await httpClient.get(`${ config.internal_service }/v1/internal/jwt-keys`); 10 | return res.data; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/src/public/services/migration.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { BlockchainVersion } from "../../models/offers"; 3 | import { GradualMigrationUser, Wallet, WalletApplication } from "../../models/users"; 4 | import { getDefaultLogger as logger } from "../../logging"; 5 | import { 6 | hasKin2ZeroBalance, 7 | hasKin3Account, 8 | migrateZeroBalance, 9 | validateMigrationListJWT, withinMigrationRateLimit 10 | } from "../../utils/migration"; 11 | import { Application } from "../../models/applications"; 12 | import { NoSuchApp } from "../../errors"; 13 | import * as metrics from "../../metrics"; 14 | 15 | type AccountStatusRequest = express.Request & { 16 | params: { 17 | public_address: string; 18 | app_id: string; 19 | } 20 | }; 21 | 22 | type AccountMigrationStatus = { 23 | should_migrate: boolean; 24 | app_blockchain_version: BlockchainVersion; 25 | restore_allowed: boolean; 26 | wallet_blockchain_version: BlockchainVersion | null; 27 | }; 28 | 29 | // return true if user can skip migration - checks if zero balance optimization can be performed 30 | async function canSkipMigration(walletAddress: string): Promise { 31 | if ( 32 | // zero balance accounts don't need to migrate. 33 | // We check the balance of this account on kin2 prior to deciding 34 | await hasKin2ZeroBalance(walletAddress) 35 | // account must be created on kin3, otherwise the migrate call will block for a few seconds, 36 | // which we prefer the client will do 37 | && await hasKin3Account(walletAddress)) { 38 | try { 39 | await migrateZeroBalance(walletAddress); 40 | // XXX - this is called by migration servic: await WalletApplication.updateCreatedDate(walletAddress, "createdDateKin3") 41 | logger().info(`can skip migration for ${ walletAddress }`); 42 | return true; 43 | } catch (e) { 44 | logger().warn("migration on behalf of user failed ", { reason: (e as Error).message }); 45 | // fail to call migrate - let user call it instead 46 | } 47 | } 48 | logger().info(`can NOT skip migration for ${ walletAddress }`); 49 | return false; 50 | } 51 | 52 | // return blockchainVersion for a wallet 53 | async function getBlockchainVersionForWallet(wallet: WalletApplication, app: Application): Promise<{ blockchainVersion: BlockchainVersion, shouldMigrate: boolean }> { 54 | 55 | if (wallet.createdDateKin3) { 56 | logger().info(`wallet created on kin3 - dont migrate ${ wallet.walletAddress }`); 57 | metrics.migrationInfo(app.id, "wallet_on_kin3"); 58 | return { blockchainVersion: "3", shouldMigrate: false }; 59 | } 60 | 61 | if (app.config.blockchain_version === "3" && app.config.gradual_migration_date && await withinMigrationRateLimit(app.id, wallet.walletAddress)) { 62 | logger().info(`app on kin3 - should migrate (gradual kill switch) ${ wallet.walletAddress }`); 63 | metrics.migrationInfo(app.id, "app_on_kin3"); 64 | return { blockchainVersion: "3", shouldMigrate: true }; 65 | } 66 | 67 | if (app.config.blockchain_version === "3" && !app.config.gradual_migration_date) { 68 | logger().info(`app on kin3 - should migrate ${ wallet.walletAddress }`); 69 | metrics.migrationInfo(app.id, "app_on_kin3"); 70 | return { blockchainVersion: "3", shouldMigrate: true }; 71 | } 72 | 73 | if (app.shouldApplyGradualMigration()) { 74 | const whitelisted = await GradualMigrationUser.findByWallet(wallet.walletAddress); 75 | if (whitelisted.length > 0 && (whitelisted.some(w => !!w.migrationDate) || await withinMigrationRateLimit(app.id, wallet.walletAddress))) { 76 | await GradualMigrationUser.setAsMigrated(whitelisted.map(w => w.userId)); 77 | logger().info(`kin2 user on migration list - should migrate ${ wallet.walletAddress }`); 78 | metrics.migrationInfo(app.id, "gradual_migration"); 79 | return { blockchainVersion: "3", shouldMigrate: true }; 80 | } 81 | // else, user is not whitelisted or is whitelisted but rate limit applied 82 | } 83 | logger().info(`kin2 user not on migration list - dont migrate ${ wallet.walletAddress }`); 84 | return { blockchainVersion: "2", shouldMigrate: false }; 85 | } 86 | 87 | export const accountStatus = async function(req: AccountStatusRequest, res: express.Response) { 88 | const publicAddress: string = req.params.public_address; 89 | const appId: string = req.params.app_id; 90 | const app = await Application.get(appId); 91 | if (!app) { 92 | throw NoSuchApp(appId); 93 | } 94 | logger().info(`handling account status request app_id: ${ appId } public_address: ${ publicAddress }`); 95 | const wallet = await WalletApplication.get(publicAddress); 96 | 97 | let blockchainVersion: BlockchainVersion; 98 | let walletBlockchainVersion: BlockchainVersion | null = null; 99 | let shouldMigrate: boolean; 100 | if (!wallet) { 101 | blockchainVersion = app.config.blockchain_version; 102 | shouldMigrate = false; // TODO shouldMigrate non existing wallet on kin3? 103 | } else { 104 | ({ blockchainVersion, shouldMigrate } = await getBlockchainVersionForWallet(wallet, app)); 105 | if (shouldMigrate && await canSkipMigration(publicAddress)) { 106 | metrics.skipMigration(appId); 107 | shouldMigrate = false; 108 | walletBlockchainVersion = blockchainVersion; 109 | } 110 | } 111 | logger().info(`handling account status response app_id: ${ appId } public_address: ${ publicAddress }, ${ shouldMigrate }, ${ blockchainVersion }`); 112 | 113 | res.status(200).send({ 114 | should_migrate: shouldMigrate, 115 | app_blockchain_version: blockchainVersion, 116 | // restore allowed when no wallet was found or the appId is equal 117 | restore_allowed: !wallet || wallet.appId === appId, 118 | wallet_blockchain_version: walletBlockchainVersion, 119 | } as AccountMigrationStatus); 120 | } as express.RequestHandler; 121 | 122 | type AddGradualMigrationListRequest = express.Request & { 123 | params: { 124 | app_id: string; 125 | }; 126 | body: { 127 | jwt: string; 128 | }; 129 | }; 130 | 131 | export const addGradualMigrationUsers = async function(req: AddGradualMigrationListRequest, res: express.Response) { 132 | const appId = req.params.app_id; 133 | const jwt = req.body.jwt; 134 | const appUserIds = await validateMigrationListJWT(jwt, appId); 135 | await GradualMigrationUser.addList(appId, appUserIds); 136 | res.status(204).send(); 137 | } as express.RequestHandler; 138 | -------------------------------------------------------------------------------- /scripts/src/public/services/native_offers.ts: -------------------------------------------------------------------------------- 1 | import { JWTClaims, verify as verifyJWT } from "../jwt"; 2 | import { 3 | ExternalOrderByDifferentUser, 4 | ExternalOrderByDifferentDevice, 5 | InvalidExternalOrderJwt, 6 | MissingField 7 | } from "../../errors"; 8 | import { User } from "../../models/users"; 9 | 10 | export type ExternalOfferPayload = { 11 | id: string; 12 | amount: number; 13 | }; 14 | 15 | export type ExternalEngagedUserPayload = { 16 | device_id: string; 17 | }; 18 | 19 | export type ExternalUserPayload = { 20 | user_id: string; 21 | title: string; 22 | description: string; 23 | }; 24 | 25 | export type JWTPayload = { 26 | nonce?: string; 27 | offer: ExternalOfferPayload; 28 | }; 29 | 30 | export type SpendPayload = JWTPayload & { 31 | sender: ExternalUserPayload & ExternalEngagedUserPayload; 32 | }; 33 | 34 | export type EarnPayload = JWTPayload & { 35 | recipient: ExternalUserPayload & ExternalEngagedUserPayload; 36 | }; 37 | 38 | export type PayToUserPayload = SpendPayload & { 39 | recipient: ExternalUserPayload; 40 | }; 41 | 42 | export type ExternalEarnOrderJWT = JWTClaims<"earn"> & EarnPayload; 43 | export type ExternalSpendOrderJWT = JWTClaims<"spend"> & SpendPayload; 44 | export type ExternalPayToUserOrderJWT = JWTClaims<"pay_to_user"> & PayToUserPayload; 45 | export type ExternalOrderJWT = ExternalEarnOrderJWT | ExternalSpendOrderJWT | ExternalPayToUserOrderJWT; 46 | 47 | export function isExternalEarn(jwt: ExternalOrderJWT): jwt is ExternalEarnOrderJWT { 48 | return jwt.sub === "earn"; 49 | } 50 | 51 | export function isExternalSpend(jwt: ExternalOrderJWT): jwt is ExternalSpendOrderJWT { 52 | return jwt.sub === "spend"; 53 | } 54 | 55 | export function isPayToUser(jwt: ExternalOrderJWT): jwt is ExternalPayToUserOrderJWT { 56 | return jwt.sub === "pay_to_user"; 57 | } 58 | 59 | function validateUserPayload(data: any, key: "sender" | "recipient", shouldHaveDeviceId: boolean) { 60 | const user = data[key] as ExternalUserPayload | undefined; 61 | 62 | if (!user) { 63 | throw MissingField(key); 64 | } 65 | 66 | if (!user.user_id) { 67 | throw MissingField(`${ key }.user_id`); 68 | } 69 | 70 | if (!user.title) { 71 | throw MissingField(`${ key }.title`); 72 | } 73 | 74 | if (!user.description) { 75 | throw MissingField(`${ key }.description`); 76 | } 77 | 78 | if (shouldHaveDeviceId && !(user as ExternalUserPayload & ExternalEngagedUserPayload).device_id) { 79 | throw MissingField(`${ key }.device_id`); 80 | } 81 | } 82 | 83 | export async function validateExternalOrderJWT(jwt: string, user: User, deviceId: string): Promise { 84 | const decoded = await verifyJWT(jwt); 85 | 86 | const payload = decoded.payload; 87 | if (payload.sub !== "earn" && payload.sub !== "spend" && payload.sub !== "pay_to_user") { 88 | throw InvalidExternalOrderJwt(`Subject can be either "earn", "spend' or "pay_to_user"`); 89 | } 90 | 91 | // offer field has to exist in earn/spend/pay_to_user JWTs 92 | if (!payload.offer) { 93 | throw MissingField("offer"); 94 | } 95 | if (typeof payload.offer.amount !== "number") { 96 | console.log("throwing"); 97 | throw InvalidExternalOrderJwt("amount field must be a number"); 98 | } 99 | 100 | if (decoded.payload.iss !== user.appId) { 101 | throw InvalidExternalOrderJwt("issuer must match appId"); 102 | } 103 | 104 | switch (payload.sub) { 105 | case "spend": 106 | validateUserPayload(payload, "sender", true); 107 | break; 108 | 109 | case "earn": 110 | validateUserPayload(payload, "recipient", true); 111 | break; 112 | 113 | case "pay_to_user": 114 | validateUserPayload(payload, "sender", true); 115 | validateUserPayload(payload, "recipient", false); 116 | break; 117 | } 118 | 119 | if (isExternalSpend(payload) || isPayToUser(payload)) { 120 | if (payload.sender.user_id !== user.appUserId) { 121 | throw ExternalOrderByDifferentUser(user.appUserId, payload.sender.user_id); 122 | } 123 | 124 | if (payload.sender.device_id !== deviceId) { 125 | throw ExternalOrderByDifferentDevice(deviceId, payload.sender.device_id); 126 | } 127 | } else { // payload.sub === "earn" 128 | if (payload.recipient.user_id !== user.appUserId) { 129 | throw ExternalOrderByDifferentUser(user.appUserId, payload.recipient.user_id); 130 | } 131 | 132 | if (payload.recipient.device_id !== deviceId) { 133 | throw ExternalOrderByDifferentDevice(deviceId, payload.recipient.device_id); 134 | } 135 | } 136 | 137 | return payload as ExternalOrderJWT; 138 | } 139 | -------------------------------------------------------------------------------- /scripts/src/public/services/native_offers.v1.ts: -------------------------------------------------------------------------------- 1 | import { JWTClaims, verify as verifyJWT } from "../jwt"; 2 | import { ExternalOrderByDifferentUser, InvalidExternalOrderJwt, MissingField } from "../../errors"; 3 | import { getDefaultLogger as log } from "../../logging"; 4 | import { User } from "../../models/users"; 5 | 6 | export type ExternalOfferPayload = { 7 | id: string; 8 | amount: number; 9 | }; 10 | export type ExternalSenderPayload = { 11 | user_id?: string; 12 | title: string; 13 | description: string; 14 | }; 15 | export type ExternalRecipientPayload = { user_id: string } & ExternalSenderPayload; 16 | 17 | export type JWTPayload = { 18 | nonce?: string; 19 | offer: ExternalOfferPayload; 20 | }; 21 | 22 | export type SpendPayload = JWTPayload & { 23 | sender: ExternalSenderPayload; 24 | }; 25 | export type EarnPayload = JWTPayload & { 26 | recipient: ExternalRecipientPayload; 27 | }; 28 | export type PayToUserPayload = EarnPayload & SpendPayload; 29 | 30 | export type ExternalEarnOrderJWT = JWTClaims<"earn"> & EarnPayload; 31 | export type ExternalSpendOrderJWT = JWTClaims<"spend"> & SpendPayload; 32 | export type ExternalPayToUserOrderJWT = JWTClaims<"pay_to_user"> & PayToUserPayload; 33 | export type ExternalOrderJWT = ExternalEarnOrderJWT | ExternalSpendOrderJWT | ExternalPayToUserOrderJWT; 34 | 35 | export function isExternalEarn(jwt: ExternalOrderJWT): jwt is ExternalEarnOrderJWT { 36 | return jwt.sub === "earn"; 37 | } 38 | 39 | export function isExternalSpend(jwt: ExternalOrderJWT): jwt is ExternalSpendOrderJWT { 40 | return jwt.sub === "spend"; 41 | } 42 | 43 | export function isPayToUser(jwt: ExternalOrderJWT): jwt is ExternalPayToUserOrderJWT { 44 | return jwt.sub === "pay_to_user"; 45 | } 46 | 47 | export async function validateExternalOrderJWT(jwt: string, user: User): Promise { 48 | const decoded = await verifyJWT, "spend" | "earn" | "pay_to_user">(jwt); 49 | 50 | if (decoded.payload.sub !== "earn" && decoded.payload.sub !== "spend" && decoded.payload.sub !== "pay_to_user") { 51 | throw InvalidExternalOrderJwt(`Subject can be either "earn", "spend' or "pay_to_user"`); 52 | } 53 | 54 | // offer field has to exist in earn/spend/pay_to_user JWTs 55 | if (!decoded.payload.offer) { 56 | throw MissingField("offer"); 57 | } 58 | 59 | if (typeof decoded.payload.offer.amount !== "number") { 60 | throw InvalidExternalOrderJwt("amount field must be a number"); 61 | } 62 | 63 | if (decoded.payload.iss !== user.appId) { 64 | throw InvalidExternalOrderJwt("issuer must match appId"); 65 | } 66 | 67 | switch (decoded.payload.sub) { 68 | case "spend": 69 | if (!decoded.payload.sender) { 70 | throw MissingField("sender"); 71 | } 72 | break; 73 | 74 | case "earn": 75 | if (!decoded.payload.recipient) { 76 | throw MissingField("recipient"); 77 | } 78 | break; 79 | 80 | case "pay_to_user": 81 | if (!decoded.payload.sender) { 82 | throw MissingField("sender"); 83 | } 84 | if (!decoded.payload.recipient) { 85 | throw MissingField("recipient"); 86 | } 87 | break; 88 | 89 | default: 90 | break; 91 | } 92 | 93 | if ( 94 | (decoded.payload.sub === "spend" || decoded.payload.sub === "pay_to_user") 95 | && !!decoded.payload.sender!.user_id && decoded.payload.sender!.user_id !== user.appUserId 96 | ) { 97 | // if sender.user_id is defined and is different than current user, raise error 98 | throw ExternalOrderByDifferentUser(user.appUserId, decoded.payload.sender!.user_id || ""); 99 | } 100 | 101 | if (decoded.payload.sub === "earn" && decoded.payload.recipient && decoded.payload.recipient.user_id !== user.appUserId) { 102 | // check that user_id is defined for earn and is the same as current user 103 | throw ExternalOrderByDifferentUser(user.appUserId, decoded.payload.recipient.user_id); 104 | } 105 | 106 | return decoded.payload as ExternalOrderJWT; 107 | } 108 | -------------------------------------------------------------------------------- /scripts/src/public/services/offer_contents.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultLogger as logger } from "../../logging"; 2 | import { isNothing } from "../../utils/utils"; 3 | import * as db from "../../models/offers"; 4 | import { InvalidPollAnswers, NoSuchOffer } from "../../errors"; 5 | import * as dbOrder from "../../models/orders"; 6 | import { OfferContent } from "../../models/offers"; 7 | 8 | export interface Question { 9 | id: string; 10 | answers: string[]; 11 | } 12 | 13 | export enum PageType { 14 | "FullPageMultiChoice", 15 | "ImageAndText", 16 | "EarnThankYou", 17 | "TimedFullPageMultiChoice", 18 | "SuccessBasedThankYou", 19 | } 20 | 21 | export interface Page { 22 | type: PageType; 23 | } 24 | 25 | export interface PollPage extends Page { 26 | title: string; 27 | description: string; 28 | question: Question; 29 | } 30 | 31 | export interface QuizPage extends Page { 32 | description: string; 33 | amount: number; 34 | rightAnswer: number; // XXX change answers to have an id 35 | question: Question; 36 | } 37 | 38 | export interface EarnThankYouPage extends Page { 39 | type: PageType.EarnThankYou; 40 | description: string; 41 | } 42 | 43 | export interface SuccessBasedThankYouPage extends Page { 44 | type: PageType.SuccessBasedThankYou; 45 | description: string; 46 | } 47 | 48 | export interface Poll { 49 | pages: Array; 50 | } 51 | 52 | export interface Quiz { 53 | pages: Array; 54 | } 55 | 56 | export interface TutorialPage extends Page { 57 | type: PageType.ImageAndText; 58 | title: string; 59 | image: string; 60 | bodyHtml: string; 61 | footerHtml: string; 62 | buttonText: string; 63 | } 64 | 65 | export interface Tutorial { 66 | pages: Array; 67 | } 68 | 69 | export type Answers = { [key: string]: number }; 70 | export type AnswersBackwardSupport = { [key: string]: string }; 71 | 72 | export interface CouponInfo { 73 | title: string; 74 | description: string; 75 | amount: number; 76 | image: string; 77 | confirmation: { 78 | title: string; 79 | description: string; 80 | image: string; 81 | }; 82 | } 83 | 84 | export interface CouponOrderContent { 85 | title: string; 86 | description: string; 87 | link: string; 88 | image: string; 89 | } 90 | 91 | /** 92 | * replace template variables in offer content or order contents 93 | */ 94 | export function replaceTemplateVars(args: { amount: number }, template: string) { 95 | // XXX currently replace here instead of client 96 | return template 97 | .replace(/\${amount}/g, args.amount.toLocaleString("en-US")) 98 | .replace(/\${amount.raw}/g, args.amount.toString()); 99 | } 100 | 101 | // check the order answers and return the new amount for the order 102 | export async function submitFormAndMutateMarketplaceOrder(order: dbOrder.MarketplaceOrder, form: string | undefined) { 103 | const offer = await db.Offer.get(order.offerId); 104 | if (!offer) { 105 | throw NoSuchOffer(order.offerId); 106 | } 107 | 108 | if (offer.type === "earn") { 109 | const offerContent = (await OfferContent.get(order.offerId))!; 110 | 111 | switch (offerContent.contentType) { 112 | case "poll": 113 | // validate form 114 | if (!isValid(offerContent, form)) { 115 | throw InvalidPollAnswers(); 116 | } 117 | await savePollAnswers(order.user.id, order.offerId, order.id, form); // TODO should we also save quiz results? 118 | break; 119 | case "quiz": 120 | order.amount = await sumCorrectQuizAnswers(offerContent, form) || 1; // TODO remove || 1 - don't reward wrong answers 121 | // should we replace order.meta.content 122 | break; 123 | case "tutorial": 124 | // nothing 125 | break; 126 | default: 127 | logger().warn(`unexpected content type ${ offerContent.contentType }`); 128 | } 129 | } 130 | } 131 | 132 | function isValid(offerContent: db.OfferContent, form: string | undefined): form is string { 133 | if (isNothing(form)) { 134 | return false; 135 | } 136 | 137 | let answers: Answers; 138 | try { 139 | answers = JSON.parse(form); 140 | } catch (e) { 141 | return false; 142 | } 143 | 144 | return typeof answers === "object" && !Array.isArray(answers); 145 | } 146 | 147 | async function sumCorrectQuizAnswers(offerContent: db.OfferContent, form: string | undefined): Promise { 148 | if (isNothing(form)) { 149 | return 0; 150 | } 151 | 152 | let answers: Answers; 153 | try { 154 | answers = JSON.parse(form); 155 | } catch (e) { 156 | return 0; 157 | } 158 | 159 | const quiz: Quiz = JSON.parse(offerContent.content); // this might fail if not valid json without replaceTemplateVars 160 | 161 | function sumQuizRightAnswersAmount(sum: number, page: QuizPage | SuccessBasedThankYouPage) { 162 | if (page.type === PageType.TimedFullPageMultiChoice) { 163 | if (answers[page.question.id] === page.rightAnswer) { 164 | return sum + page.amount; 165 | } 166 | } 167 | return sum; 168 | } 169 | 170 | return quiz.pages.reduce(sumQuizRightAnswersAmount, 0); 171 | } 172 | 173 | async function savePollAnswers(userId: string, offerId: string, orderId: string, content: string): Promise { 174 | const answers = db.PollAnswer.new({ 175 | userId, offerId, orderId, content 176 | }); 177 | 178 | await answers.save(); 179 | } 180 | -------------------------------------------------------------------------------- /scripts/src/public/services/payment.ts: -------------------------------------------------------------------------------- 1 | // wrapper for the payment service 2 | // TODO: this is used by both public and internal so should move to shared dir 3 | import { performance } from "perf_hooks"; 4 | import { getDefaultLogger as logger } from "../../logging"; 5 | import { getConfig } from "../config"; 6 | import { BlockchainVersion } from "../../models/offers"; 7 | import { Application } from "../../models/applications"; 8 | import { getAxiosClient } from "../../utils/axios_client"; 9 | import { WalletApplication } from "../../models/users"; 10 | import { UserHasNoWallet } from "../../errors"; 11 | 12 | const config = getConfig(); 13 | const webhook = `${ config.internal_service }/v1/internal/webhook`; 14 | 15 | const httpClient = getAxiosClient(); 16 | 17 | interface PaymentRequest { 18 | amount: number; 19 | app_id: string; 20 | recipient_address: string; 21 | id: string; 22 | callback: string; 23 | } 24 | 25 | interface SubmitTransactionRequest extends PaymentRequest { 26 | sender_address: string; 27 | transaction: string; 28 | } 29 | 30 | export interface Payment { 31 | amount: number; 32 | app_id: string; 33 | recipient_address: string; 34 | id: string; 35 | transaction_id: string; 36 | sender_address: string; 37 | timestamp: string; 38 | } 39 | 40 | interface WalletRequest { 41 | id: string; 42 | app_id: string; 43 | wallet_address: string; 44 | callback: string; 45 | } 46 | 47 | export interface Wallet { 48 | wallet_address: string; 49 | kin_balance: number; 50 | native_balance: number; 51 | } 52 | 53 | export interface Watcher { 54 | wallet_addresses: string[]; 55 | callback: string; 56 | service_id?: string; 57 | } 58 | 59 | const SERVICE_ID = "marketplace"; 60 | 61 | export async function payTo(walletAddress: string, appId: string, amount: number, orderId: string, blockchainVersion: BlockchainVersion) { 62 | logger().info(`paying ${ amount } to ${ walletAddress } with orderId ${ orderId }`); 63 | const payload: PaymentRequest = { 64 | amount, 65 | app_id: appId, 66 | recipient_address: walletAddress, 67 | id: orderId, 68 | callback: webhook, 69 | }; 70 | const t = performance.now(); 71 | 72 | await httpClient.post(`${ getPaymentServiceUrl(blockchainVersion) }/payments`, payload); 73 | 74 | logger().info("pay to took " + (performance.now() - t) + "ms"); 75 | } 76 | 77 | export async function submitTransaction(recepientAddress: string, senderAddress: string, appId: string, amount: number, orderId: string, transaction: string) { 78 | logger().info(`submitTransaction of ${ amount } to ${ recepientAddress } from ${ senderAddress } with orderId ${ orderId }`); 79 | const payload: SubmitTransactionRequest = { 80 | amount, 81 | app_id: appId, 82 | recipient_address: recepientAddress, 83 | sender_address: senderAddress, 84 | id: orderId, 85 | callback: webhook, 86 | transaction, 87 | }; 88 | const t = performance.now(); 89 | 90 | await httpClient.post(`${ getPaymentServiceUrl("3") }/tx/submit`, payload); 91 | 92 | logger().info("pay to took " + (performance.now() - t) + "ms"); 93 | } 94 | 95 | export async function createWallet(walletAddress: string, appId: string, userId: string, blockchainVersion: BlockchainVersion) { 96 | const payload: WalletRequest = { 97 | id: userId, 98 | wallet_address: walletAddress, 99 | app_id: appId, 100 | callback: webhook, 101 | }; 102 | const t = performance.now(); 103 | 104 | await httpClient.post(`${ getPaymentServiceUrl(blockchainVersion) }/wallets`, payload); 105 | 106 | logger().info("wallet creation took " + (performance.now() - t) + "ms"); 107 | } 108 | 109 | export async function getWalletData(walletAddress: string, options?: { timeout?: number }): Promise { 110 | options = options || {}; 111 | const blockchainVersion = await WalletApplication.getBlockchainVersion(walletAddress); 112 | 113 | const res = await httpClient.get(`${ getPaymentServiceUrl(blockchainVersion) }/wallets/${ walletAddress }`, { timeout: options.timeout }); 114 | return res.data; 115 | } 116 | 117 | export async function getPayments(walletAddress: string, options?: { timeout?: number }): Promise<{ payments: Payment[] }> { 118 | options = options || {}; 119 | const blockchainVersion = await WalletApplication.getBlockchainVersion(walletAddress); 120 | 121 | const res = await httpClient.get(`${ getPaymentServiceUrl(blockchainVersion) }/wallets/${ walletAddress }/payments`, { timeout: options.timeout }); 122 | return res.data; 123 | } 124 | 125 | export async function setWatcherEndpoint(addresses: string[], blockchainVersion: BlockchainVersion): Promise { 126 | const payload: Watcher = { wallet_addresses: addresses, callback: webhook }; 127 | const res = await httpClient.put(`${ getPaymentServiceUrl(blockchainVersion) }/services/${ SERVICE_ID }`, payload); 128 | return res.data; 129 | } 130 | 131 | export async function addWatcherEndpoint(address: string, paymentId: string, appId: string) { 132 | const blockchainVersion = (await Application.get(appId))!.config.blockchain_version; 133 | logger().info("watch url will be " + `${ getPaymentServiceUrl(blockchainVersion) }/services/${ SERVICE_ID }/watchers/${ address }/payments/${ paymentId }`); 134 | await httpClient.put(`${ getPaymentServiceUrl(blockchainVersion) }/services/${ SERVICE_ID }/watchers/${ address }/payments/${ paymentId }`); 135 | } 136 | 137 | export type BlockchainConfig = { 138 | horizon_url: string; 139 | network_passphrase: string; 140 | asset_issuer: string; 141 | asset_code: string; 142 | }; 143 | 144 | export async function getBlockchainConfig(blockchainVersion: BlockchainVersion): Promise { 145 | const res = await httpClient.get(`${ getPaymentServiceUrl(blockchainVersion) }/config`); 146 | return res.data; 147 | } 148 | 149 | function getPaymentServiceUrl(blockchainVersion: BlockchainVersion): string { 150 | if (blockchainVersion === "3") { 151 | return config.payment_service_v3; 152 | } 153 | return config.payment_service; 154 | } 155 | -------------------------------------------------------------------------------- /scripts/src/redis.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from "util"; 2 | import { RedisClient } from "redis"; 3 | import Redlock = require("redlock"); 4 | 5 | import { getConfig } from "./config"; 6 | import { getDefaultLogger as logger } from "./logging"; 7 | 8 | let isMocked = false; 9 | let client: RedisAsyncClient; 10 | 11 | export type RedisAsyncFunctions = { 12 | get(key: string): Promise; 13 | mget(...key: string[]): Promise; 14 | set(key: string, value: string): Promise<"OK">; 15 | setex(key: string, seconds: number, value: string): Promise<"OK">; 16 | del(key: string): Promise; 17 | incrby(key: string, incValue: number): Promise; 18 | }; 19 | 20 | export type RedisAsyncClient = RedisClient & { 21 | async: RedisAsyncFunctions; 22 | }; 23 | 24 | export function getRedisClient(): RedisAsyncClient { 25 | if (!client) { 26 | if (getConfig().redis === "mock") { 27 | isMocked = true; 28 | client = require("redis-mock").createClient(); 29 | } else { 30 | client = require("redis").createClient(getConfig().redis); 31 | } 32 | client.async = {} as RedisAsyncFunctions; 33 | ["get", "mget", "set", "setex", "del", "incrby"].forEach(name => { 34 | (client.async as any)[name] = promisify((client as any)[name]).bind(client); 35 | }); 36 | } 37 | 38 | return client; 39 | } 40 | 41 | const redlock = new Redlock( 42 | [getRedisClient()], 43 | { 44 | // the expected clock drift; for more details 45 | // see http://redis.io/topics/distlock 46 | driftFactor: 0.01, // time in ms 47 | 48 | // the max number of times Redlock will attempt 49 | // to lock a resource before erroring 50 | retryCount: 10, 51 | 52 | // the time in ms between attempts 53 | retryDelay: 200, // time in ms 54 | 55 | // the max time in ms randomly added to retries 56 | // to improve performance under high contention 57 | // see https://www.awsarchitectureblog.com/2015/03/backoff.html 58 | retryJitter: 200 // time in ms 59 | } 60 | ); 61 | redlock.on("clientError", error => { 62 | logger().error("redis lock client error: ", error); 63 | }); 64 | 65 | export function acquireLock(resource: string, ttl: number = 1000): PromiseLike { 66 | return redlock.lock(resource, ttl); 67 | } 68 | 69 | export type LockHandler = () => T | Promise; 70 | export function lock(resource: string, fn: LockHandler): Promise; 71 | export function lock(resource: string, ttl: number, fn: LockHandler): Promise; 72 | export async function lock(resource: string, p1: number | LockHandler, p2?: LockHandler): Promise { 73 | const ttl = typeof p1 === "number" ? p1 : 1000; 74 | const fn = typeof p1 === "number" ? p2! : p1; 75 | 76 | if (isMocked) { 77 | return Promise.resolve(fn()); 78 | } 79 | 80 | const alock = await redlock.lock(resource, ttl); 81 | let result = fn(); 82 | if ((result as Promise).then) { 83 | result = await result; 84 | } 85 | 86 | await alock.unlock(); 87 | 88 | return result as T; 89 | } 90 | -------------------------------------------------------------------------------- /scripts/src/repl-client.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as net from "net"; 3 | import * as path from "path"; 4 | 5 | const args = process.argv.slice(2); 6 | if (args.length < 1 || !(args[0] as string).match(/^[\w\d.\-_]{3,}:\d{1,5}$/)) { 7 | console.log(`USAGE: ${ path.basename(process.argv[1]) } `); 8 | process.exit(1); 9 | } 10 | 11 | const [host, port] = args[0].split(":"); 12 | 13 | const socket = net.connect(Number(port), host); 14 | 15 | process.stdin.pipe(socket); 16 | socket.pipe(process.stdout); 17 | 18 | socket.on("connect", () => { 19 | process.stdin.setRawMode!(true); 20 | }); 21 | 22 | socket.on("close", () => { 23 | console.log("\nSocket closed"); 24 | process.exit(0); 25 | }); 26 | 27 | process.on("exit", () => { 28 | console.log("Exiting..."); 29 | socket.end(); 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/src/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "http"; 2 | import { Server as netServer } from "net"; 3 | 4 | import * as metrics from "./metrics"; 5 | import { getConfig } from "./config"; 6 | import { ServerError } from "./utils/utils"; 7 | import { getDefaultLogger as logger } from "./logging"; 8 | 9 | const config = getConfig(); 10 | 11 | function cleanup(server: Server) { 12 | logger().info("Shutting down"); 13 | server.close(() => { 14 | logger().info("Done, have a great day!"); 15 | process.exit(0); 16 | }); 17 | } 18 | 19 | /** 20 | * Event listener for HTTP server "error" event. 21 | */ 22 | export function onError(error: ServerError) { 23 | if (error.syscall !== "listen") { 24 | throw error; 25 | } 26 | 27 | // handle specific listen errors with friendly messages 28 | switch (error.code) { 29 | case "EACCES": 30 | logger().error(`${ config.port } requires elevated privileges`); 31 | process.exit(1); 32 | break; 33 | case "EADDRINUSE": 34 | logger().error(`${ config.port } is already in use`); 35 | process.exit(1); 36 | break; 37 | default: 38 | throw error; 39 | } 40 | } 41 | 42 | /** 43 | * Event listener for HTTP server "listening" event. 44 | */ 45 | export function onListening(server: Server | netServer) { 46 | return () => { 47 | const addr = server.address() as { port: number }; 48 | const handler = cleanup.bind(null, server); 49 | process.on("SIGINT", handler); 50 | process.on("SIGTERM", handler); 51 | logger().debug(`Listening on ${ addr.port }`); 52 | }; 53 | } 54 | 55 | /** 56 | * 57 | */ 58 | export function abort(reason?: string, appId?: string) { 59 | metrics.reportProcessAbort(reason, appId); 60 | process.exit(1); // process manager should restart the process 61 | } 62 | -------------------------------------------------------------------------------- /scripts/src/tests/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { init as initConfig, getConfig, Config } from "../config"; 2 | import * as helpers from "./helpers"; 3 | import { generateId } from "../utils/utils"; 4 | import { initLogger } from "../logging"; 5 | import { close as closeModels, init as initModels } from "../models"; 6 | import { localCache } from "../utils/cache"; 7 | import * as metrics from "../metrics"; 8 | 9 | type FakePropertyOnConfig = Config & { 10 | cache_ttl: { 11 | nonexistent_prop: number; 12 | } 13 | }; 14 | 15 | describe("Config", () => { 16 | beforeEach(async done => { 17 | initLogger(); 18 | await initModels(); 19 | await helpers.clearDatabase(); 20 | await helpers.createOffers(); 21 | helpers.patchDependencies(); 22 | 23 | localCache.clear(); 24 | done(); 25 | }); 26 | 27 | afterEach(async done => { 28 | await closeModels(); 29 | await metrics.destruct(); 30 | done(); 31 | }); 32 | 33 | test("Check cache ttl returns default value on non0-existent properties", () => { 34 | initConfig("../../config/public.default.json"); 35 | const config: FakePropertyOnConfig = getConfig(); 36 | expect(config.cache_ttl.nonexistent_prop).toBe(config.cache_ttl.default); 37 | }); 38 | 39 | test("shouldApplyGradualMigration", async () => { 40 | const app = await helpers.createApp(generateId()); 41 | expect(app.shouldApplyGradualMigration()).toBeFalsy(); 42 | app.config.gradual_migration_date = "2019-05-05T10:10:10Z"; 43 | expect(app.shouldApplyGradualMigration()).toBeTruthy(); 44 | expect(app.shouldApplyGradualMigration(new Date("2019-06-05T10:10:10Z"))).toBeTruthy(); 45 | expect(app.shouldApplyGradualMigration(new Date("2019-04-05T10:10:10Z"))).toBeFalsy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /scripts/src/tests/init_tests.ts: -------------------------------------------------------------------------------- 1 | import { init as initConfig } from "../config"; // must be the first import 2 | initConfig("tests/config.json"); 3 | -------------------------------------------------------------------------------- /scripts/src/tests/services/index.spec.ts: -------------------------------------------------------------------------------- 1 | import mock = require("supertest"); 2 | 3 | import { app as webApp } from "../../public/app"; 4 | import * as metrics from "../../metrics"; 5 | import { signJwt } from "../helpers"; 6 | import { validateExternalOrderJWT } from "../../public/services/native_offers"; 7 | import { InvalidExternalOrderJwt, MissingField } from "../../errors"; 8 | import { close as closeModels, init as initModels } from "../../models"; 9 | import { getAppBlockchainVersion as getAppBlockchainVersionService } from "../../public/services/applications"; 10 | import * as helpers from "../helpers"; 11 | import { localCache } from "../../utils/cache"; 12 | import { initLogger } from "../../logging"; 13 | import { generateId, IdPrefix } from "../../utils/utils"; 14 | import { validateMigrationListJWT } from "../../utils/migration"; 15 | import { GradualMigrationUser } from "../../models/users"; 16 | 17 | describe("general api checks", async () => { 18 | beforeEach(async done => { 19 | initLogger(); 20 | await initModels(); 21 | helpers.patchDependencies(); 22 | localCache.clear(); 23 | done(); 24 | }); 25 | 26 | afterEach(async () => { 27 | await closeModels(); 28 | await metrics.destruct(); 29 | }); 30 | 31 | test("unknown api endpoints should return 404", async () => { 32 | await mock(webApp) 33 | .get("/v1/no_such_page") 34 | .expect(404); 35 | }); 36 | 37 | test("app blockchain version should be 2 | 3", async () => { 38 | const application = await helpers.createApp(generateId(IdPrefix.App)); 39 | const blockchainVersion = await getAppBlockchainVersionService(application.id); 40 | expect(blockchainVersion === application.config.blockchain_version && (blockchainVersion === "2" || blockchainVersion === "3")); // checking blochain version from getAppBlockchainVersionService equals to application.config and remains 2 || 3 41 | 42 | await mock(webApp) 43 | .get(`/v2/applications/${ application.id }/blockchain_version/`) 44 | .then(response => { 45 | expect(response.status === 200); 46 | expect(response.body === application.config.blockchain_version && (response.body === "2" || response.body === "3")); 47 | }); 48 | }); 49 | 50 | test("External Order JWT validation throws when amount is not a number", async () => { 51 | const app = await helpers.createApp(generateId(IdPrefix.App)); 52 | const user = await helpers.createUser({ appId: app.id }); 53 | const jwt = await signJwt(app.id, "pay_to_user", { 54 | offer: { 55 | offer_id: "offer.id", 56 | amount: "23", 57 | }, 58 | sender: { 59 | user_id: "some_user_id", 60 | device_id: "some_device_id", 61 | title: "sent moneys", 62 | description: "money sent to test p2p", 63 | }, 64 | recipient: { 65 | user_id: "recipientId", 66 | title: "get moneys", 67 | description: "money received from p2p testing" 68 | }, 69 | }); 70 | await expect(validateExternalOrderJWT(jwt, user, "some_deviceId")).rejects.toThrow(InvalidExternalOrderJwt("amount field must be a number")); 71 | }); 72 | 73 | test("addGradualMigrationUsers", async () => { 74 | const app = await helpers.createApp(generateId(IdPrefix.App)); 75 | const users = [ 76 | await helpers.createUser({ appId: app.id }), 77 | await helpers.createUser({ appId: app.id }), 78 | await helpers.createUser({ appId: app.id }), 79 | await helpers.createUser({ appId: app.id }), 80 | ]; 81 | const userIds = users.map(u => u.appUserId); 82 | const jwt = await signJwt(app.id, "migrate_users", { 83 | user_ids: userIds, 84 | }); 85 | expect(await validateMigrationListJWT(jwt, app.id)).toEqual(userIds); 86 | 87 | await mock(webApp) 88 | .post(`/v2/applications/${ app.id }/migration/users`) 89 | .send({ jwt }) 90 | .expect(204); 91 | const dbUsers = await GradualMigrationUser.findByIds(users.map(u => u.id)); 92 | expect(dbUsers.length).toEqual(users.length); 93 | }); 94 | 95 | test("addGradualMigrationUsers missing user_ids", async () => { 96 | const app = await helpers.createApp(generateId(IdPrefix.App)); 97 | const users = [ 98 | await helpers.createUser({ appId: app.id }), 99 | await helpers.createUser({ appId: app.id }), 100 | await helpers.createUser({ appId: app.id }), 101 | await helpers.createUser({ appId: app.id }), 102 | ]; 103 | const jwt = await signJwt(app.id, "migrate_users", { 104 | blah: users.map(u => u.appUserId), 105 | }); 106 | await expect(validateMigrationListJWT(jwt, app.id)).rejects.toThrow(MissingField("user_ids")); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /scripts/src/tests/services/users.v1.spec.ts: -------------------------------------------------------------------------------- 1 | import mock = require("supertest"); 2 | import { Response } from "supertest"; 3 | 4 | import { app } from "../../public/app"; 5 | import * as metrics from "../../metrics"; 6 | import { AuthToken, User } from "../../models/users"; 7 | import { generateId, generateRandomString, IdPrefix } from "../../utils/utils"; 8 | import { V1WhitelistSignInData } from "../../public/routes/users"; 9 | import { close as closeModels, init as initModels } from "../../models/index"; 10 | import { AuthToken as ApiAuthToken, userExists } from "../../public/services/users"; 11 | 12 | import * as helpers from "../helpers"; 13 | import { localCache } from "../../utils/cache"; 14 | import { createApp } from "../helpers"; 15 | 16 | describe("api tests for v1 users", async () => { 17 | beforeAll(async done => { 18 | await initModels(); 19 | helpers.patchDependencies(); 20 | done(); 21 | }); 22 | 23 | afterAll(async () => { 24 | await closeModels(); 25 | await metrics.destruct(); 26 | }); 27 | 28 | test("user register whitelist", async () => { 29 | const myApp = await helpers.createApp(generateId(IdPrefix.App)); 30 | const signInData: V1WhitelistSignInData = { 31 | sign_in_type: "whitelist", 32 | api_key: myApp.apiKey, 33 | device_id: "my_device_id", 34 | user_id: "my_app_user_id", 35 | wallet_address: helpers.getKeyPair().public 36 | }; 37 | 38 | const res = await mock(app) 39 | .post(`/v1/users/`) 40 | .send(signInData) 41 | .set("x-request-id", "123"); 42 | 43 | const token: ApiAuthToken = res.body; 44 | expect(token.app_id).toEqual(myApp.id); 45 | const lastCreatedToken = (await AuthToken.findOne({ order: { createdDate: "DESC" } }))!; 46 | expect(token.token).toEqual(lastCreatedToken.id); 47 | }); 48 | 49 | test("user profile test", async () => { 50 | const appId = generateId(IdPrefix.App); 51 | await createApp(appId); 52 | const user1 = await helpers.createUser({ appId }); 53 | const user2 = await helpers.createUser({ appId }); 54 | const token: AuthToken = (await AuthToken.findOne({ userId: user1.id }))!; 55 | 56 | await mock(app) 57 | .get(`/v1/users/non_user`) 58 | .set("x-request-id", "123") 59 | .set("Authorization", `Bearer ${ token.id }`) 60 | .expect(404, {}); 61 | 62 | await mock(app) 63 | .get(`/v1/users/${ user1.appUserId }`) 64 | .set("x-request-id", "123") 65 | .set("Authorization", `Bearer ${ token.id }`) 66 | .expect(200, { stats: { earn_count: 0, spend_count: 0 } }); 67 | 68 | await mock(app) 69 | .get(`/v1/users/${ user2.appUserId }`) 70 | .set("x-request-id", "123") 71 | .set("Authorization", `Bearer ${ token.id }`) 72 | .expect(200, {}); 73 | 74 | await helpers.createOrders(user1.id); // creates 1 pending and 1 completed and 1 failed of earn and spend 75 | 76 | await mock(app) 77 | .get(`/v1/users/${ user1.appUserId }`) 78 | .set("x-request-id", "123") 79 | .set("Authorization", `Bearer ${ token.id }`) 80 | .expect(200) 81 | .expect((res: Response) => { 82 | if (res.body.stats.earn_count !== 2 || res.body.stats.spend_count !== 2) { 83 | throw new Error("unexpected body: " + JSON.stringify(res.body)); 84 | } 85 | }); 86 | 87 | // different appId 88 | const app2 = await createApp(generateId(IdPrefix.App)); 89 | const user3 = await helpers.createUser({ appId: app2.id }); 90 | await mock(app) 91 | .get(`/v1/users/${ user3.appUserId }`) 92 | .set("x-request-id", "123") 93 | .set("Authorization", `Bearer ${ token.id }`) 94 | .expect(404); 95 | }); 96 | 97 | test("updateUser", async () => { 98 | const myApp = await helpers.createApp(generateId(IdPrefix.App)); 99 | localCache.clear(); 100 | const user1 = await helpers.createUser({ appId: myApp.id, createWallet: false }); 101 | const newWalletAddress = generateRandomString({ length: 56 }); 102 | const badAddress = generateRandomString({ length: 40 }); 103 | const token = (await AuthToken.findOne({ userId: user1.id }))!; 104 | const mockedApp = mock(app); 105 | 106 | await mockedApp 107 | .patch("/v1/users") 108 | .send({ wallet_address: newWalletAddress }) 109 | .set("Authorization", `Bearer ${ token.id }`) 110 | .expect(204); 111 | const u1 = (await User.findOne({ id: user1.id }))!; 112 | let wallets = await u1.getWallets(); 113 | 114 | expect(wallets.count).toBe(1); 115 | expect(wallets.first!.address).toBe(newWalletAddress); 116 | 117 | await mockedApp 118 | .patch("/v1/users") 119 | .send({ wallet_address: badAddress }) 120 | .set("Authorization", `Bearer ${ token.id }`) 121 | .expect(400); 122 | wallets = await u1.getWallets(); 123 | expect(wallets.count).toBe(1); 124 | expect(wallets.first!.address).toBe(newWalletAddress); 125 | }); 126 | 127 | test("userExists", async () => { 128 | const user = await helpers.createUser(); 129 | expect(await userExists(user.appId, user.appUserId)).toBeTruthy(); 130 | expect(await userExists("another-app", user.appUserId)).toBeFalsy(); 131 | expect(await userExists(user.appId, "another-user-id")).toBeFalsy(); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /scripts/src/tests/translations.spec.ts: -------------------------------------------------------------------------------- 1 | import csvParse = require("csv-parse/lib/sync"); 2 | 3 | import * as path from "path"; 4 | import { readFileSync } from "fs"; 5 | 6 | import { CsvParse, TranslationDataRow } from "../admin/translations"; 7 | import { init as initModels, close as closeModels } from "../models/index"; 8 | import { processFile as adaptCsv } from "../adapt_translation_csv"; 9 | import * as translations from "../admin/translations"; 10 | import { Offer, OfferContent } from "../models/offers"; 11 | import { OfferTranslation } from "../models/translations"; 12 | import { initLogger } from "../logging"; 13 | import * as helpers from "./helpers"; 14 | import { localCache } from "../utils/cache"; 15 | import { initDb } from "../manage_db_data"; 16 | 17 | const CSV_TEMPLATE_FILE = "/tmp/translations_template-by_tests.csv"; 18 | const CSV_TRANSLATION_FILE = "/tmp/translations-by_tests.csv"; // The file the adapted translations will be written to 19 | 20 | describe("translations tests", async () => { 21 | jest.setTimeout(30000); 22 | beforeAll(async done => { 23 | initLogger(); 24 | await initModels(); 25 | await helpers.clearDatabase(); 26 | const scriptConfig = { 27 | apps_dir: "data/apps", 28 | offers_dir: "data/offers", 29 | update_earn_thumbnails: false, 30 | no_update: false, 31 | only_update: false, 32 | dry_run: false, 33 | require_update_confirm: false, 34 | app_list: ["ALL"], 35 | create_db: true, 36 | trans_file: null, 37 | trans_lang: null, 38 | rules_dir: null 39 | }; 40 | await initDb(scriptConfig, false); 41 | helpers.patchDependencies(); 42 | localCache.clear(); 43 | done(); 44 | }); 45 | 46 | afterAll(async done => { 47 | await closeModels(); 48 | done(); 49 | }); 50 | 51 | test("test writeCsvTemplateToFile", async done => { 52 | console.log("test writeCsvTemplateToFile START"); 53 | await translations.writeCsvTemplateToFile(CSV_TEMPLATE_FILE); 54 | const csv = readFileSync(CSV_TEMPLATE_FILE); 55 | const parsedCsv = (csvParse as CsvParse)(csv); 56 | const csvData = parsedCsv.splice(1); 57 | const [type, key, defaultStr, translation, charLimit] = (csvData[Math.round(csvData.length / 2)]) as TranslationDataRow; // Get a translation 58 | expect(type).toMatch(/poll|quiz/); 59 | const keySegments = key.split(":"); 60 | expect(keySegments.length).toBeGreaterThanOrEqual(3); 61 | expect(keySegments[0]).toMatch(/offer$|offer_contents/); 62 | expect(keySegments[1]).toMatch(/O[\w]{20}/); // Validate offer id starts with O and is 21 chars 63 | expect(keySegments[2]).toMatch(/title$|description$|orderDescription|orderTitle|content$/); 64 | expect(typeof defaultStr).toBe("string"); 65 | expect(defaultStr.length).toBeGreaterThan(1); 66 | expect(typeof translation).toBe("string"); 67 | expect(translation.length).toBe(0); 68 | expect(Number(charLimit)).toBeGreaterThan(0); 69 | console.log("test writeCsvTemplateToFile DONE"); 70 | done(); 71 | }); 72 | 73 | test("Adapt test translation CSV to the offers in the DB", async done => { 74 | console.log("Adapt test translation CSV... START"); 75 | await translations.writeCsvTemplateToFile(CSV_TEMPLATE_FILE); 76 | await adaptCsv(path.join(__dirname, "../../../data/translations/test_pt-BR.csv"), CSV_TEMPLATE_FILE, CSV_TRANSLATION_FILE); 77 | const csv = readFileSync(CSV_TRANSLATION_FILE); 78 | const parsedCsv = (csvParse as CsvParse)(csv); 79 | const csvData = parsedCsv.splice(1); 80 | let [type, key, defaultStr, translation, charLimit] = (csvData[Math.round(csvData.length / 2)]) as TranslationDataRow; // Get a random translation 81 | expect(translation.length).toBeGreaterThan(0); 82 | expect(translation.length).toBeLessThanOrEqual(Number(charLimit)); 83 | const testTranslation = csvData.filter(([type, key, defaultStr, translation]: [string, string, string, string]) => translation === "Favoritos"); 84 | expect(testTranslation.length).toBe(1); 85 | [type, key, defaultStr, translation, charLimit] = testTranslation[0]; 86 | const [table, offerId, column, jsonPath] = key.split(":"); 87 | expect((await Offer.findOne({ id: offerId }))!.meta.title).toBe("Favorites"); 88 | console.log("Adapt test translation CSV... DONE"); 89 | done(); 90 | }); 91 | 92 | test("processFile (import) translation CSV", async done => { 93 | console.log("processFile (import) translation CSV START"); 94 | await translations.writeCsvTemplateToFile(CSV_TEMPLATE_FILE); 95 | await adaptCsv(path.join(__dirname, "../../../data/translations/test_pt-BR.csv"), CSV_TEMPLATE_FILE, CSV_TRANSLATION_FILE); 96 | translations.processFile(CSV_TRANSLATION_FILE, "pt-BR"); 97 | expect(await OfferTranslation.find({ translation: "Favoritos" })); 98 | console.log("processFile (import) translation CSV DONE"); 99 | done(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /scripts/src/utils/axios_client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | const axiosRetry = require("axios-retry"); 4 | 5 | const DEFAULTS = { 6 | timeout: 300, 7 | retries: 6 8 | }; 9 | 10 | export function getAxiosClient(options: { timeout?: number, retries?: number } = {}): AxiosInstance { 11 | const client = axios.create({ timeout: options.timeout || DEFAULTS.timeout }); 12 | axiosRetry(client, { retries: options.retries || DEFAULTS.retries, retryCondition: () => true, shouldResetTimeout: true }); 13 | return client; 14 | } 15 | -------------------------------------------------------------------------------- /scripts/src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment"; 2 | import { getConfig } from "../public/config"; 3 | 4 | const config = getConfig(); 5 | 6 | interface CacheValue { 7 | expiresAt: moment.Moment; 8 | data: any; 9 | } 10 | 11 | const cacheTTL = config.cache_ttl.default; // minutes 12 | 13 | const defaultTTL = moment.duration(cacheTTL, "seconds"); 14 | const items = new Map(); 15 | 16 | export const localCache = { 17 | get(key: string): T | null { 18 | const value = items.get(key); 19 | if (value && moment().isBefore(value.expiresAt)) { 20 | return value.data; 21 | } else { 22 | return null; 23 | } 24 | }, 25 | set(key: string, data: any, expiration: moment.Duration = defaultTTL) { 26 | items.set(key, { 27 | expiresAt: moment().add(expiration), 28 | data 29 | }); 30 | }, 31 | clear() { 32 | items.clear(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /scripts/src/utils/migration.ts: -------------------------------------------------------------------------------- 1 | import { getAxiosClient } from "./axios_client"; 2 | import { BlockchainConfig, getBlockchainConfig } from "../public/services/payment"; 3 | import { getDefaultLogger as logger } from "../logging"; 4 | import { getConfig } from "../public/config"; 5 | import { verify as verifyJwt } from "../public/jwt"; 6 | import { InvalidExternalOrderJwt, InvalidJwtField, MissingField } from "../errors"; 7 | import { assertRateLimitMigration } from "./rate_limit"; 8 | import * as metrics from "../metrics"; 9 | import { Application } from "../models/applications"; 10 | 11 | const httpClient = getAxiosClient(); 12 | let BLOCKCHAIN: BlockchainConfig; 13 | let BLOCKCHAIN3: BlockchainConfig; 14 | const ALREADY_MIGRATED_ERROR = 4002; 15 | 16 | export type WalletResponse = { 17 | balances: Array<{ 18 | balance: string, 19 | asset_type: "credit_alphanum4" | "native", 20 | asset_code?: string, 21 | asset_issuer?: string 22 | }>; 23 | signers: Array<{ 24 | weight: number; 25 | }>; 26 | }; 27 | 28 | export async function init() { 29 | BLOCKCHAIN = await getBlockchainConfig("2"); 30 | BLOCKCHAIN3 = await getBlockchainConfig("3"); 31 | } 32 | 33 | // return True if wallet has zero balance on kin2 34 | export async function hasKin2ZeroBalance(walletAddress: string): Promise { 35 | try { 36 | const res = await httpClient.get(`${ BLOCKCHAIN.horizon_url }/accounts/${ walletAddress }`); 37 | for (const balance of res.data.balances) { 38 | if (balance.asset_issuer === BLOCKCHAIN.asset_issuer && 39 | balance.asset_code === BLOCKCHAIN.asset_code) { 40 | return parseFloat(balance.balance) === 0; 41 | } 42 | } 43 | return true; // no balance is zero balance 44 | } catch (e) { 45 | logger().warn("couldn't reach horizon to check user balance - assuming non-zero", { walletAddress }); 46 | return false; // assume user has non zero balance if can't reach horizon 47 | } 48 | } 49 | 50 | export async function hasKin3Account(walletAddress: string) { 51 | try { 52 | await httpClient.get(`${ BLOCKCHAIN3.horizon_url }/accounts/${ walletAddress }`); 53 | return true; 54 | } catch (e) { 55 | return false; 56 | } 57 | } 58 | 59 | export class MigrationError extends Error { 60 | } 61 | 62 | // returns true if migration call succeeded 63 | export async function migrateZeroBalance(walletAddress: string): Promise { 64 | const res = await httpClient.post(`${ getConfig().migration_service }/migrate?address=${ walletAddress }`, 65 | null, 66 | { validateStatus: status => status < 500 }); // allow 4xx errors 67 | if (res.status < 300 || 68 | res.status === 400 && res.data.code === ALREADY_MIGRATED_ERROR) { 69 | return; 70 | } 71 | 72 | throw new MigrationError(`migration failed with status: ${ res.status }`); 73 | } 74 | 75 | type MigrationListPayload = { 76 | user_ids: string[]; 77 | }; 78 | 79 | // return a list of user_ids from jwt if valid 80 | export async function validateMigrationListJWT(jwt: string, appId: string): Promise { 81 | const decoded = await verifyJwt, "migration_list">(jwt); 82 | 83 | if (!decoded.payload.user_ids) { 84 | throw MissingField("user_ids"); 85 | } 86 | 87 | if (decoded.payload.iss !== appId) { 88 | throw InvalidExternalOrderJwt("issuer must match appId"); 89 | } 90 | const app = (await Application.get(appId))!; 91 | const defaultUsersLimit = 500000; 92 | if (decoded.payload.user_ids.length > (app.config.gradual_migration_jwt_users_limit || defaultUsersLimit)) { 93 | throw InvalidJwtField(`'user_ids' value should be less than ${(app.config.gradual_migration_jwt_users_limit || defaultUsersLimit)}`); 94 | } 95 | 96 | return decoded.payload.user_ids; 97 | } 98 | 99 | // return true if a user migration is within rate limits 100 | export async function withinMigrationRateLimit(appId: string, userId?: string) { 101 | try { 102 | await assertRateLimitMigration(appId); 103 | logger().info(`within migration limit for app ${ appId }, user ${ userId }`); 104 | return true; 105 | } catch (e) { 106 | metrics.migrationRateLimitExceeded(appId); 107 | logger().info(`exceeded migration limit for app ${ appId }, user ${ userId }`); 108 | return false; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import * as _path from "path"; 2 | 3 | const fromProjectRoot = _path.join.bind(path, __dirname, "../../../"); 4 | export function path(path: string): string { 5 | if (path.startsWith("/")) { 6 | return path; 7 | } 8 | return fromProjectRoot(path); 9 | } 10 | -------------------------------------------------------------------------------- /tests/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 0, 3 | "loggers": [ 4 | { 5 | "name": "console", 6 | "type": "console", 7 | "format": "pretty-json" 8 | } 9 | ], 10 | "assets_base": "https://s3.amazonaws.com/kinmarketplace-assets/version1/", 11 | "db": { 12 | "type": "postgres", 13 | "host": "localhost", 14 | "port": 25432, 15 | "username": "user", 16 | "password": "pass", 17 | "database": "ecosystem", 18 | "synchronize": true, 19 | "logging": false 20 | }, 21 | "redis": "mock", 22 | "statsd": { 23 | "host": "localhost", 24 | "port": 8125 25 | }, 26 | "payment_service": "http://localhost:5000", 27 | "internal_service": "http://localhost:3001", 28 | "jwt_keys_dir": "tests/jwt", 29 | "bi_service": "https://kin-bi.appspot.com/eco_", 30 | "webview": "https://some-url.com", 31 | "ecosystem_service": "http://localhost:3000", 32 | "environment_name": "local", 33 | "sign_in_types": [ 34 | "jwt", 35 | "whitelist" 36 | ], 37 | "max_daily_earn_offers": 4 38 | } 39 | -------------------------------------------------------------------------------- /tests/jwt/private_keys/es256_0-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHQCAQEEINt5yjRwHhRGypySrfg1EPgF/+SNO22jNHWotkClJ4hpoAcGBSuBBAAK 3 | oUQDQgAEM7Dbok9yzQEGZ5HYEw4huZ5OON5ZsGzj+SlIB31Ha2UWmq8s6v+W7xdm 4 | lhxPmXFj6MOxC2+rgHT/lITuB5lE+A== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/jwt/private_keys/rs512_0-priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw 3 | 33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW 4 | +jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB 5 | AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS 6 | 3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp 7 | uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE 8 | 2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0 9 | GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K 10 | Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY 11 | 6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5 12 | fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523 13 | Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP 14 | FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw== 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/jwt/public_keys/es256_0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEM7Dbok9yzQEGZ5HYEw4huZ5OON5ZsGzj 3 | +SlIB31Ha2UWmq8s6v+W7xdmlhxPmXFj6MOxC2+rgHT/lITuB5lE+A== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/jwt/public_keys/rs512_0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 3 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 4 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 5 | o2kQ+X5xK9cipRgEKwIDAQAB 6 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/wait-for: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TIMEOUT=15 4 | QUIET=0 5 | 6 | echoerr() { 7 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 8 | } 9 | 10 | usage() { 11 | exitcode="$1" 12 | cat << USAGE >&2 13 | Usage: 14 | $cmdname host:port [-t timeout] [-- command args] 15 | -q | --quiet Do not output any status messages 16 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 17 | -- COMMAND ARGS Execute command with args after the test finishes 18 | USAGE 19 | exit "$exitcode" 20 | } 21 | 22 | wait_for() { 23 | for i in `seq $TIMEOUT` ; do 24 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 25 | 26 | result=$? 27 | if [ $result -eq 0 ] ; then 28 | if [ $# -gt 0 ] ; then 29 | exec "$@" 30 | fi 31 | exit 0 32 | fi 33 | sleep 1 34 | done 35 | echo "Operation timed out" >&2 36 | exit 1 37 | } 38 | 39 | while [ $# -gt 0 ] 40 | do 41 | case "$1" in 42 | *:* ) 43 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 44 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 45 | shift 1 46 | ;; 47 | -q | --quiet) 48 | QUIET=1 49 | shift 1 50 | ;; 51 | -t) 52 | TIMEOUT="$2" 53 | if [ "$TIMEOUT" = "" ]; then break; fi 54 | shift 2 55 | ;; 56 | --timeout=*) 57 | TIMEOUT="${1#*=}" 58 | shift 1 59 | ;; 60 | --) 61 | shift 62 | break 63 | ;; 64 | --help) 65 | usage 0 66 | ;; 67 | *) 68 | echoerr "Unknown argument: $1" 69 | usage 1 70 | ;; 71 | esac 72 | done 73 | 74 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 75 | echoerr "Error: you need to provide a host and port to test." 76 | usage 2 77 | fi 78 | 79 | wait_for "$@" 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "outDir": "scripts/bin", 5 | "rootDir": "scripts/src", 6 | "module": "commonjs", 7 | "lib": ["es2018"], 8 | 9 | "strict": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "alwaysStrict": true, 13 | "skipLibCheck": true, 14 | "removeComments": true, 15 | "skipDefaultLibCheck": true, 16 | "emitDecoratorMetadata": true, 17 | "experimentalDecorators": true, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "exclude": [ 21 | "scripts/bin", 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-eslint-rules" 5 | ], 6 | "rules": { 7 | "indent": [ 8 | true, 9 | "tabs", 10 | 2 11 | ], 12 | "quotemark": [ 13 | true, 14 | "double" 15 | ], 16 | "object-curly-spacing": [ 17 | true, 18 | "always" 19 | ], 20 | "arrow-parens": [ 21 | true, 22 | "ban-single-arg-parens" 23 | ], 24 | "whitespace": [ 25 | true, 26 | "check-branch", 27 | "check-decl", 28 | "check-operator", 29 | "check-separator", 30 | "check-typecast", 31 | "check-type", 32 | "check-typecast", 33 | "check-module" 34 | ], 35 | "eofline": true, 36 | "max-line-length": [ 37 | 250 38 | ], 39 | "no-console": false, 40 | "interface-name": [ 41 | true, 42 | "never-prefix" 43 | ], 44 | "no-var-requires": false, 45 | "ordered-imports": false, 46 | "no-shadowed-variable": false, 47 | "interface-over-type-literal": false, 48 | "max-classes-per-file": false, 49 | "object-literal-sort-keys": false, 50 | "no-unused-expression": [ 51 | true, 52 | "allow-fast-null-checks" 53 | ], 54 | "variable-name": [ 55 | false 56 | ], 57 | "only-arrow-functions": false, 58 | "trailing-comma": false, 59 | "no-empty-interface": false, 60 | "no-empty": false, 61 | "unified-signatures": false, 62 | "ban-types": [ 63 | false 64 | ], 65 | "one-line": true, 66 | "object-literal-key-quotes": [ 67 | false 68 | ] 69 | } 70 | } 71 | --------------------------------------------------------------------------------