Compare commits
10 Commits
main
...
ayungavis/
Author | SHA1 | Date | |
---|---|---|---|
|
00ba84fccd | ||
|
d770926651 | ||
|
e46544cfd3 | ||
|
44e38740a2 | ||
|
4ca1a4bcfd | ||
|
d6bdcf8a6c | ||
|
d2990424d2 | ||
|
d943423780 | ||
|
239df3661e | ||
|
bdd892ebe8 |
@ -1,61 +0,0 @@
|
||||
name: Deploy Snowball frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY_USER_KEY: ${{ secrets.REGISTRY_USER_KEY }}
|
||||
REGISTRY_BOND_ID: ${{ secrets.REGISTRY_BOND_ID }}
|
||||
DEPLOYER_LRN: lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||
AUTHORITY: laconic-deploy
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Download yarn
|
||||
run: |
|
||||
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||
chmod +x /usr/local/bin/yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
# Create a .env file with the necessary variables
|
||||
echo "REGISTRY_BOND_ID=$REGISTRY_BOND_ID" > packages/deployer/.env
|
||||
echo "DEPLOYER_LRN=$DEPLOYER_LRN" >> packages/deployer/.env
|
||||
echo "AUTHORITY=$AUTHORITY" >> packages/deployer/.env
|
||||
|
||||
# Create a config file with necessary endpoints and secrets
|
||||
cat > packages/deployer/config.yml <<EOF
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey: $REGISTRY_USER_KEY
|
||||
bondId: $REGISTRY_BOND_ID
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
EOF
|
||||
|
||||
- name: Run deploy script
|
||||
run: |
|
||||
cd packages/deployer
|
||||
./deploy-frontend.sh
|
@ -1,30 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download yarn
|
||||
run: |
|
||||
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||
chmod +x /usr/local/bin/yarn
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn
|
||||
- name: Build libs
|
||||
run: yarn workspace gql-client run build
|
||||
- name: Linter check
|
||||
run: yarn lint
|
2
.github/workflows/lint.yaml
vendored
@ -19,7 +19,5 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn
|
||||
- name: Build libs
|
||||
run: yarn workspace gql-client run build
|
||||
- name: Linter check
|
||||
run: yarn lint
|
||||
|
39
.github/workflows/test-app-deployment.yaml
vendored
@ -1,39 +0,0 @@
|
||||
name: Test webapp deployment
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_app_deployment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Test webapp deployment
|
||||
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
|
||||
- name: Notify Vulcanize Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
|
||||
- name: Notify DeepStack Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}
|
5
.gitignore
vendored
@ -5,8 +5,3 @@ yarn-error.log
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
packages/backend/dev/
|
||||
packages/frontend/dist/
|
||||
|
||||
# ignore all .DS_Store files
|
||||
**/.DS_Store
|
||||
|
@ -1 +0,0 @@
|
||||
v20.12.1
|
3
.vscode/settings.json
vendored
@ -1,7 +1,6 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
|
264
README.md
@ -1,23 +1,269 @@
|
||||
# snowballtools-base
|
||||
|
||||
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
|
||||
## Setup
|
||||
|
||||
## Getting Started
|
||||
- Clone the `snowballtools-base` repo
|
||||
|
||||
### Install dependencies
|
||||
```bash
|
||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||
```
|
||||
|
||||
In the root of the project, run:
|
||||
- In root of the repo, install depedencies
|
||||
|
||||
```zsh
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build backend
|
||||
- Build packages
|
||||
|
||||
```zsh
|
||||
```bash
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
### Environment variables, running the development server, and deployment
|
||||
## Backend
|
||||
|
||||
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
|
||||
- Change directory to `packages/backend`
|
||||
|
||||
```bash
|
||||
cd packages/backend
|
||||
```
|
||||
|
||||
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||
|
||||
```bash
|
||||
mv environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
### Backend Production
|
||||
|
||||
- Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
[server]
|
||||
...
|
||||
[server.session]
|
||||
# Secret should be changed to a different random string
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
||||
# Set URL of the frontend app
|
||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
||||
# Set to true for session cookies to work behind proxy
|
||||
trustProxy = true
|
||||
# Set empty domain when using secure connection
|
||||
domain = ""
|
||||
```
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
- In "Homepage URL", type `https://dashboard.snowballtools.com`
|
||||
- In "Authorization callback URL", type `https://dashboard.snowballtools.com/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://api.snowballtools.com"
|
||||
...
|
||||
```
|
||||
|
||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
# Use actual port for REST endpoint
|
||||
restEndpoint = "http://api.laconic.com:1317"
|
||||
# Use actual port for GQL endpoint
|
||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
||||
# Set private key of account to be used in Laconicd
|
||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
||||
# Set Bond ID to be used for publishing records
|
||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
||||
chainId = "laconic_9000-1"
|
||||
# Set authority that is existing in the chain
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
...
|
||||
```
|
||||
|
||||
- Start the server in `packages/backend`
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Setup Laconicd
|
||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
||||
```
|
||||
|
||||
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
# For registryConfig.restEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||
# 0.0.0.0:32777
|
||||
|
||||
# For registryConfig.gqlEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||
# 0.0.0.0:32771
|
||||
```
|
||||
|
||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Run the script to create bond, reserve the authority and set authority bond
|
||||
|
||||
```bash
|
||||
yarn test:registry:init
|
||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
||||
```
|
||||
|
||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Setup ngrok for GitHub webhooks
|
||||
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
||||
- Start ngrok and point to backend server endpoint
|
||||
|
||||
```bash
|
||||
ngrok http http://localhost:8000
|
||||
```
|
||||
|
||||
- Look for the forwarding URL in ngrok
|
||||
|
||||
```bash
|
||||
...
|
||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
||||
...
|
||||
```
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
||||
...
|
||||
```
|
||||
|
||||
- Start the server in `packages/backend`
|
||||
|
||||
```bash
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
- Change directory to `packages/frontend` in a new terminal
|
||||
|
||||
```bash
|
||||
cd packages/frontend
|
||||
```
|
||||
|
||||
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||
|
||||
```bash
|
||||
mv .env.example .env
|
||||
```
|
||||
|
||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||
```
|
||||
|
||||
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# Set actual owner/name of the template repo that will be used for creating new repo
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||
```
|
||||
|
||||
### Frontend Production
|
||||
|
||||
- Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
- Set the following values in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# Backend server endpoint
|
||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||
```
|
||||
|
||||
- Build the React application
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
- Use a web server for hosting static built files
|
||||
|
||||
```bash
|
||||
python3 -m http.server -d build 3000
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||
|
||||
```env
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Project home page URL is not required to be set
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
|
||||
```
|
||||
|
||||
- Start the React application
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
- The React application will be running in `http://localhost:3000/`
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PKG_DIR="./packages/frontend"
|
||||
OUTPUT_DIR="${PKG_DIR}/dist"
|
||||
OUTPUT_DIR="${PKG_DIR}/build"
|
||||
DEST_DIR=${1:-/data}
|
||||
|
||||
if [[ -d "$DEST_DIR" ]]; then
|
||||
@ -9,23 +9,24 @@ if [[ -d "$DEST_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$PKG_DIR/.env" ]]; then
|
||||
echo "Using existing .env file"
|
||||
else
|
||||
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
|
||||
echo "Created .env file. Please populate with the correct values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
|
||||
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
|
||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||
EOF
|
||||
|
||||
yarn || exit 1
|
||||
yarn build --ignore backend || exit 1
|
||||
yarn build || exit 1
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||
|
@ -4,15 +4,15 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"depcheck": "^1.4.2",
|
||||
"husky": "^8.0.3",
|
||||
"lerna": "^8.0.0",
|
||||
"patch-package": "^8.0.0"
|
||||
"depcheck": "^1.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "lerna run build --stream",
|
||||
"lint": "lerna run lint --stream"
|
||||
"lint": "lerna run lint --stream -- --max-warnings=0"
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
v20.12.1
|
@ -1,76 +1 @@
|
||||
# backend
|
||||
|
||||
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
In the root of the project, run:
|
||||
|
||||
```zsh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build backend
|
||||
|
||||
```zsh
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
#### Local
|
||||
|
||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||
|
||||
```zsh
|
||||
cp environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
#### Staging environment variables
|
||||
|
||||
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
|
||||
|
||||
#### Production environment variables
|
||||
|
||||
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
|
||||
|
||||
### Run development server
|
||||
|
||||
```zsh
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||
|
||||
```zsh
|
||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||
```
|
||||
|
||||
### Staging
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/staging-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/production-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Deployment status
|
||||
|
||||
Dumb for now
|
||||
|
||||
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
|
||||
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
|
||||
|
||||
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
|
||||
# Backend for Snowball Tools
|
||||
|
@ -3,12 +3,9 @@
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
@ -22,7 +19,6 @@
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
@ -30,14 +26,9 @@
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
||||
[misc]
|
||||
projectDomain = "apps.snowballtools.com"
|
||||
|
@ -1,25 +1,20 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"license": "UNLICENSED",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@cerc-io/laconic-sdk": "^0.1.14",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"apollo-server-core": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.18.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
@ -40,21 +35,30 @@
|
||||
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "yarn clean && tsc && yarn copy-assets",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "tsc --noEmit",
|
||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
||||
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
|
||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-session": "^2.0.49",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-semistandard": "^15.0.1",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"workspace": "^0.0.1-preview.1"
|
||||
}
|
||||
|
@ -34,21 +34,16 @@ export interface RegistryConfig {
|
||||
privateKey: string;
|
||||
bondId: string;
|
||||
fetchDeploymentRecordDelay: number;
|
||||
checkAuctionStatusDelay: number;
|
||||
authority: string;
|
||||
fee: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
fees: string;
|
||||
gasPrice: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuctionConfig {
|
||||
commitFee: string;
|
||||
commitsDuration: string;
|
||||
revealFee: string;
|
||||
revealsDuration: string;
|
||||
denom: string;
|
||||
export interface MiscConfig {
|
||||
projectDomain: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@ -56,11 +51,5 @@ export interface Config {
|
||||
database: DatabaseConfig;
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
auction: AuctionConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
apiPublicKey: string;
|
||||
apiPrivateKey: string;
|
||||
defaultOrganizationId: string;
|
||||
};
|
||||
misc: MiscConfig;
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ import {
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere,
|
||||
IsNull,
|
||||
Not
|
||||
FindOptionsWhere
|
||||
} from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
@ -13,7 +11,7 @@ import assert from 'assert';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||
|
||||
import { DatabaseConfig } from './config';
|
||||
import { DatabaseConfig, MiscConfig } from './config';
|
||||
import { User } from './entity/User';
|
||||
import { Organization } from './entity/Organization';
|
||||
import { Project } from './entity/Project';
|
||||
@ -23,7 +21,6 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { getEntities, loadAndSaveData } from './utils';
|
||||
import { UserOrganization } from './entity/UserOrganization';
|
||||
import { Deployer } from './entity/Deployer';
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||
|
||||
@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource;
|
||||
private projectDomain: string;
|
||||
|
||||
constructor({ dbPath }: DatabaseConfig) {
|
||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
@ -43,31 +41,20 @@ export class Database {
|
||||
synchronize: true,
|
||||
logging: false
|
||||
});
|
||||
|
||||
this.projectDomain = projectDomain;
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
await this.dataSource.initialize();
|
||||
log('database initialized');
|
||||
|
||||
let organizations = await this.getOrganizations({});
|
||||
const organizations = await this.getOrganizations({});
|
||||
|
||||
// Load an organization if none exist
|
||||
if (!organizations.length) {
|
||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
organizations = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||
}
|
||||
|
||||
// Hotfix for updating old DB data
|
||||
if (organizations[0].slug === 'snowball-tools-1') {
|
||||
const [orgEntity] = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
|
||||
await this.updateOrganization(
|
||||
organizations[0].id,
|
||||
{
|
||||
slug: orgEntity.slug as string,
|
||||
name: orgEntity.name as string
|
||||
}
|
||||
)
|
||||
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,14 +121,6 @@ export class Database {
|
||||
return newUserOrganization;
|
||||
}
|
||||
|
||||
async updateOrganization(organizationId: string, data: DeepPartial<Organization>): Promise<boolean> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
const updateResult = await organizationRepository.update({ id: organizationId }, data);
|
||||
assert(updateResult.affected);
|
||||
|
||||
return updateResult.affected > 0;
|
||||
}
|
||||
|
||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const projects = await projectRepository.find(options);
|
||||
@ -161,9 +140,7 @@ export class Database {
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.where('project.id = :projectId', {
|
||||
projectId
|
||||
@ -173,24 +150,6 @@ export class Database {
|
||||
return project;
|
||||
}
|
||||
|
||||
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||
const allProjects = await this.getProjects({
|
||||
where: {
|
||||
auctionId: Not(IsNull()),
|
||||
},
|
||||
relations: ['deployments'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const projects = allProjects.filter(project => {
|
||||
if (project.deletedAt !== null) return false;
|
||||
|
||||
return project.deployments.length === 0;
|
||||
});
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
@ -236,8 +195,7 @@ export class Database {
|
||||
relations: {
|
||||
project: true,
|
||||
domain: true,
|
||||
createdBy: true,
|
||||
deployer: true,
|
||||
createdBy: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
@ -478,19 +436,6 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployment = await deploymentRepository.findOneOrFail({
|
||||
where: {
|
||||
id: deploymentId
|
||||
}
|
||||
});
|
||||
|
||||
const deleteResult = await deploymentRepository.softRemove(deployment);
|
||||
|
||||
return Boolean(deleteResult);
|
||||
}
|
||||
|
||||
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
@ -507,15 +452,11 @@ export class Database {
|
||||
id: organizationId
|
||||
});
|
||||
|
||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
async saveProject(project: Project): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
return projectRepository.save(project);
|
||||
}
|
||||
|
||||
async updateProjectById (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
@ -601,24 +542,4 @@ export class Database {
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const newDomain = await deployerRepository.save(data);
|
||||
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async getDeployers(): Promise<Deployer[]> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const deployers = await deployerRepository.find();
|
||||
return deployers;
|
||||
}
|
||||
|
||||
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
||||
|
||||
return deployer;
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
|
||||
import { Project } from './Project';
|
||||
|
||||
@Entity()
|
||||
export class Deployer {
|
||||
@PrimaryColumn('varchar')
|
||||
deployerLrn!: string;
|
||||
|
||||
@Column('varchar')
|
||||
deployerId!: string;
|
||||
|
||||
@Column('varchar')
|
||||
deployerApiUrl!: string;
|
||||
|
||||
@Column('varchar')
|
||||
baseDomain!: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
minimumPayment!: string | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
paymentAddress!: string | null;
|
||||
|
||||
@ManyToMany(() => Project, (project) => project.deployers)
|
||||
projects!: Project[];
|
||||
}
|
@ -6,15 +6,13 @@ import {
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
DeleteDateColumn
|
||||
JoinColumn
|
||||
} from 'typeorm';
|
||||
|
||||
import { Project } from './Project';
|
||||
import { Domain } from './Domain';
|
||||
import { User } from './User';
|
||||
import { Deployer } from './Deployer';
|
||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||
import { AppDeploymentRecordAttributes } from '../types';
|
||||
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
@ -26,7 +24,6 @@ export enum DeploymentStatus {
|
||||
Building = 'Building',
|
||||
Ready = 'Ready',
|
||||
Error = 'Error',
|
||||
Deleting = 'Deleting',
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
@ -34,22 +31,10 @@ export interface ApplicationDeploymentRequest {
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
lrn?: string;
|
||||
auction?: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
payment?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
deployment: string;
|
||||
auction?: string;
|
||||
payment?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
version: string;
|
||||
@ -114,22 +99,6 @@ export class Deployment {
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||
|
||||
@ManyToOne(() => Deployer)
|
||||
@JoinColumn({ name: 'deployerLrn' })
|
||||
deployer!: Deployer;
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
@ -152,7 +121,4 @@ export class Deployment {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
@ -7,16 +7,13 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
DeleteDateColumn,
|
||||
JoinTable,
|
||||
ManyToMany
|
||||
DeleteDateColumn
|
||||
} from 'typeorm';
|
||||
|
||||
import { User } from './User';
|
||||
import { Organization } from './Organization';
|
||||
import { ProjectMember } from './ProjectMember';
|
||||
import { Deployment } from './Deployment';
|
||||
import { Deployer } from './Deployer';
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@ -49,20 +46,6 @@ export class Project {
|
||||
@Column('text', { default: '' })
|
||||
description!: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
auctionId!: string | null;
|
||||
|
||||
// Tx hash for sending coins from snowball to deployer
|
||||
@Column('varchar', { nullable: true })
|
||||
txHash!: string | null;
|
||||
|
||||
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
||||
@JoinTable()
|
||||
deployers!: Deployer[]
|
||||
|
||||
@Column('boolean', { default: false, nullable: true })
|
||||
fundsReleased!: boolean;
|
||||
|
||||
// TODO: Compute template & framework in import repository
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null;
|
||||
@ -70,10 +53,6 @@ export class Project {
|
||||
@Column('varchar', { nullable: true })
|
||||
framework!: string | null;
|
||||
|
||||
// Address of the user who created the project i.e. requested deployments
|
||||
@Column('varchar')
|
||||
paymentAddress!: string;
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
@ -82,6 +61,9 @@ export class Project {
|
||||
@Column('varchar')
|
||||
icon!: string;
|
||||
|
||||
@Column('varchar')
|
||||
subDomain!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -39,12 +39,6 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
subOrgId!: string;
|
||||
|
||||
@Column()
|
||||
turnkeyWalletId!: string;
|
||||
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'express-async-errors';
|
||||
import 'reflect-metadata';
|
||||
import debug from 'debug';
|
||||
import fs from 'fs';
|
||||
@ -10,6 +9,8 @@ import { Database } from './database';
|
||||
import { createAndStartServer } from './server';
|
||||
import { createResolvers } from './resolvers';
|
||||
import { getConfig } from './utils';
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { Service } from './service';
|
||||
import { Registry } from './registry';
|
||||
|
||||
@ -17,15 +18,16 @@ const log = debug('snowball:server');
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
const { server, database, gitHub, registryConfig } = await getConfig();
|
||||
// TODO: get config path using cli
|
||||
const { server, database, gitHub, registryConfig, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const app = new OAuthApp({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
clientId: gitHub.oAuth.clientId,
|
||||
clientSecret: gitHub.oAuth.clientSecret,
|
||||
clientSecret: gitHub.oAuth.clientSecret
|
||||
});
|
||||
|
||||
const db = new Database(database);
|
||||
const db = new Database(database, misc);
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
@ -33,7 +35,7 @@ export const main = async (): Promise<void> => {
|
||||
{ gitHubConfig: gitHub, registryConfig },
|
||||
db,
|
||||
app,
|
||||
registry,
|
||||
registry
|
||||
);
|
||||
|
||||
const typeDefs = fs
|
||||
|
@ -1,67 +1,55 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
ApplicationRecord,
|
||||
Deployment,
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationDeploymentRemovalRequest
|
||||
ApplicationDeploymentRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
||||
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||
const SLEEP_DURATION = 1000;
|
||||
|
||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
registryConfig.chainId
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord ({
|
||||
octokit,
|
||||
repository,
|
||||
appName,
|
||||
packageJSON,
|
||||
commitHash,
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
octokit: Octokit
|
||||
repository: string;
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||
// Use registry-sdk to publish record
|
||||
// Use laconic-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords(
|
||||
@ -94,7 +82,7 @@ export class Registry {
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: repo,
|
||||
name: appName,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
@ -107,158 +95,63 @@ export class Registry {
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationRecord,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
log(`Published application record ${result.id}`);
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(repo);
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||
// TODO: Discuss computation of CRN
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
await this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||
cid: result.data.id,
|
||||
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
return {
|
||||
applicationRecordId: result.id,
|
||||
applicationRecordId: result.data.id,
|
||||
applicationRecordData: applicationRecord
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<Deployment>,
|
||||
): Promise<{
|
||||
applicationDeploymentAuctionId: string;
|
||||
}> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
|
||||
await this.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
});
|
||||
|
||||
const lrn = this.getLrn(appName);
|
||||
const config = await getConfig();
|
||||
const auctionConfig = config.auction;
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auctionResult = await registryTransactionWithRetry(() =>
|
||||
this.registry.createProviderAuction(
|
||||
{
|
||||
commitFee: auctionConfig.commitFee,
|
||||
commitsDuration: auctionConfig.commitsDuration,
|
||||
revealFee: auctionConfig.revealFee,
|
||||
revealsDuration: auctionConfig.revealsDuration,
|
||||
denom: auctionConfig.denom,
|
||||
maxPrice: auctionParams.maxPrice,
|
||||
numProviders: auctionParams.numProviders,
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
if (!auctionResult.auction) {
|
||||
throw new Error('Error creating auction');
|
||||
}
|
||||
|
||||
// Create record of type applicationDeploymentAuction and publish
|
||||
const applicationDeploymentAuction = {
|
||||
application: lrn,
|
||||
auction: auctionResult.auction.id,
|
||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||
};
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentAuction,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||
log(`Application deployment auction record published: ${result.id}`);
|
||||
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||
|
||||
return {
|
||||
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
packageJsonName: string,
|
||||
repository: string,
|
||||
auctionId?: string | null,
|
||||
lrn: string,
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
payment?: string | null
|
||||
environmentVariables: { [key: string]: string }
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const lrn = this.getLrn(data.appName);
|
||||
const records = await this.registry.resolveNames([lrn]);
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
const applicationRecord = records[0];
|
||||
|
||||
if (!applicationRecord) {
|
||||
throw new Error(`No record found for ${lrn}`);
|
||||
throw new Error(`No record found for ${crn}`);
|
||||
}
|
||||
|
||||
// Create record of type ApplicationDeploymentRequest and publish
|
||||
@ -266,8 +159,11 @@ export class Registry {
|
||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
config: JSON.stringify({
|
||||
@ -279,83 +175,27 @@ export class Registry {
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
}),
|
||||
deployer: data.lrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
log(`Application deployment request record published: ${result.id}`);
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRequestId: result.id,
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
async getAuctionWinningDeployerRecords(
|
||||
auctionId: string
|
||||
): Promise<DeployerRecord[]> {
|
||||
const records = await this.registry.getAuctionsByIds([auctionId]);
|
||||
const auctionResult = records[0];
|
||||
|
||||
let deployerRecords = [];
|
||||
const { winnerAddresses } = auctionResult;
|
||||
|
||||
for (const auctionWinner of winnerAddresses) {
|
||||
const records = await this.getDeployerRecordsByFilter({
|
||||
paymentAddress: auctionWinner,
|
||||
});
|
||||
|
||||
const newRecords = records.filter(record => {
|
||||
return record.names !== null && record.names.length > 0;
|
||||
});
|
||||
|
||||
for (const record of newRecords) {
|
||||
if (record.id) {
|
||||
deployerRecords.push(record);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deployerRecords;
|
||||
}
|
||||
|
||||
async releaseDeployerFunds(
|
||||
auctionId: string
|
||||
): Promise<any> {
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auction = await registryTransactionWithRetry(() =>
|
||||
this.registry.releaseFunds(
|
||||
{
|
||||
auctionId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
return auction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
@ -371,165 +211,17 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
||||
// Filter records with ApplicationRecord ids
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
deployment.applicationRecordId === record.attributes.application
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch WebappDeployer Records by filter
|
||||
*/
|
||||
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
||||
...filter
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords by filter
|
||||
*/
|
||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||
...filter
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
*/
|
||||
async getDeploymentRemovalRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRemovalRecord[]> {
|
||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs
|
||||
return records.filter((record: AppDeploymentRemovalRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
|
||||
deployment.applicationDeploymentRecordId === record.attributes.deployment
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest(data: {
|
||||
deploymentId: string;
|
||||
deployerLrn: string;
|
||||
auctionId?: string | null;
|
||||
payment?: string | null;
|
||||
}): Promise<{
|
||||
applicationDeploymentRemovalRequestId: string;
|
||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||
}> {
|
||||
const applicationDeploymentRemovalRequest = {
|
||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
deployment: data.deploymentId,
|
||||
deployer: data.deployerLrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
log(`Application deployment removal request record published: ${result.id}`);
|
||||
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRemovalRequestId: result.id,
|
||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||
};
|
||||
}
|
||||
|
||||
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||
if (auctionIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
||||
|
||||
const completedAuctions = auctions
|
||||
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||
.map((auction: { id: string, status: string }) => auction.id);
|
||||
|
||||
return completedAuctions;
|
||||
}
|
||||
|
||||
async getRecordsByName(name: string): Promise<any> {
|
||||
return this.registry.resolveNames([name]);
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<any> {
|
||||
return this.registry.getAuctionsByIds([auctionId]);
|
||||
}
|
||||
|
||||
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: DeliverTxResponse =
|
||||
await registryTransactionWithRetry(() =>
|
||||
laconicClient.sendTokens(account.address, receiverAddress,
|
||||
[
|
||||
{
|
||||
denom: 'alnt',
|
||||
amount
|
||||
}
|
||||
],
|
||||
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
||||
);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
async getAccount(): Promise<Account> {
|
||||
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
||||
await account.init();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
getLrn(appName: string): string {
|
||||
getCrn (appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@ -22,19 +21,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getOrganizationsByUserId(context.user);
|
||||
},
|
||||
|
||||
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||
return service.getProjectById(context.user, projectId);
|
||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getProjectById(projectId);
|
||||
},
|
||||
|
||||
projectsInOrganization: async (
|
||||
_: any,
|
||||
{ organizationSlug }: { organizationSlug: string },
|
||||
context: any,
|
||||
) => {
|
||||
return service.getProjectsInOrganization(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
);
|
||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
||||
return service.getProjectsInOrganization(context.user, organizationSlug);
|
||||
},
|
||||
|
||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||
@ -43,7 +35,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
environmentVariables: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string },
|
||||
{ projectId }: { projectId: string }
|
||||
) => {
|
||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||
},
|
||||
@ -52,11 +44,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getProjectMembersByProjectId(projectId);
|
||||
},
|
||||
|
||||
searchProjects: async (
|
||||
_: any,
|
||||
{ searchText }: { searchText: string },
|
||||
context: any,
|
||||
) => {
|
||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
||||
return service.searchProjects(context.user, searchText);
|
||||
},
|
||||
|
||||
@ -64,37 +52,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
filter,
|
||||
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
|
||||
filter
|
||||
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
||||
) => {
|
||||
return service.getDomainsByProjectId(projectId, filter);
|
||||
},
|
||||
|
||||
getAuctionData: async (
|
||||
_: any,
|
||||
{ auctionId }: { auctionId: string },
|
||||
) => {
|
||||
return service.getAuctionData(auctionId);
|
||||
},
|
||||
|
||||
deployers: async (_: any, __: any, context: any) => {
|
||||
return service.getDeployers();
|
||||
},
|
||||
|
||||
address: async (_: any, __: any, context: any) => {
|
||||
return service.getAddress();
|
||||
},
|
||||
|
||||
verifyTx: async (
|
||||
_: any,
|
||||
{
|
||||
txHash,
|
||||
amount,
|
||||
senderAddress,
|
||||
}: { txHash: string; amount: string; senderAddress: string },
|
||||
) => {
|
||||
return service.verifyTx(txHash, amount, senderAddress);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
@ -102,13 +64,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
removeProjectMember: async (
|
||||
_: any,
|
||||
{ projectMemberId }: { projectMemberId: string },
|
||||
context: any,
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(
|
||||
context.user,
|
||||
projectMemberId,
|
||||
);
|
||||
return await service.removeProjectMember(context.user, projectMemberId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -119,13 +78,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
projectMemberId,
|
||||
data,
|
||||
data
|
||||
}: {
|
||||
projectMemberId: string;
|
||||
data: {
|
||||
permissions: Permission[];
|
||||
};
|
||||
},
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data);
|
||||
@ -139,14 +98,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
},
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data));
|
||||
@ -160,15 +119,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: { environments: string[]; key: string; value: string }[];
|
||||
},
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.addEnvironmentVariables(projectId, data),
|
||||
await service.addEnvironmentVariables(projectId, data)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
@ -180,16 +139,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
environmentVariableId,
|
||||
data,
|
||||
data
|
||||
}: {
|
||||
environmentVariableId: string;
|
||||
data: DeepPartial<EnvironmentVariable>;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(
|
||||
environmentVariableId,
|
||||
data,
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
@ -199,7 +158,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
removeEnvironmentVariable: async (
|
||||
_: any,
|
||||
{ environmentVariableId }: { environmentVariableId: string },
|
||||
{ environmentVariableId }: { environmentVariableId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||
@ -212,76 +171,26 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
updateDeploymentToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any,
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.updateDeploymentToProd(context.user, deploymentId),
|
||||
);
|
||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addProjectFromTemplate: async (
|
||||
_: any,
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: AddProjectFromTemplateInput;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams;
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
return await service.addProjectFromTemplate(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
addProject: async (
|
||||
_: any,
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: DeepPartial<Project>;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams,
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
context: any,
|
||||
data
|
||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
@ -290,7 +199,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
updateProject: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
|
||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProject(projectId, data);
|
||||
@ -303,12 +212,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
redeployToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any,
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.redeployToProd(context.user, deploymentId),
|
||||
);
|
||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -337,8 +244,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
deploymentId,
|
||||
}: { deploymentId: string; projectId: string },
|
||||
deploymentId
|
||||
}: { deploymentId: string; projectId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.rollbackDeployment(projectId, deploymentId);
|
||||
@ -348,21 +255,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
deleteDeployment: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
) => {
|
||||
try {
|
||||
return await service.deleteDeployment(deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addDomain: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: { name: string } },
|
||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addDomain(projectId, data));
|
||||
@ -374,7 +269,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
updateDomain: async (
|
||||
_: any,
|
||||
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
|
||||
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateDomain(domainId, data);
|
||||
@ -387,7 +282,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
authenticateGitHub: async (
|
||||
_: any,
|
||||
{ code }: { code: string },
|
||||
context: any,
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.authenticateGitHub(code, context.user);
|
||||
@ -399,14 +294,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.user, {
|
||||
gitHubToken: null,
|
||||
});
|
||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,94 +1,29 @@
|
||||
import { Router } from 'express';
|
||||
import { SiweMessage } from 'siwe';
|
||||
import { Service } from '../service';
|
||||
import { authenticateUser, createUser } from '../turnkey-backend';
|
||||
import { SiweMessage, generateNonce } from 'siwe';
|
||||
|
||||
const router = Router();
|
||||
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
router.get('/registration/:email', async (req, res) => {
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEmail(req.params.email);
|
||||
if (user) {
|
||||
return res.send({ subOrganizationId: user?.subOrgId });
|
||||
} else {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
router.get('/nonce', async (_, res) => {
|
||||
res.send(generateNonce());
|
||||
});
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
console.log('Register', req.body);
|
||||
const { email, challenge, attestation } = req.body;
|
||||
const user = await createUser(req.app.get('service'), {
|
||||
challenge,
|
||||
attestation,
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0],
|
||||
});
|
||||
req.session.address = user.id;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.post('/authenticate', async (req, res) => {
|
||||
console.log('Authenticate', req.body);
|
||||
const { signedWhoamiRequest } = req.body;
|
||||
const user = await authenticateUser(
|
||||
req.app.get('service'),
|
||||
signedWhoamiRequest,
|
||||
);
|
||||
if (user) {
|
||||
req.session.address = user.id;
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// SIWE Auth
|
||||
//
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature } = req.body;
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature,
|
||||
signature
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
return res.send({ success });
|
||||
}
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEthAddress(data.address);
|
||||
|
||||
if (!user) {
|
||||
const newUser = await service.createUser({
|
||||
ethAddress: data.address,
|
||||
email: `${data.address}@example.com`,
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: '',
|
||||
});
|
||||
|
||||
// SIWESession from the web3modal library requires both address and chain ID
|
||||
req.session.address = newUser.id;
|
||||
req.session.chainId = data.chainId;
|
||||
} else {
|
||||
req.session.address = user.id;
|
||||
if (success) {
|
||||
req.session.address = data.address;
|
||||
req.session.chainId = data.chainId;
|
||||
}
|
||||
|
||||
res.send({ success });
|
||||
});
|
||||
|
||||
//
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({
|
||||
address: req.session.address,
|
||||
chainId: req.session.chainId
|
||||
});
|
||||
res.send({ address: req.session.address, chainId: req.session.chainId });
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/version', async (req, res) => {
|
||||
return res.send({ version: '0.0.9' });
|
||||
});
|
||||
|
||||
export default router;
|
@ -19,14 +19,6 @@ enum DeploymentStatus {
|
||||
Building
|
||||
Ready
|
||||
Error
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
@ -72,13 +64,8 @@ type Project {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployers: [Deployer!]
|
||||
auctionId: String
|
||||
fundsReleased: Boolean
|
||||
template: String
|
||||
framework: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
webhooks: [String!]
|
||||
members: [ProjectMember!]
|
||||
environmentVariables: [EnvironmentVariable!]
|
||||
@ -86,7 +73,7 @@ type Project {
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
baseDomains: [String!]
|
||||
subDomain: String
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
@ -106,10 +93,7 @@ type Deployment {
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployer: Deployer
|
||||
applicationDeploymentRequestId: String
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
@ -135,17 +119,6 @@ type EnvironmentVariable {
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployer {
|
||||
deployerLrn: String!
|
||||
deployerId: String!
|
||||
deployerApiUrl: String!
|
||||
minimumPayment: String
|
||||
paymentAddress: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
baseDomain: String
|
||||
}
|
||||
|
||||
type AuthResult {
|
||||
token: String!
|
||||
}
|
||||
@ -156,23 +129,11 @@ input AddEnvironmentVariableInput {
|
||||
value: String!
|
||||
}
|
||||
|
||||
input AddProjectFromTemplateInput {
|
||||
templateOwner: String!
|
||||
templateRepo: String!
|
||||
owner: String!
|
||||
name: String!
|
||||
isPrivate: Boolean!
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input AddProjectInput {
|
||||
name: String!
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
template: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
@ -212,48 +173,6 @@ input FilterDomainsInput {
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String,
|
||||
numProviders: Int,
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
@ -264,11 +183,7 @@ type Query {
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
deployers: [Deployer]
|
||||
address: String!
|
||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -288,26 +203,12 @@ type Mutation {
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
deleteDomain(domainId: String!): Boolean!
|
||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||
deleteDeployment(deploymentId: String!): Boolean!
|
||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||
authenticateGitHub(code: String!): AuthResult!
|
||||
|
@ -6,7 +6,7 @@ import { createServer } from 'http';
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError,
|
||||
AuthenticationError
|
||||
} from 'apollo-server-core';
|
||||
import session from 'express-session';
|
||||
|
||||
@ -17,14 +17,10 @@ import { ServerConfig } from './config';
|
||||
import { DEFAULT_GQL_PATH } from './constants';
|
||||
import githubRouter from './routes/github';
|
||||
import authRouter from './routes/auth';
|
||||
import stagingRouter from './routes/staging';
|
||||
import { Service } from './service';
|
||||
|
||||
const log = debug('snowball:server');
|
||||
|
||||
// Set cookie expiration to 1 month in milliseconds
|
||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
address: string;
|
||||
@ -36,7 +32,7 @@ export const createAndStartServer = async (
|
||||
serverConfig: ServerConfig,
|
||||
typeDefs: TypeSource,
|
||||
resolvers: any,
|
||||
service: Service,
|
||||
service: Service
|
||||
): Promise<ApolloServer> => {
|
||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
||||
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
|
||||
@ -49,7 +45,7 @@ export const createAndStartServer = async (
|
||||
// Create the schema
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
resolvers
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
@ -64,24 +60,24 @@ export const createAndStartServer = async (
|
||||
throw new AuthenticationError('Unauthorized: No active session');
|
||||
}
|
||||
|
||||
const user = await service.getUser(address);
|
||||
// Find/create user from ETH address in request session
|
||||
const user = await service.loadOrCreateUser(address);
|
||||
|
||||
return { user };
|
||||
},
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
|
||||
],
|
||||
ApolloServerPluginLandingPageLocalDefault({ embed: true })
|
||||
]
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
app.use(cors({
|
||||
origin: appOriginUrl,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: secret,
|
||||
@ -89,12 +85,16 @@ export const createAndStartServer = async (
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
domain: domain || undefined,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||
// TODO: Set cookie maxAge and handle cookie expiry in frontend
|
||||
// maxAge: SESSION_COOKIE_MAX_AGE,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
|
||||
}
|
||||
};
|
||||
|
||||
if (domain) {
|
||||
sessionOptions.cookie!.domain = domain;
|
||||
}
|
||||
|
||||
if (trustProxy) {
|
||||
// trust first proxy
|
||||
app.set('trust proxy', 1);
|
||||
@ -109,8 +109,8 @@ export const createAndStartServer = async (
|
||||
path: gqlPath,
|
||||
cors: {
|
||||
origin: [appOriginUrl],
|
||||
credentials: true,
|
||||
},
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
@ -118,12 +118,6 @@ export const createAndStartServer = async (
|
||||
app.set('service', service);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/api/github', githubRouter);
|
||||
app.use('/staging', stagingRouter);
|
||||
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
});
|
||||
|
||||
httpServer.listen(port, host, () => {
|
||||
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
|
||||
|
@ -1,130 +0,0 @@
|
||||
import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server';
|
||||
|
||||
// Default path for the first Ethereum address in a new HD wallet.
|
||||
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
// - Purpose is a constant set to 44' following the BIP43 recommendation.
|
||||
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
// - Account, Change, and Address Index are set to 0
|
||||
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server';
|
||||
import { getConfig } from './utils';
|
||||
import { Service } from './service';
|
||||
|
||||
type TAttestation = TurnkeyApiTypes['v1Attestation'];
|
||||
|
||||
type CreateUserParams = {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
challenge: string;
|
||||
attestation: TAttestation;
|
||||
};
|
||||
|
||||
export async function createUser(
|
||||
service: Service,
|
||||
{ userName, userEmail, challenge, attestation }: CreateUserParams,
|
||||
) {
|
||||
try {
|
||||
if (await service.getUserByEmail(userEmail)) {
|
||||
throw new Error(`User already exists: ${userEmail}`);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const turnkey = new Turnkey(config.turnkey);
|
||||
|
||||
const apiClient = turnkey.api();
|
||||
|
||||
const walletName = `Default ETH Wallet`;
|
||||
|
||||
const createSubOrgResponse = await apiClient.createSubOrganization({
|
||||
subOrganizationName: `Default SubOrg for ${userEmail}`,
|
||||
rootQuorumThreshold: 1,
|
||||
rootUsers: [
|
||||
{
|
||||
userName,
|
||||
userEmail,
|
||||
apiKeys: [],
|
||||
authenticators: [
|
||||
{
|
||||
authenticatorName: 'Passkey',
|
||||
challenge,
|
||||
attestation,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
wallet: {
|
||||
walletName: walletName,
|
||||
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
|
||||
},
|
||||
});
|
||||
|
||||
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId);
|
||||
const wallet = refineNonNull(createSubOrgResponse.wallet);
|
||||
|
||||
const result = {
|
||||
id: wallet.walletId,
|
||||
address: wallet.addresses[0],
|
||||
subOrgId: subOrgId,
|
||||
};
|
||||
console.log('Turnkey success', result);
|
||||
|
||||
const user = await service.createUser({
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
subOrgId,
|
||||
ethAddress: wallet.addresses[0],
|
||||
turnkeyWalletId: wallet.walletId,
|
||||
});
|
||||
console.log('New user', user);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to create user:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
service: Service,
|
||||
signedWhoamiRequest: {
|
||||
url: string;
|
||||
body: any;
|
||||
stamp: {
|
||||
stampHeaderName: string;
|
||||
stampHeaderValue: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const tkRes = await fetch(signedWhoamiRequest.url, {
|
||||
method: 'POST',
|
||||
body: signedWhoamiRequest.body,
|
||||
headers: {
|
||||
[signedWhoamiRequest.stamp.stampHeaderName]:
|
||||
signedWhoamiRequest.stamp.stampHeaderValue,
|
||||
},
|
||||
});
|
||||
console.log('AUTH RESULT', tkRes.status);
|
||||
if (tkRes.status !== 200) {
|
||||
console.log(await tkRes.text());
|
||||
return null;
|
||||
}
|
||||
const orgId = (await tkRes.json()).organizationId;
|
||||
const user = await service.getUserBySubOrgId(orgId);
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to authenticate:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function refineNonNull<T>(
|
||||
input: T | null | undefined,
|
||||
errorMessage?: string,
|
||||
): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
@ -24,13 +24,10 @@ export interface GitPushEventPayload {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string;
|
||||
auction: string;
|
||||
deployer: string;
|
||||
dns: string;
|
||||
meta: string;
|
||||
name: string;
|
||||
@ -40,13 +37,6 @@ export interface AppDeploymentRecordAttributes {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecordAttributes {
|
||||
deployment: string;
|
||||
request: string;
|
||||
type: 'ApplicationDeploymentRemovalRecord';
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface RegistryRecord {
|
||||
id: string;
|
||||
names: string[] | null;
|
||||
@ -59,46 +49,3 @@ interface RegistryRecord {
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRemovalRecordAttributes;
|
||||
}
|
||||
|
||||
export interface AddProjectFromTemplateInput {
|
||||
templateOwner: string;
|
||||
templateRepo: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
paymentAddress: string;
|
||||
txHash: string;
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string,
|
||||
numProviders: number,
|
||||
}
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
environments: string[],
|
||||
key: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export interface DeployerRecord {
|
||||
id: string;
|
||||
names: string[];
|
||||
owners: string[];
|
||||
bondId: string;
|
||||
createTime: string;
|
||||
expiryTime: string;
|
||||
attributes: {
|
||||
apiUrl: string;
|
||||
minimumPayment: string | null;
|
||||
name: string;
|
||||
paymentAddress: string;
|
||||
publicKey: string;
|
||||
type: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
@ -1,24 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import fs from 'fs-extra';
|
||||
import { Octokit } from 'octokit';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
export async function getConfig() {
|
||||
// TODO: get config path using cli
|
||||
return await _getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
}
|
||||
|
||||
const _getConfig = async <ConfigType>(
|
||||
configFile: string,
|
||||
export const getConfig = async <ConfigType>(
|
||||
configFile: string
|
||||
): Promise<ConfigType> => {
|
||||
const configFilePath = path.resolve(configFile);
|
||||
const fileExists = await fs.pathExists(configFilePath);
|
||||
@ -52,7 +41,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
dataSource: DataSource,
|
||||
entities: any,
|
||||
relations?: any | undefined,
|
||||
relations?: any | undefined
|
||||
): Promise<Entity[]> => {
|
||||
const entityRepository = dataSource.getRepository(entityType);
|
||||
|
||||
@ -67,7 +56,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
entity = {
|
||||
...entity,
|
||||
[field]: relations[field][entityData[valueIndex]],
|
||||
[field]: relations[field][entityData[valueIndex]]
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -77,67 +66,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
return savedEntity;
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const getRepoDetails = async (
|
||||
octokit: Octokit,
|
||||
repository: string,
|
||||
commitHash: string | undefined,
|
||||
): Promise<{
|
||||
repo: string;
|
||||
packageJSON: PackageJSON;
|
||||
repoUrl: string;
|
||||
}> => {
|
||||
const [owner, repo] = repository.split('/');
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: commitHash,
|
||||
});
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found');
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
return {
|
||||
repo,
|
||||
packageJSON,
|
||||
repoUrl
|
||||
};
|
||||
}
|
||||
|
||||
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
||||
export const registryTransactionWithRetry = async (
|
||||
txMethod: () => Promise<any>
|
||||
): Promise<any> => {
|
||||
try {
|
||||
return await txMethod();
|
||||
} catch (error: any) {
|
||||
if (!error.message.includes('account sequence mismatch')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
|
||||
|
||||
try {
|
||||
return await txMethod();
|
||||
} catch (retryError: any) {
|
||||
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import * as fs from 'fs/promises';
|
||||
import debug from 'debug';
|
||||
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:delete-database');
|
||||
|
||||
@ -11,7 +13,7 @@ const deleteFile = async (filePath: string) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig();
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
@ -1,7 +1,12 @@
|
||||
[
|
||||
{
|
||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||
"name": "Deploy Tools",
|
||||
"slug": "deploy-tools"
|
||||
"name": "Snowball Tools",
|
||||
"slug": "snowball-tools-1"
|
||||
},
|
||||
{
|
||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||
"name": "Laconic",
|
||||
"slug": "laconic-2"
|
||||
}
|
||||
]
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:initialize-database');
|
||||
|
||||
@ -154,7 +156,7 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig();
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||
|
||||
if (!isDbPresent) {
|
||||
|
@ -1,40 +1,39 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { Config } from '../src/config';
|
||||
import { getConfig } from '../src/utils';
|
||||
|
||||
const log = debug('snowball:initialize-registry');
|
||||
|
||||
const DENOM = 'alnt';
|
||||
const DENOM = 'aphoton';
|
||||
const BOND_AMOUNT = '1000000000';
|
||||
|
||||
async function main () {
|
||||
const { registryConfig } = await getConfig();
|
||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
log(`Bond ${bondId} set for authority ${name}`);
|
||||
}
|
||||
|
@ -2,20 +2,22 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deploy-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database, misc } = await getConfig();
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
registryConfig.chainId
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
@ -38,7 +40,7 @@ async function main() {
|
||||
});
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
||||
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||
|
||||
const applicationDeploymentRecord = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
@ -59,8 +61,6 @@ async function main() {
|
||||
url
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
@ -68,26 +68,11 @@ async function main() {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
);
|
||||
}
|
||||
|
||||
log('Application deployment record data:', applicationDeploymentRecord);
|
||||
log(`Application deployment record published: ${result.id}`);
|
||||
log(`Application deployment record published: ${result.data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,67 +0,0 @@
|
||||
import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deployment-removal-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database } = await getConfig();
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: database.dbPath,
|
||||
synchronize: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
|
||||
const deploymentRepository = dataSource.getRepository(Deployment);
|
||||
const deployments = await deploymentRepository.find({
|
||||
relations: {
|
||||
project: true
|
||||
},
|
||||
where: {
|
||||
status: DeploymentStatus.Deleting
|
||||
}
|
||||
});
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const applicationDeploymentRemovalRecord = {
|
||||
type: "ApplicationDeploymentRemovalRecord",
|
||||
version: "1.0.0",
|
||||
deployment: deployment.applicationDeploymentRecordId,
|
||||
request: deployment.applicationDeploymentRemovalRequestId,
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
);
|
||||
|
||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||
log(`Application deployment removal record published: ${result.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err);
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
REGISTRY_BOND_ID=
|
||||
DEPLOYER_LRN=
|
||||
AUTHORITY=
|
1
packages/deployer/.gitignore
vendored
@ -1 +0,0 @@
|
||||
records/*.yml
|
@ -1,26 +1,56 @@
|
||||
# deployer
|
||||
|
||||
- Install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
- Replace variables in the following files
|
||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
|
||||
```yml
|
||||
# Example
|
||||
record:
|
||||
...
|
||||
meta:
|
||||
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
|
||||
...
|
||||
```
|
||||
|
||||
- Update record version in [records/application-record.yml](records/application-record.yml)
|
||||
```yml
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: <NEW_VERSION>
|
||||
...
|
||||
```
|
||||
|
||||
- Update commit hash in the following places:
|
||||
- [records/application-record.yml](records/application-record.yml)
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
repository_ref: <COMMIT_HASH>
|
||||
...
|
||||
```
|
||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
meta:
|
||||
...
|
||||
repository_ref: <COMMIT_HASH>
|
||||
```
|
||||
- [deploy-frontend.sh](deploy-frontend.sh)
|
||||
```bash
|
||||
brew install jq # if you do not have jq installed already
|
||||
...
|
||||
RCD_APP_VERSION="0.1.0"
|
||||
REPO_REF="<COMMIT_HASH>"
|
||||
...
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
|
||||
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
|
||||
@ -29,7 +59,6 @@
|
||||
## Notes
|
||||
|
||||
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
@ -38,27 +67,20 @@
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
...
|
||||
```
|
||||
|
||||
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
||||
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Check balances
|
||||
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic registry account get
|
||||
yarn laconic cns account get
|
||||
|
||||
# Bond balance
|
||||
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
|
||||
```
|
@ -1,10 +0,0 @@
|
||||
services:
|
||||
registry:
|
||||
restEndpoint: 'http://console.laconic.com:1317'
|
||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||
chainId: laconic_9000-1
|
||||
gas:
|
||||
fees:
|
||||
gasPrice: 1
|
@ -1,8 +1,9 @@
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey:
|
||||
bondId:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
chainId: laconic_9000-1
|
||||
gas: 1000000
|
||||
fees: 200000aphoton
|
||||
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
|
@ -1,156 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash for a branch
|
||||
BRANCH_NAME="main"
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||
|
||||
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
CONFIG_FILE=config.yml
|
||||
RCD_APP_VERSION="0.1.0"
|
||||
REPO_REF="353fd0f6219a955be186b5914e93baa9058b4473"
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${RCD_APP_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${REPO_REF}" "$RECORD_ID"
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
|
||||
echo "Paying address: $paymentAddress with amount $paymentAmount..."
|
||||
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy.laconic.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
@ -1,149 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
@ -4,6 +4,6 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||
"@cerc-io/laconic-registry-cli": "^0.1.10"
|
||||
}
|
||||
}
|
||||
|
21
packages/deployer/records/application-deployment-request.yml
Normal file
@ -0,0 +1,21 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@0.1.0
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.0
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
# If GitHub client ID is changed, same ID and corresponding secret has to be set in backend config
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: 1ed99146e0d07aa62d44
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: deep-stack/test-progressive-web-app
|
||||
# New config env after changes for image upload PWA
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: deep-stack/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: deep-stack/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
# Set CURRENT_DATE_TIME; Use command date -u
|
||||
note: Added by Snowball @ <CURRENT_DATE_TIME>
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 353fd0f6219a955be186b5914e93baa9058b4473
|
10
packages/deployer/records/application-record.yml
Normal file
@ -0,0 +1,10 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.5
|
||||
repository_ref: 353fd0f6219a955be186b5914e93baa9058b4473
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
# name is set to repo name
|
||||
name: snowballtools-base-frontend
|
||||
# app_version is set from package.json
|
||||
app_version: 0.1.0
|
@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=records/application-deployment-removal-request.yml
|
||||
# TODO: Pass deployment record ID as arg
|
||||
DEPLOYMENT_RECORD_ID=bafyreidjho77xeczaqpyawhc4wbpm5it5atibtuxk6ost6vnpu2svlp3ka
|
||||
|
||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployer: $DEPLOYER_LRN
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deployment removal successful"
|
@ -1,23 +0,0 @@
|
||||
# deployer test
|
||||
|
||||
Check if the live web app deployer is in a working state
|
||||
|
||||
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
|
||||
- Config used: [../config.yml](../config.yml)
|
||||
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
||||
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
||||
- Fetch the latest version of `deployment-test-app` from registry and increment `ApplicationRecord` version
|
||||
- Publish the resulting `ApplicationRecord` record
|
||||
- Set names to the record and check name resolution
|
||||
- Publish the `ApplicationDeploymentRequest` record
|
||||
- Check that the deployment occurs
|
||||
- Check that a `ApplicationDeploymentRecord` is created
|
||||
- Check that the deployment record has correct `ApplicationRecord` id
|
||||
- Check that the URL present in deployment record is active
|
||||
- Create and publish a [`ApplicationDeploymentRemovalRequest`](./records/application-deployment-removal-request.yml) record
|
||||
- Check that the deployment is removed
|
||||
- Check that a `ApplicationDeploymentRemovalRecord` is created
|
||||
- Check that the deployment URL goes down
|
||||
- The test script is run in a GitHub CI [workflow](../../../.github/workflows/test-app-deployment.yaml) that:
|
||||
- Is scheduled to run everyday on the default (`main`) branch or can be triggered manually
|
||||
- Sends Slack alerts to configured channels on failure
|
@ -1,4 +0,0 @@
|
||||
record:
|
||||
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
@ -1,15 +0,0 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: "1.0.0"
|
||||
name: deployment-test-app@0.1.24
|
||||
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
||||
repository: "https://github.com/snowball-tools/test-progressive-web-app"
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
@ -1,8 +0,0 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.1
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
|
||||
app_type: webapp
|
||||
name: deployment-test-app
|
||||
app_version: 0.1.24
|
@ -1,225 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Fetch the package.json file content
|
||||
# Extract version from package.json content
|
||||
package_json=$(wget -qO- "$REPO_URL/raw/$LATEST_HASH/package.json")
|
||||
PACKAGE_VERSION=$(echo "$package_json" | jq -r '.version')
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=packages/deployer/config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
APP_NAME=deployment-test-app
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
RECORD_FILE=packages/deployer/test/records/application-record.yml
|
||||
|
||||
cat >$RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: $APP_NAME
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
|
||||
|
||||
cat >$REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: $APP_NAME@$PACKAGE_VERSION
|
||||
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
echo "Record files generated successfully."
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record exists for REGISTRY_APP_LRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that a ApplicationDeploymentRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_deployment_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRecord for deployment request $DEPLOYMENT_REQUEST_ID not found, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRecord found"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
DEPLOYMENT_RECORD_ID=$(echo $deployment_records_response | jq -r '.[0].id')
|
||||
echo $DEPLOYMENT_RECORD_ID
|
||||
|
||||
# Check if ApplicationDeploymentRecord has the correct record id
|
||||
fetched_application_record_id=$(echo $deployment_records_response | jq -r '.[0].attributes.application')
|
||||
if [ "$fetched_application_record_id" = "$RECORD_ID" ]; then
|
||||
echo "ApplicationRecord id matched"
|
||||
else
|
||||
echo "ApplicationRecord id does not match, expected: $RECORD_ID, received: $fetched_application_record_id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the url present in ApplicationDeploymentRecord is active
|
||||
fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
||||
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=10
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "200" ]; then
|
||||
echo "Deployment URL $fetched_url is active"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is not active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
|
||||
|
||||
cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the application url is down after deployment removal
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=5
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "404" ]; then
|
||||
echo "Deployment URL $fetched_url is down"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is still active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Test successful"
|
7
packages/frontend/.env
Normal file
@ -0,0 +1,7 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
@ -1,18 +1,7 @@
|
||||
VITE_SERVER_URL='http://localhost:8000'
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
VITE_GITHUB_CLIENT_ID=
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
VITE_WALLET_CONNECT_ID=
|
||||
|
||||
VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
|
||||
VITE_LACONICD_CHAIN_ID=
|
||||
VITE_WALLET_IFRAME_URL=
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
1
packages/frontend/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
build
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
25
packages/frontend/.eslintrc.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 13,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
1
packages/frontend/.gitignore
vendored
@ -22,4 +22,3 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
@ -1 +0,0 @@
|
||||
v20.12.1
|
@ -1 +1,3 @@
|
||||
dist/
|
||||
# artifacts
|
||||
build
|
||||
coverage
|
||||
|
@ -1,33 +0,0 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, 'package.json')));
|
||||
}
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
getAbsolutePath('@storybook/addon-onboarding'),
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
getAbsolutePath('storybook-addon-remix-react-router'),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath('@storybook/react-vite'),
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
staticDirs: ['../public'],
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,16 +0,0 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '../src/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
@ -1,63 +1,46 @@
|
||||
# frontend
|
||||
# Getting Started with Create React App
|
||||
|
||||
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using [typescript-tailwindcss-eslint-prettier](https://github.com/cufarvid/cra-templates) template.
|
||||
|
||||
## Getting Started
|
||||
## Available Scripts
|
||||
|
||||
### Install dependencies
|
||||
In the project directory, you can run:
|
||||
|
||||
In the root of the project, run:
|
||||
### `yarn start`
|
||||
|
||||
```zsh
|
||||
yarn
|
||||
```
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
### Build backend
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
```zsh
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
### `yarn test`
|
||||
|
||||
### Environment variables
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
#### Local
|
||||
### `yarn build`
|
||||
|
||||
Copy the `.env.example` file to `.env`:
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
```zsh
|
||||
cp .env.example .env
|
||||
```
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
#### Staging environment variables
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
|
||||
### `yarn eject`
|
||||
|
||||
#### Production environment variables
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
### Run development server
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
```zsh
|
||||
yarn dev
|
||||
```
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Deployment
|
||||
## Learn More
|
||||
|
||||
From the root of the project,
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
### Staging
|
||||
|
||||
```zsh
|
||||
cd packages/deployer && ./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```zsh
|
||||
cd packages/deployer && ./deploy-frontend.sh
|
||||
```
|
||||
|
||||
### Deployment status
|
||||
|
||||
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"projectId": "Project:663d04870db27ed66a48e466",
|
||||
"zip": true
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark dark:bg-background dark:text-foreground">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="laconic tools dashboard" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Laconic</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
7
packages/frontend/logo.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="500" height="500" fill="#0F86F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -1,57 +1,36 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bugsnag/browser-performance": "^2.4.1",
|
||||
"@bugsnag/js": "^7.22.7",
|
||||
"@bugsnag/plugin-react": "^7.22.7",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@mui/material": "^6.1.3",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/types": "^0.2.0",
|
||||
"@snowballtools/utils": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@turnkey/http": "^2.10.0",
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.16.1",
|
||||
"@web3modal/siwe": "4.0.5",
|
||||
"@web3modal/wagmi": "4.0.5",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.3.2",
|
||||
"framer-motion": "^11.0.8",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"gql-client": "^1.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"luxon": "^3.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
@ -61,44 +40,57 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropdown": "^1.11.0",
|
||||
"react-hook-form": "^7.49.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-oauth-popup": "^1.0.5",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "2.1.4",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "2.5.7",
|
||||
"wagmi": "^2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.3.3",
|
||||
"@storybook/addon-essentials": "^8.0.10",
|
||||
"@storybook/addon-interactions": "^8.0.10",
|
||||
"@storybook/addon-links": "^8.0.10",
|
||||
"@storybook/addon-onboarding": "^8.0.10",
|
||||
"@storybook/blocks": "^8.0.10",
|
||||
"@storybook/react": "^8.0.10",
|
||||
"@storybook/react-vite": "^8.0.10",
|
||||
"@storybook/test": "^8.0.10",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chromatic": "^11.3.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"postcss": "^8.4.38",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "^3.1.0",
|
||||
"storybook": "^8.0.10",
|
||||
"storybook-addon-remix-react-router": "^3.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.0"
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -1 +0,0 @@
|
||||
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
BIN
packages/frontend/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
packages/frontend/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packages/frontend/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
@ -1,3 +0,0 @@
|
||||
<svg width="197" height="2" viewBox="0 0 197 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="1.19141" x2="197" y2="1.19141" stroke="#94A7B8" stroke-dasharray="1 12"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 196 B |
BIN
packages/frontend/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 674 B |
BIN
packages/frontend/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 989 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 7.2 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="4" fill="#29292E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0494 24.6233C18.8425 21.8302 20.5713 17.973 20.5706 13.7142C20.5717 13.1361 20.5396 12.5645 20.4762 12L12 12.0008L12.0003 28.2867C11.9996 30.2608 12.7522 32.2356 14.2578 33.7411C15.7633 35.2466 17.7395 36.0001 19.7139 35.9991L19.7134 35.9996L36 36L35.9995 27.5227C35.4362 27.4605 34.8645 27.4285 34.2852 27.4284C30.0275 27.4289 26.1701 29.1577 23.377 31.9507C21.3446 33.9321 18.0858 33.9325 16.0785 31.9252C14.0722 29.9191 14.0715 26.6593 16.0494 24.6233ZM34.2419 13.7624C31.9012 11.4217 28.0982 11.4208 25.7566 13.7624C23.4151 16.1038 23.4159 19.9067 25.7566 22.2473C28.0986 24.5892 31.9004 24.5889 34.2419 22.2473C36.5835 19.9059 36.5839 16.1042 34.2419 13.7624Z" fill="#FBFBFB"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 892 B |
@ -1,10 +1 @@
|
||||
<svg width="115" height="20" viewBox="0 0 115 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L6.42729 19.9997L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z" fill="#FBFBFB"/>
|
||||
<path d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z" fill="#FBFBFB"/>
|
||||
<path d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z" fill="#FBFBFB"/>
|
||||
<path d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3813 1.07196 60.6967 1.07196C56.8169 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.8169 18.9291 60.6967 18.9291C64.3813 18.9291 65.9636 16.8901 65.9853 12.1468H62.9508C62.9292 15.8599 62.474 16.7828 60.6967 16.7828C58.6593 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.6809 3.19678 60.6967 3.21823C62.474 3.21823 62.9292 4.18413 62.9292 8.06885Z" fill="#FBFBFB"/>
|
||||
<path d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z" fill="#FBFBFB"/>
|
||||
<path d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z" fill="#FBFBFB"/>
|
||||
<path d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z" fill="#FBFBFB"/>
|
||||
<path d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z" fill="#FBFBFB"/>
|
||||
</svg>
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
packages/frontend/public/mstile-144x144.png
Normal file
After Width: | Height: | Size: 3.2 KiB |