├── .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 | 
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 |
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
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 |
--------------------------------------------------------------------------------