Compare commits

..

1 Commits

Author SHA1 Message Date
Vivian Phung
b7f118bed6
Update local.toml.example in packages/backend 2024-06-24 19:41:51 -04:00
179 changed files with 6040 additions and 6142 deletions

View File

@ -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

View File

@ -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

View File

@ -14,18 +14,17 @@ VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid' VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo' 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_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_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_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key' VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid' VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url' VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
EOF EOF
yarn || exit 1 yarn || exit 1
yarn build --ignore backend || exit 1 yarn build || exit 1
if [[ ! -d "$OUTPUT_DIR" ]]; then if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "Missing output directory: $OUTPUT_DIR" 1>&2 echo "Missing output directory: $OUTPUT_DIR" 1>&2

View File

@ -22,10 +22,10 @@ yarn build --ignore frontend
#### Local #### Local
Copy the `environments/local.toml.example` file to `environments/local.toml`: Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
```zsh ```zsh
cp environments/local.toml.example environments/local.toml cp envionments/local.toml.example envionments/local.toml
``` ```
#### Staging environment variables #### Staging environment variables
@ -44,7 +44,7 @@ yarn start
## Deployment ## Deployment
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments): Clone the deployer repository:
```zsh ```zsh
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git

View File

@ -4,11 +4,8 @@
gqlPath = "/graphql" gqlPath = "/graphql"
[server.session] [server.session]
secret = "" secret = ""
# Frontend webapp URL origin
appOriginUrl = "http://localhost:3000" appOriginUrl = "http://localhost:3000"
# Set to true if server running behind proxy
trustProxy = false trustProxy = false
# Backend URL hostname
domain = "localhost" domain = "localhost"
[database] [database]
@ -20,9 +17,18 @@
clientId = "" clientId = ""
clientSecret = "" clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig] [registryConfig]
fetchDeploymentRecordDelay = 5000 fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000
restEndpoint = "http://localhost:1317" restEndpoint = "http://localhost:1317"
gqlEndpoint = "http://localhost:9473/api" gqlEndpoint = "http://localhost:9473/api"
chainId = "laconic_9000-1" chainId = "laconic_9000-1"
@ -30,14 +36,9 @@
bondId = "" bondId = ""
authority = "" authority = ""
[registryConfig.fee] [registryConfig.fee]
gas = "" amount = "200000"
fees = "" denom = "aphoton"
gasPrice = "1alnt" gas = "750000"
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions [misc]
[auction] projectDomain = "apps.snowballtools.com"
commitFee = "100000"
commitsDuration = "120s"
revealFee = "100000"
revealsDuration = "120s"
denom = "alnt"

View File

@ -4,10 +4,10 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@cerc-io/registry-sdk": "^0.2.11",
"@graphql-tools/schema": "^10.0.2", "@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12", "@graphql-tools/utils": "^10.0.12",
"@octokit/oauth-app": "^6.1.0", "@octokit/oauth-app": "^6.1.0",
"@snowballtools/laconic-sdk": "^0.1.17",
"@turnkey/sdk-server": "^0.1.0", "@turnkey/sdk-server": "^0.1.0",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",

View File

@ -34,21 +34,16 @@ export interface RegistryConfig {
privateKey: string; privateKey: string;
bondId: string; bondId: string;
fetchDeploymentRecordDelay: number; fetchDeploymentRecordDelay: number;
checkAuctionStatusDelay: number;
authority: string; authority: string;
fee: { fee: {
amount: string;
denom: string;
gas: string; gas: string;
fees: string;
gasPrice: string;
}; };
} }
export interface AuctionConfig { export interface MiscConfig {
commitFee: string; projectDomain: string;
commitsDuration: string;
revealFee: string;
revealsDuration: string;
denom: string;
} }
export interface Config { export interface Config {
@ -56,7 +51,7 @@ export interface Config {
database: DatabaseConfig; database: DatabaseConfig;
gitHub: GitHubConfig; gitHub: GitHubConfig;
registryConfig: RegistryConfig; registryConfig: RegistryConfig;
auction: AuctionConfig; misc: MiscConfig;
turnkey: { turnkey: {
apiBaseUrl: string; apiBaseUrl: string;
apiPublicKey: string; apiPublicKey: string;

View File

@ -3,9 +3,7 @@ import {
DeepPartial, DeepPartial,
FindManyOptions, FindManyOptions,
FindOneOptions, FindOneOptions,
FindOptionsWhere, FindOptionsWhere
IsNull,
Not
} from 'typeorm'; } from 'typeorm';
import path from 'path'; import path from 'path';
import debug from 'debug'; import debug from 'debug';
@ -13,7 +11,7 @@ import assert from 'assert';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary'; import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig } from './config'; import { DatabaseConfig, MiscConfig } from './config';
import { User } from './entity/User'; import { User } from './entity/User';
import { Organization } from './entity/Organization'; import { Organization } from './entity/Organization';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
@ -23,7 +21,6 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { getEntities, loadAndSaveData } from './utils'; import { getEntities, loadAndSaveData } from './utils';
import { UserOrganization } from './entity/UserOrganization'; import { UserOrganization } from './entity/UserOrganization';
import { Deployer } from './entity/Deployer';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
// TODO: Fix order of methods // TODO: Fix order of methods
export class Database { export class Database {
private dataSource: DataSource; private dataSource: DataSource;
private projectDomain: string;
constructor({ dbPath }: DatabaseConfig) { constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
this.dataSource = new DataSource({ this.dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
database: dbPath, database: dbPath,
@ -43,49 +41,38 @@ export class Database {
synchronize: true, synchronize: true,
logging: false logging: false
}); });
this.projectDomain = projectDomain;
} }
async init(): Promise<void> { async init (): Promise<void> {
await this.dataSource.initialize(); await this.dataSource.initialize();
log('database initialized'); log('database initialized');
let organizations = await this.getOrganizations({}); const organizations = await this.getOrganizations({});
// Load an organization if none exist // Load an organization if none exist
if (!organizations.length) { if (!organizations.length) {
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
organizations = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]); 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
}
)
} }
} }
async getUser(options: FindOneOptions<User>): Promise<User | null> { async getUser (options: FindOneOptions<User>): Promise<User | null> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.findOne(options); const user = await userRepository.findOne(options);
return user; return user;
} }
async addUser(data: DeepPartial<User>): Promise<User> { async addUser (data: DeepPartial<User>): Promise<User> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.save(data); const user = await userRepository.save(data);
return user; return user;
} }
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> { async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const updateResult = await userRepository.update({ id: user.id }, data); const updateResult = await userRepository.update({ id: user.id }, data);
assert(updateResult.affected); assert(updateResult.affected);
@ -93,7 +80,7 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganizations( async getOrganizations (
options: FindManyOptions<Organization> options: FindManyOptions<Organization>
): Promise<Organization[]> { ): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
@ -102,7 +89,7 @@ export class Database {
return organizations; return organizations;
} }
async getOrganization( async getOrganization (
options: FindOneOptions<Organization> options: FindOneOptions<Organization>
): Promise<Organization | null> { ): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
@ -111,7 +98,7 @@ export class Database {
return organization; return organization;
} }
async getOrganizationsByUserId(userId: string): Promise<Organization[]> { async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const userOrgs = await organizationRepository.find({ const userOrgs = await organizationRepository.find({
@ -127,29 +114,21 @@ export class Database {
return userOrgs; return userOrgs;
} }
async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> { async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization); const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
const newUserOrganization = await userOrganizationRepository.save(data); const newUserOrganization = await userOrganizationRepository.save(data);
return newUserOrganization; return newUserOrganization;
} }
async updateOrganization(organizationId: string, data: DeepPartial<Organization>): Promise<boolean> { async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
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 projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository.find(options); const projects = await projectRepository.find(options);
return projects; return projects;
} }
async getProjectById(projectId: string): Promise<Project | null> { async getProjectById (projectId: string): Promise<Project | null> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository const project = await projectRepository
@ -161,9 +140,7 @@ export class Database {
) )
.leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('deployments.deployer', 'deployer')
.leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.owner', 'owner')
.leftJoinAndSelect('project.deployers', 'deployers')
.leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.organization', 'organization')
.where('project.id = :projectId', { .where('project.id = :projectId', {
projectId projectId
@ -173,25 +150,7 @@ export class Database {
return project; return project;
} }
async allProjectsWithoutDeployments(): Promise<Project[]> { async getProjectsInOrganization (
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, userId: string,
organizationSlug: string organizationSlug: string
): Promise<Project[]> { ): Promise<Project[]> {
@ -222,7 +181,7 @@ export class Database {
/** /**
* Get deployments with specified filter * Get deployments with specified filter
*/ */
async getDeployments( async getDeployments (
options: FindManyOptions<Deployment> options: FindManyOptions<Deployment>
): Promise<Deployment[]> { ): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
@ -231,13 +190,12 @@ export class Database {
return deployments; return deployments;
} }
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> { async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
return this.getDeployments({ return this.getDeployments({
relations: { relations: {
project: true, project: true,
domain: true, domain: true,
createdBy: true, createdBy: true
deployer: true,
}, },
where: { where: {
project: { project: {
@ -250,7 +208,7 @@ export class Database {
}); });
} }
async getDeployment( async getDeployment (
options: FindOneOptions<Deployment> options: FindOneOptions<Deployment>
): Promise<Deployment | null> { ): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
@ -259,14 +217,14 @@ export class Database {
return deployment; return deployment;
} }
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> { async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find(options); const domains = await domainRepository.find(options);
return domains; return domains;
} }
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> { async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const id = nanoid(); const id = nanoid();
@ -280,7 +238,7 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId( async getProjectMembersByProjectId (
projectId: string projectId: string
): Promise<ProjectMember[]> { ): Promise<ProjectMember[]> {
const projectMemberRepository = const projectMemberRepository =
@ -301,7 +259,7 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId( async getEnvironmentVariablesByProjectId (
projectId: string, projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable> filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
@ -320,7 +278,7 @@ export class Database {
return environmentVariables; return environmentVariables;
} }
async removeProjectMemberById(projectMemberId: string): Promise<boolean> { async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
const projectMemberRepository = const projectMemberRepository =
this.dataSource.getRepository(ProjectMember); this.dataSource.getRepository(ProjectMember);
@ -335,7 +293,7 @@ export class Database {
} }
} }
async updateProjectMemberById( async updateProjectMemberById (
projectMemberId: string, projectMemberId: string,
data: DeepPartial<ProjectMember> data: DeepPartial<ProjectMember>
): Promise<boolean> { ): Promise<boolean> {
@ -349,7 +307,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProjectMember( async addProjectMember (
data: DeepPartial<ProjectMember> data: DeepPartial<ProjectMember>
): Promise<ProjectMember> { ): Promise<ProjectMember> {
const projectMemberRepository = const projectMemberRepository =
@ -359,7 +317,7 @@ export class Database {
return newProjectMember; return newProjectMember;
} }
async addEnvironmentVariables( async addEnvironmentVariables (
data: DeepPartial<EnvironmentVariable>[] data: DeepPartial<EnvironmentVariable>[]
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
const environmentVariableRepository = const environmentVariableRepository =
@ -370,7 +328,7 @@ export class Database {
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable( async updateEnvironmentVariable (
environmentVariableId: string, environmentVariableId: string,
data: DeepPartial<EnvironmentVariable> data: DeepPartial<EnvironmentVariable>
): Promise<boolean> { ): Promise<boolean> {
@ -384,7 +342,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteEnvironmentVariable( async deleteEnvironmentVariable (
environmentVariableId: string environmentVariableId: string
): Promise<boolean> { ): Promise<boolean> {
const environmentVariableRepository = const environmentVariableRepository =
@ -400,7 +358,7 @@ export class Database {
} }
} }
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = const projectMemberRepository =
this.dataSource.getRepository(ProjectMember); this.dataSource.getRepository(ProjectMember);
@ -423,7 +381,7 @@ export class Database {
return projectMemberWithProject[0]; return projectMemberWithProject[0];
} }
async getProjectsBySearchText( async getProjectsBySearchText (
userId: string, userId: string,
searchText: string searchText: string
): Promise<Project[]> { ): Promise<Project[]> {
@ -445,14 +403,14 @@ export class Database {
return projects; return projects;
} }
async updateDeploymentById( async updateDeploymentById (
deploymentId: string, deploymentId: string,
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data); return this.updateDeployment({ id: deploymentId }, data);
} }
async updateDeployment( async updateDeployment (
criteria: FindOptionsWhere<Deployment>, criteria: FindOptionsWhere<Deployment>,
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
@ -462,7 +420,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async updateDeploymentsByProjectIds( async updateDeploymentsByProjectIds (
projectIds: string[], projectIds: string[],
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
@ -478,7 +436,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteDeploymentById(deploymentId: string): Promise<boolean> { async deleteDeploymentById (deploymentId: string): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOneOrFail({ const deployment = await deploymentRepository.findOneOrFail({
where: { where: {
@ -491,7 +449,7 @@ export class Database {
return Boolean(deleteResult); return Boolean(deleteResult);
} }
async addProject(user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> { async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists // TODO: Check if organization exists
@ -507,16 +465,12 @@ export class Database {
id: organizationId id: organizationId
}); });
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async saveProject(project: Project): Promise<Project> { async updateProjectById (
const projectRepository = this.dataSource.getRepository(Project);
return projectRepository.save(project);
}
async updateProjectById(
projectId: string, projectId: string,
data: DeepPartial<Project> data: DeepPartial<Project>
): Promise<boolean> { ): Promise<boolean> {
@ -529,7 +483,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteProjectById(projectId: string): Promise<boolean> { async deleteProjectById (projectId: string): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository.findOneOrFail({ const project = await projectRepository.findOneOrFail({
where: { where: {
@ -545,7 +499,7 @@ export class Database {
return Boolean(deleteResult); return Boolean(deleteResult);
} }
async deleteDomainById(domainId: string): Promise<boolean> { async deleteDomainById (domainId: string): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const deleteResult = await domainRepository.softDelete({ id: domainId }); const deleteResult = await domainRepository.softDelete({ id: domainId });
@ -557,21 +511,21 @@ export class Database {
} }
} }
async addDomain(data: DeepPartial<Domain>): Promise<Domain> { async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const newDomain = await domainRepository.save(data); const newDomain = await domainRepository.save(data);
return newDomain; return newDomain;
} }
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> { async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domain = await domainRepository.findOne(options); const domain = await domainRepository.findOne(options);
return domain; return domain;
} }
async updateDomainById( async updateDomainById (
domainId: string, domainId: string,
data: DeepPartial<Domain> data: DeepPartial<Domain>
): Promise<boolean> { ): Promise<boolean> {
@ -581,7 +535,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async getDomainsByProjectId( async getDomainsByProjectId (
projectId: string, projectId: string,
filter?: FindOptionsWhere<Domain> filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> { ): Promise<Domain[]> {
@ -601,24 +555,4 @@ export class Database {
return domains; 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;
}
} }

View File

@ -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[];
}

View File

@ -13,7 +13,6 @@ import {
import { Project } from './Project'; import { Project } from './Project';
import { Domain } from './Domain'; import { Domain } from './Domain';
import { User } from './User'; import { User } from './User';
import { Deployer } from './Deployer';
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
export enum Environment { export enum Environment {
@ -34,21 +33,21 @@ export interface ApplicationDeploymentRequest {
version: string; version: string;
name: string; name: string;
application: string; application: string;
lrn?: string;
auction?: string;
config: string; config: string;
meta: string; meta: string;
payment?: string;
} }
export interface ApplicationDeploymentRemovalRequest { export interface ApplicationDeploymentRemovalRequest {
type: string; type: string;
version: string; version: string;
deployment: string; deployment: string;
auction?: string;
payment?: string;
} }
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: string;
}
export interface ApplicationRecord { export interface ApplicationRecord {
type: string; type: string;
@ -113,23 +112,19 @@ export class Deployment {
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRemovalRequestId!: string | null; applicationDeploymentRemovalRequestId!: string | null;
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null; applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRemovalRecordId!: string | null; applicationDeploymentRemovalRecordId!: string | null;
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@ManyToOne(() => Deployer)
@JoinColumn({ name: 'deployerLrn' })
deployer!: Deployer;
@Column({ @Column({
enum: Environment enum: Environment
}) })
@ -152,7 +147,7 @@ export class Deployment {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn() @DeleteDateColumn()
deletedAt!: Date | null; deletedAt!: Date | null;
} }

View File

@ -7,16 +7,13 @@ import {
ManyToOne, ManyToOne,
JoinColumn, JoinColumn,
OneToMany, OneToMany,
DeleteDateColumn, DeleteDateColumn
JoinTable,
ManyToMany
} from 'typeorm'; } from 'typeorm';
import { User } from './User'; import { User } from './User';
import { Organization } from './Organization'; import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment'; import { Deployment } from './Deployment';
import { Deployer } from './Deployer';
@Entity() @Entity()
export class Project { export class Project {
@ -49,20 +46,6 @@ export class Project {
@Column('text', { default: '' }) @Column('text', { default: '' })
description!: string; 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 // TODO: Compute template & framework in import repository
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
template!: string | null; template!: string | null;
@ -70,10 +53,6 @@ export class Project {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
framework!: string | null; framework!: string | null;
// Address of the user who created the project i.e. requested deployments
@Column('varchar')
paymentAddress!: string;
@Column({ @Column({
type: 'simple-array' type: 'simple-array'
}) })
@ -82,6 +61,9 @@ export class Project {
@Column('varchar') @Column('varchar')
icon!: string; icon!: string;
@Column('varchar')
subDomain!: string;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;

View File

@ -17,7 +17,7 @@ const log = debug('snowball:server');
const OAUTH_CLIENT_TYPE = 'oauth-app'; const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const { server, database, gitHub, registryConfig } = await getConfig(); const { server, database, gitHub, registryConfig, misc } = await getConfig();
const app = new OAuthApp({ const app = new OAuthApp({
clientType: OAUTH_CLIENT_TYPE, clientType: OAUTH_CLIENT_TYPE,
@ -25,7 +25,7 @@ export const main = async (): Promise<void> => {
clientSecret: gitHub.oAuth.clientSecret, clientSecret: gitHub.oAuth.clientSecret,
}); });
const db = new Database(database); const db = new Database(database, misc);
await db.init(); await db.init();
const registry = new Registry(registryConfig); const registry = new Registry(registryConfig);

View File

@ -1,12 +1,9 @@
import assert from 'assert';
import debug from 'debug'; import debug from 'debug';
import { DateTime } from 'luxon'; import assert from 'assert';
import { Octokit } from 'octokit';
import { inc as semverInc } from 'semver'; 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 { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { import {
@ -15,53 +12,49 @@ import {
ApplicationDeploymentRequest, ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest ApplicationDeploymentRemovalRequest
} from './entity/Deployment'; } from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types'; import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils'; import { sleep } from './utils';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
const SLEEP_DURATION = 1000; 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 { export class Registry {
private registry: LaconicRegistry; private registry: LaconicRegistry;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
constructor(registryConfig: RegistryConfig) { constructor (registryConfig: RegistryConfig) {
this.registryConfig = registryConfig; this.registryConfig = registryConfig;
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
this.registry = new LaconicRegistry( this.registry = new LaconicRegistry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
{ chainId: registryConfig.chainId, gasPrice } registryConfig.chainId
); );
} }
async createApplicationRecord({ async createApplicationRecord ({
octokit, appName,
repository, packageJSON,
commitHash, commitHash,
appType, appType,
repoUrl
}: { }: {
octokit: Octokit appName: string;
repository: string; packageJSON: PackageJSON;
commitHash: string; commitHash: string;
appType: string; appType: string;
repoUrl: string;
}): Promise<{ }): Promise<{
applicationRecordId: string; applicationRecordId: string;
applicationRecordData: ApplicationRecord; applicationRecordData: ApplicationRecord;
}> { }> {
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash) // Use laconic-sdk to publish record
// Use registry-sdk to publish record
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records // Fetch previous records
const records = await this.registry.queryRecords( const records = await this.registry.queryRecords(
@ -94,7 +87,7 @@ export class Registry {
repository_ref: commitHash, repository_ref: commitHash,
repository: [repoUrl], repository: [repoUrl],
app_type: appType, app_type: appType,
name: repo, name: appName,
...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }), ...(packageJSON.license && { license: packageJSON.license }),
@ -107,158 +100,68 @@ export class Registry {
...(packageJSON.version && { app_version: packageJSON.version }) ...(packageJSON.version && { app_version: packageJSON.version })
}; };
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const result = await this.registry.setRecord(
{
const result = await registryTransactionWithRetry(() => privateKey: this.registryConfig.privateKey,
this.registry.setRecord( record: applicationRecord,
{ bondId: this.registryConfig.bondId
privateKey: this.registryConfig.privateKey, },
record: applicationRecord, '',
bondId: this.registryConfig.bondId this.registryConfig.fee
},
this.registryConfig.privateKey,
fee
)
); );
log(`Published application record ${result.id}`);
log('Application record data:', applicationRecord); log('Application record data:', applicationRecord);
// TODO: Discuss computation of LRN // TODO: Discuss computation of CRN
const lrn = this.getLrn(repo); const crn = this.getCrn(appName);
log(`Setting name: ${lrn} for record ID: ${result.id}`); log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await registryTransactionWithRetry(() => await this.registry.setName(
this.registry.setName( { cid: result.data.id, crn },
{ this.registryConfig.privateKey,
cid: result.id, this.registryConfig.fee
lrn
},
this.registryConfig.privateKey,
fee
)
); );
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await registryTransactionWithRetry(() => await this.registry.setName(
this.registry.setName( { cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
{ this.registryConfig.privateKey,
cid: result.id, this.registryConfig.fee
lrn: `${lrn}@${applicationRecord.app_version}`
},
this.registryConfig.privateKey,
fee
)
); );
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await registryTransactionWithRetry(() => await this.registry.setName(
this.registry.setName( {
{ cid: result.data.id,
cid: result.id, crn: `${crn}@${applicationRecord.repository_ref}`
lrn: `${lrn}@${applicationRecord.repository_ref}` },
}, this.registryConfig.privateKey,
this.registryConfig.privateKey, this.registryConfig.fee
fee
)
); );
return { return {
applicationRecordId: result.id, applicationRecordId: result.data.id,
applicationRecordData: applicationRecord applicationRecordData: applicationRecord
}; };
} }
async createApplicationDeploymentAuction( async createApplicationDeploymentRequest (data: {
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, deployment: Deployment,
appName: string, appName: string,
repository: string, repository: string,
auctionId?: string | null,
lrn: string,
environmentVariables: { [key: string]: string }, environmentVariables: { [key: string]: string },
dns: string, dns: string,
payment?: string | null
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string; applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest; applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> { }> {
const lrn = this.getLrn(data.appName); const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([lrn]); const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0]; const applicationRecord = records[0];
if (!applicationRecord) { 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 // Create record of type ApplicationDeploymentRequest and publish
@ -266,9 +169,12 @@ export class Registry {
type: APP_DEPLOYMENT_REQUEST_TYPE, type: APP_DEPLOYMENT_REQUEST_TYPE,
version: '1.0.0', version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${lrn}@${applicationRecord.attributes.app_version}`, application: `${crn}@${applicationRecord.attributes.app_version}`,
dns: data.dns, dns: data.dns,
// 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 // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
config: JSON.stringify({ config: JSON.stringify({
env: data.environmentVariables env: data.environmentVariables
@ -279,87 +185,32 @@ export class Registry {
)}`, )}`,
repository: data.repository, repository: data.repository,
repository_ref: data.deployment.commitHash repository_ref: data.deployment.commitHash
}), })
deployer: data.lrn,
...(data.auctionId && { auction: data.auctionId }),
...(data.payment && { payment: data.payment }),
}; };
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
const result = await this.registry.setRecord(
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); {
privateKey: this.registryConfig.privateKey,
const result = await registryTransactionWithRetry(() => record: applicationDeploymentRequest,
this.registry.setRecord( bondId: this.registryConfig.bondId
{ },
privateKey: this.registryConfig.privateKey, '',
record: applicationDeploymentRequest, this.registryConfig.fee
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
)
); );
log(`Application deployment request record published: ${result.data.id}`);
log(`Application deployment request record published: ${result.id}`);
log('Application deployment request data:', applicationDeploymentRequest); log('Application deployment request data:', applicationDeploymentRequest);
return { return {
applicationDeploymentRequestId: result.id, applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest 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 * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords( async getDeploymentRecords (
deployments: Deployment[] deployments: Deployment[]
): Promise<AppDeploymentRecord[]> { ): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
@ -371,33 +222,20 @@ export class Registry {
true true
); );
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL // Filter records with ApplicationRecord ID and Deployment specific URL
return records.filter((record: AppDeploymentRecord) => return records.filter((record: AppDeploymentRecord) =>
deployments.some( deployments.some(
(deployment) => (deployment) =>
deployment.applicationDeploymentRequestId === record.attributes.request && deployment.applicationRecordId === record.attributes.application &&
record.attributes.url.includes(deployment.id) record.attributes.url.includes(deployment.id)
) )
); );
} }
/**
* 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 * Fetch ApplicationDeploymentRecords by filter
*/ */
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> { async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
return this.registry.queryRecords( return this.registry.queryRecords(
{ {
type: APP_DEPLOYMENT_RECORD_TYPE, type: APP_DEPLOYMENT_RECORD_TYPE,
@ -410,7 +248,7 @@ export class Registry {
/** /**
* Fetch ApplicationDeploymentRemovalRecords for deployments * Fetch ApplicationDeploymentRemovalRecords for deployments
*/ */
async getDeploymentRemovalRecords( async getDeploymentRemovalRecords (
deployments: Deployment[] deployments: Deployment[]
): Promise<AppDeploymentRemovalRecord[]> { ): Promise<AppDeploymentRemovalRecord[]> {
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments // Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
@ -431,11 +269,8 @@ export class Registry {
); );
} }
async createApplicationDeploymentRemovalRequest(data: { async createApplicationDeploymentRemovalRequest (data: {
deploymentId: string; deploymentId: string;
deployerLrn: string;
auctionId?: string | null;
payment?: string | null;
}): Promise<{ }): Promise<{
applicationDeploymentRemovalRequestId: string; applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
@ -443,93 +278,30 @@ export class Registry {
const applicationDeploymentRemovalRequest = { const applicationDeploymentRemovalRequest = {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0', version: '1.0.0',
deployment: data.deploymentId, 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 this.registry.setRecord(
{
const result = await registryTransactionWithRetry(() => privateKey: this.registryConfig.privateKey,
this.registry.setRecord( record: applicationDeploymentRemovalRequest,
{ bondId: this.registryConfig.bondId
privateKey: this.registryConfig.privateKey, },
record: applicationDeploymentRemovalRequest, '',
bondId: this.registryConfig.bondId this.registryConfig.fee
},
this.registryConfig.privateKey,
fee
)
); );
log(`Application deployment removal request record published: ${result.id}`); log(`Application deployment removal request record published: ${result.data.id}`);
log('Application deployment removal request data:', applicationDeploymentRemovalRequest); log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
return { return {
applicationDeploymentRemovalRequestId: result.id, applicationDeploymentRemovalRequestId: result.data.id,
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
}; };
} }
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> { getCrn (appName: string): 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 {
assert(this.registryConfig.authority, "Authority doesn't exist"); assert(this.registryConfig.authority, "Authority doesn't exist");
return `lrn://${this.registryConfig.authority}/applications/${appName}`; return `crn://${this.registryConfig.authority}/applications/${appName}`;
} }
} }

View File

@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types'; import { AddProjectFromTemplateInput } from './types';
const log = debug('snowball:resolver'); const log = debug('snowball:resolver');
@ -22,8 +22,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getOrganizationsByUserId(context.user); return service.getOrganizationsByUserId(context.user);
}, },
project: async (_: any, { projectId }: { projectId: string }, context: any) => { project: async (_: any, { projectId }: { projectId: string }) => {
return service.getProjectById(context.user, projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async ( projectsInOrganization: async (
@ -69,32 +69,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
) => { ) => {
return service.getDomainsByProjectId(projectId, filter); 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 // TODO: Return error in GQL response
@ -229,16 +203,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
{ {
organizationSlug, organizationSlug,
data, data,
lrn, }: { organizationSlug: string; data: AddProjectFromTemplateInput },
auctionParams,
environmentVariables
}: {
organizationSlug: string;
data: AddProjectFromTemplateInput;
lrn: string;
auctionParams: AuctionParams;
environmentVariables: EnvironmentVariables[];
},
context: any, context: any,
) => { ) => {
try { try {
@ -246,9 +211,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
context.user, context.user,
organizationSlug, organizationSlug,
data, data,
lrn,
auctionParams,
environmentVariables
); );
} catch (err) { } catch (err) {
log(err); log(err);
@ -261,27 +223,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
{ {
organizationSlug, organizationSlug,
data, data,
lrn, }: { organizationSlug: string; data: DeepPartial<Project> },
auctionParams,
environmentVariables
}: {
organizationSlug: string;
data: DeepPartial<Project>;
lrn: string;
auctionParams: AuctionParams,
environmentVariables: EnvironmentVariables[];
},
context: any, context: any,
) => { ) => {
try { try {
return await service.addProject( return await service.addProject(context.user, organizationSlug, data);
context.user,
organizationSlug,
data,
lrn,
auctionParams,
environmentVariables
);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;

View File

@ -5,6 +5,19 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router(); const router = Router();
//
// Access Code
//
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '44444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
// //
// Turnkey // Turnkey
// //
@ -27,7 +40,7 @@ router.post('/register', async (req, res) => {
userEmail: email, userEmail: email,
userName: email.split('@')[0], userName: email.split('@')[0],
}); });
req.session.address = user.id; req.session.userId = user.id;
res.sendStatus(200); res.sendStatus(200);
}); });
@ -39,7 +52,7 @@ router.post('/authenticate', async (req, res) => {
signedWhoamiRequest, signedWhoamiRequest,
); );
if (user) { if (user) {
req.session.address = user.id; req.session.userId = user.id;
res.sendStatus(200); res.sendStatus(200);
} else { } else {
res.sendStatus(401); res.sendStatus(401);
@ -47,10 +60,11 @@ router.post('/authenticate', async (req, res) => {
}); });
// //
// SIWE Auth // Lit
// //
router.post('/validate', async (req, res) => { router.post('/validate', async (req, res) => {
const { message, signature } = req.body; const { message, signature, action } = req.body;
const { success, data } = await new SiweMessage(message).verify({ const { success, data } = await new SiweMessage(message).verify({
signature, signature,
}); });
@ -61,20 +75,23 @@ router.post('/validate', async (req, res) => {
const service: Service = req.app.get('service'); const service: Service = req.app.get('service');
const user = await service.getUserByEthAddress(data.address); const user = await service.getUserByEthAddress(data.address);
if (!user) { if (action === 'signup') {
if (user) {
return res.send({ success: false, error: 'user_already_exists' });
}
const newUser = await service.createUser({ const newUser = await service.createUser({
ethAddress: data.address, ethAddress: data.address,
email: `${data.address}@example.com`, email: '',
name: '',
subOrgId: '', subOrgId: '',
turnkeyWalletId: '', turnkeyWalletId: '',
}); });
req.session.userId = newUser.id;
// SIWESession from the web3modal library requires both address and chain ID } else if (action === 'login') {
req.session.address = newUser.id; if (!user) {
req.session.chainId = data.chainId; return res.send({ success: false, error: 'user_not_found' });
} else { }
req.session.address = user.id; req.session.userId = user.id;
req.session.chainId = data.chainId;
} }
res.send({ success }); res.send({ success });
@ -84,10 +101,9 @@ router.post('/validate', async (req, res) => {
// General // General
// //
router.get('/session', (req, res) => { router.get('/session', (req, res) => {
if (req.session.address && req.session.chainId) { if (req.session.userId) {
res.send({ res.send({
address: req.session.address, userId: req.session.userId,
chainId: req.session.chainId
}); });
} else { } else {
res.status(401).send({ error: 'Unauthorized: No active session' }); res.status(401).send({ error: 'Unauthorized: No active session' });
@ -95,12 +111,9 @@ router.get('/session', (req, res) => {
}); });
router.post('/logout', (req, res) => { router.post('/logout', (req, res) => {
req.session.destroy((err) => { // This is how you clear cookie-session
if (err) { (req as any).session = null;
return res.send({ success: false }); res.send({ success: true });
}
res.send({ success: true });
});
}); });
export default router; export default router;

View File

@ -3,7 +3,7 @@ import { Router } from 'express';
const router = Router(); const router = Router();
router.get('/version', async (req, res) => { router.get('/version', async (req, res) => {
return res.send({ version: '0.0.9' }); return res.send({ version: '0.0.8' });
}); });
export default router; export default router;

View File

@ -22,13 +22,6 @@ enum DeploymentStatus {
Deleting Deleting
} }
enum AuctionStatus {
completed
reveal
commit
expired
}
enum DomainStatus { enum DomainStatus {
Live Live
Pending Pending
@ -72,13 +65,8 @@ type Project {
repository: String! repository: String!
prodBranch: String! prodBranch: String!
description: String description: String
deployers: [Deployer!]
auctionId: String
fundsReleased: Boolean
template: String template: String
framework: String framework: String
paymentAddress: String!
txHash: String!
webhooks: [String!] webhooks: [String!]
members: [ProjectMember!] members: [ProjectMember!]
environmentVariables: [EnvironmentVariable!] environmentVariables: [EnvironmentVariable!]
@ -86,7 +74,7 @@ type Project {
updatedAt: String! updatedAt: String!
organization: Organization! organization: Organization!
icon: String icon: String
baseDomains: [String!] subDomain: String
} }
type ProjectMember { type ProjectMember {
@ -106,10 +94,7 @@ type Deployment {
commitMessage: String! commitMessage: String!
url: String url: String
environment: Environment! environment: Environment!
deployer: Deployer
applicationDeploymentRequestId: String
isCurrent: Boolean! isCurrent: Boolean!
baseDomain: String
status: DeploymentStatus! status: DeploymentStatus!
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
@ -135,17 +120,6 @@ type EnvironmentVariable {
updatedAt: String! updatedAt: String!
} }
type Deployer {
deployerLrn: String!
deployerId: String!
deployerApiUrl: String!
minimumPayment: String
paymentAddress: String
createdAt: String!
updatedAt: String!
baseDomain: String
}
type AuthResult { type AuthResult {
token: String! token: String!
} }
@ -162,8 +136,6 @@ input AddProjectFromTemplateInput {
owner: String! owner: String!
name: String! name: String!
isPrivate: Boolean! isPrivate: Boolean!
paymentAddress: String!
txHash: String!
} }
input AddProjectInput { input AddProjectInput {
@ -171,8 +143,6 @@ input AddProjectInput {
repository: String! repository: String!
prodBranch: String! prodBranch: String!
template: String template: String
paymentAddress: String!
txHash: String!
} }
input UpdateProjectInput { input UpdateProjectInput {
@ -212,48 +182,6 @@ input FilterDomainsInput {
status: DomainStatus 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 { type Query {
user: User! user: User!
organizations: [Organization!] organizations: [Organization!]
@ -264,11 +192,7 @@ type Query {
environmentVariables(projectId: String!): [EnvironmentVariable!] environmentVariables(projectId: String!): [EnvironmentVariable!]
projectMembers(projectId: String!): [ProjectMember!] projectMembers(projectId: String!): [ProjectMember!]
searchProjects(searchText: String!): [Project!] searchProjects(searchText: String!): [Project!]
getAuctionData(auctionId: String!): Auction!
domains(projectId: String!, filter: FilterDomainsInput): [Domain] domains(projectId: String!, filter: FilterDomainsInput): [Domain]
deployers: [Deployer]
address: String!
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
} }
type Mutation { type Mutation {
@ -291,17 +215,8 @@ type Mutation {
addProjectFromTemplate( addProjectFromTemplate(
organizationSlug: String! organizationSlug: String!
data: AddProjectFromTemplateInput data: AddProjectFromTemplateInput
lrn: String
auctionParams: AuctionParams
environmentVariables: [AddEnvironmentVariableInput!]
): Project!
addProject(
organizationSlug: String!
data: AddProjectInput!
lrn: String
auctionParams: AuctionParams
environmentVariables: [AddEnvironmentVariableInput!]
): Project! ): Project!
addProject(organizationSlug: String!, data: AddProjectInput): Project!
updateProject(projectId: String!, data: UpdateProjectInput): Boolean! updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean! deleteProject(projectId: String!): Boolean!

View File

@ -8,7 +8,7 @@ import {
ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageLocalDefault,
AuthenticationError, AuthenticationError,
} from 'apollo-server-core'; } from 'apollo-server-core';
import session from 'express-session'; import cookieSession from 'cookie-session';
import { TypeSource } from '@graphql-tools/utils'; import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema'; import { makeExecutableSchema } from '@graphql-tools/schema';
@ -22,13 +22,9 @@ import { Service } from './service';
const log = debug('snowball:server'); 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' { declare module 'express-session' {
interface SessionData { interface SessionData {
address: string; userId: string;
chainId: number;
} }
} }
@ -58,13 +54,14 @@ export const createAndStartServer = async (
context: async ({ req }) => { context: async ({ req }) => {
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization // https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
const { address } = req.session; const { userId } = req.session;
if (!address) { if (!userId) {
throw new AuthenticationError('Unauthorized: No active session'); throw new AuthenticationError('Unauthorized: No active session');
} }
const user = await service.getUser(address); const user = await service.getUser(userId);
return { user }; return { user };
}, },
plugins: [ plugins: [
@ -83,25 +80,20 @@ export const createAndStartServer = async (
}), }),
); );
const sessionOptions: session.SessionOptions = {
secret: secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: new URL(appOriginUrl).protocol === 'https:',
maxAge: COOKIE_MAX_AGE,
domain: domain || undefined,
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
}
};
if (trustProxy) { if (trustProxy) {
// trust first proxy // trust first proxy
app.set('trust proxy', 1); app.set('trust proxy', 1);
} }
app.use( app.use(
session(sessionOptions) cookieSession({
secret: secret,
secure: new URL(appOriginUrl).protocol === 'https:',
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
maxAge: 23 * 60 * 60 * 1000,
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
domain: domain || undefined,
}),
); );
server.applyMiddleware({ server.applyMiddleware({

View File

@ -1,12 +1,12 @@
import assert from 'assert'; import assert from 'assert';
import debug from 'debug'; import debug from 'debug';
import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm'; import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { Octokit, RequestError } from 'octokit'; import { Octokit, RequestError } from 'octokit';
import { OAuthApp } from '@octokit/oauth-app'; import { OAuthApp } from '@octokit/oauth-app';
import { Database } from './database'; import { Database } from './database';
import { ApplicationRecord, Deployment, DeploymentStatus, Environment } from './entity/Deployment'; import { Deployment, DeploymentStatus, Environment } from './entity/Deployment';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Organization } from './entity/Organization'; import { Organization } from './entity/Organization';
@ -14,19 +14,15 @@ import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember'; import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User'; import { User } from './entity/User';
import { Registry } from './registry'; import { Registry } from './registry';
import { Deployer } from './entity/Deployer';
import { GitHubConfig, RegistryConfig } from './config'; import { GitHubConfig, RegistryConfig } from './config';
import { import {
AddProjectFromTemplateInput, AddProjectFromTemplateInput,
AppDeploymentRecord, AppDeploymentRecord,
AppDeploymentRemovalRecord, AppDeploymentRemovalRecord,
AuctionParams,
DeployerRecord,
EnvironmentVariables,
GitPushEventPayload, GitPushEventPayload,
PackageJSON,
} from './types'; } from './types';
import { Role } from './entity/UserOrganization'; import { Role } from './entity/UserOrganization';
import { getRepoDetails } from './utils';
const log = debug('snowball:service'); const log = debug('snowball:service');
@ -43,16 +39,15 @@ interface Config {
export class Service { export class Service {
private db: Database; private db: Database;
private oauthApp: OAuthApp; private oauthApp: OAuthApp;
private laconicRegistry: Registry; private registry: Registry;
private config: Config; private config: Config;
private deployRecordCheckTimeout?: NodeJS.Timeout; private deployRecordCheckTimeout?: NodeJS.Timeout;
private auctionStatusCheckTimeout?: NodeJS.Timeout;
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) { constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
this.db = db; this.db = db;
this.oauthApp = app; this.oauthApp = app;
this.laconicRegistry = registry; this.registry = registry;
this.config = config; this.config = config;
this.init(); this.init();
} }
@ -65,8 +60,6 @@ export class Service {
this.checkDeployRecordsAndUpdate(); this.checkDeployRecordsAndUpdate();
// Start check for ApplicationDeploymentRemovalRecords asynchronously // Start check for ApplicationDeploymentRemovalRecords asynchronously
this.checkDeploymentRemovalRecordsAndUpdate(); this.checkDeploymentRemovalRecordsAndUpdate();
// Start check for Deployment Auctions asynchronously
this.checkAuctionStatus();
} }
/** /**
@ -74,7 +67,6 @@ export class Service {
*/ */
destroy(): void { destroy(): void {
clearTimeout(this.deployRecordCheckTimeout); clearTimeout(this.deployRecordCheckTimeout);
clearTimeout(this.auctionStatusCheckTimeout);
} }
/** /**
@ -116,7 +108,7 @@ export class Service {
} }
// Fetch ApplicationDeploymentRecord for deployments // Fetch ApplicationDeploymentRecord for deployments
const records = await this.laconicRegistry.getDeploymentRecords(deployments); const records = await this.registry.getDeploymentRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRecords`); log(`Found ${records.length} ApplicationDeploymentRecords`);
// Update deployments for which ApplicationDeploymentRecords were returned // Update deployments for which ApplicationDeploymentRecords were returned
@ -149,7 +141,7 @@ export class Service {
// Fetch ApplicationDeploymentRemovalRecords for deployments // Fetch ApplicationDeploymentRemovalRecords for deployments
const records = const records =
await this.laconicRegistry.getDeploymentRemovalRecords(deployments); await this.registry.getDeploymentRemovalRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`); log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
// Update deployments for which ApplicationDeploymentRemovalRecords were returned // Update deployments for which ApplicationDeploymentRemovalRecords were returned
@ -165,28 +157,41 @@ export class Service {
/** /**
* Update deployments with ApplicationDeploymentRecord data * Update deployments with ApplicationDeploymentRecord data
* Deployments that are completed but not updated in DB
*/ */
async updateDeploymentsWithRecordData( async updateDeploymentsWithRecordData(
records: AppDeploymentRecord[], records: AppDeploymentRecord[],
): Promise<void> { ): Promise<void> {
// Fetch the deployments to be updated using deployment requestId // Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map((record) => ({ where: records.map((record) => ({
applicationDeploymentRequestId: record.attributes.request, applicationRecordId: record.attributes.application,
})), })),
relations: {
deployer: true,
project: true,
},
order: { order: {
createdAt: 'DESC', createdAt: 'DESC',
}, },
}); });
// Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
},
new Set<string>(),
);
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds),
{ isCurrent: false },
);
const recordToDeploymentsMap = deployments.reduce( const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => { (acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationDeploymentRequestId!] = deployment; acc[deployment.applicationRecordId] = deployment;
return acc; return acc;
}, },
{}, {},
@ -194,57 +199,22 @@ export class Service {
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.request]; const deployment = recordToDeploymentsMap[record.attributes.application];
if (!deployment.project) { await this.db.updateDeploymentById(deployment.id, {
log(`Project ${deployment.projectId} not found`); applicationDeploymentRecordId: record.id,
return; applicationDeploymentRecordData: record.attributes,
} else { url: record.attributes.url,
deployment.applicationDeploymentRecordId = record.id; status: DeploymentStatus.Ready,
deployment.applicationDeploymentRecordData = record.attributes; isCurrent: deployment.environment === Environment.Production,
deployment.url = record.attributes.url; });
deployment.status = DeploymentStatus.Ready;
deployment.isCurrent = deployment.environment === Environment.Production;
await this.db.updateDeploymentById(deployment.id, deployment); log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
// Release deployer funds on successful deployment );
if (!deployment.project.fundsReleased) {
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
// Return remaining amount to owner
await this.returnUserFundsByProjectId(deployment.projectId, true);
await this.db.updateProjectById(deployment.projectId, {
fundsReleased,
});
}
log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
);
}
}); });
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
// Get deployments that are in production environment
const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent);
// Set the isCurrent state to false for the old deployments
for (const deployment of prodDeployments) {
const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId);
const oldDeployments = projectDeployments
.filter(projectDeployment => projectDeployment.deployer.deployerLrn === deployment.deployer.deployerLrn && projectDeployment.id !== deployment.id);
for (const oldDeployment of oldDeployments) {
await this.db.updateDeployment(
{ id: oldDeployment.id },
{ isCurrent: false }
);
}
}
await Promise.all(deploymentUpdatePromises);
} }
/** /**
@ -292,45 +262,6 @@ export class Service {
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
} }
/**
* Checks the status for all ongoing auctions
* Calls the createDeploymentFromAuction method for deployments with completed auctions
*/
async checkAuctionStatus(): Promise<void> {
const projects = await this.db.allProjectsWithoutDeployments();
const validAuctionIds = projects.map((project) => project.auctionId)
.filter((id): id is string => Boolean(id));
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds);
const projectsToBedeployed = projects.filter((project) =>
completedAuctionIds.includes(project.auctionId!)
);
for (const project of projectsToBedeployed) {
const deployerRecords = await this.laconicRegistry.getAuctionWinningDeployerRecords(project!.auctionId!);
if (!deployerRecords) {
log(`No winning deployer for auction ${project!.auctionId}`);
// Return all funds to the owner
await this.returnUserFundsByProjectId(project.id, false)
} else {
const deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
for (const deployer of deployers) {
log(`Creating deployment for deployer ${deployer.deployerLrn}`);
await this.createDeploymentFromAuction(project, deployer);
// Update project with deployer
await this.updateProjectWithDeployer(project.id, deployer);
}
}
}
this.auctionStatusCheckTimeout = setTimeout(() => {
this.checkAuctionStatus();
}, this.config.registryConfig.checkAuctionStatusDelay);
}
async getUser(userId: string): Promise<User | null> { async getUser(userId: string): Promise<User | null> {
return this.db.getUser({ return this.db.getUser({
where: { where: {
@ -364,7 +295,7 @@ export class Service {
} }
async createUser(params: { async createUser(params: {
name?: string; name: string;
email: string; email: string;
subOrgId: string; subOrgId: string;
ethAddress: string; ethAddress: string;
@ -407,13 +338,8 @@ export class Service {
return dbOrganizations; return dbOrganizations;
} }
async getProjectById(user: User, projectId: string): Promise<Project | null> { async getProjectById(projectId: string): Promise<Project | null> {
const dbProject = await this.db.getProjectById(projectId); const dbProject = await this.db.getProjectById(projectId);
if (dbProject && dbProject.owner.id !== user.id) {
return null;
}
return dbProject; return dbProject;
} }
@ -593,7 +519,6 @@ export class Service {
domain: prodBranchDomains[0], domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage,
deployer: oldDeployment.deployer
}); });
return newDeployment; return newDeployment;
@ -603,20 +528,44 @@ export class Service {
userId: string, userId: string,
octokit: Octokit, octokit: Octokit,
data: DeepPartial<Deployment>, data: DeepPartial<Deployment>,
deployerLrn?: string
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); assert(data.project?.repository, 'Project repository not found');
log( log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`, `Creating deployment in project ${data.project.name} from branch ${data.branch}`,
); );
const [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({
owner,
repo,
path: 'package.json',
ref: data.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;
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record) // TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = const { applicationRecordId, applicationRecordData } =
await this.laconicRegistry.createApplicationRecord({ await this.registry.createApplicationRecord({
octokit, appName: repo,
repository: data.project.repository, packageJSON,
appType: data.project!.template!, appType: data.project!.template!,
commitHash: data.commitHash!, commitHash: data.commitHash!,
repoUrl,
}); });
// Update previous deployment with prod branch domain // Update previous deployment with prod branch domain
@ -632,138 +581,6 @@ export class Service {
); );
} }
let deployer;
if (deployerLrn) {
deployer = await this.db.getDeployerByLRN(deployerLrn);
} else {
deployer = data.deployer;
}
const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData);
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
lrn: deployer!.deployerLrn!,
payment: data.project.txHash,
auctionId: data.project.auctionId
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
lrn: deployer!.deployerLrn!,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
payment: data.project.txHash,
auctionId: data.project.auctionId
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment;
}
async createDeploymentFromAuction(
project: DeepPartial<Project>,
deployer: Deployer
): Promise<Deployment> {
const octokit = await this.getOctokit(project.ownerId!);
const [owner, repo] = project.repository!.split('/');
const repoUrl = (
await octokit.rest.repos.get({
owner,
repo,
})
).data.html_url;
const {
data: [latestCommit],
} = await octokit.rest.repos.listCommits({
owner,
repo,
sha: project.prodBranch,
per_page: 1,
});
const lrn = this.laconicRegistry.getLrn(repo);
const [record] = await this.laconicRegistry.getRecordsByName(lrn);
const applicationRecordId = record.id;
const applicationRecordData = record.attributes;
const deployerLrn = deployer!.deployerLrn
// Create deployment with prod branch and latest commit
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
};
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData);
const environmentVariablesObj = await this.getEnvVariables(project!.id!);
// To set project DNS
if (deploymentData.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
auctionId: project.auctionId!,
lrn: deployerLrn,
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
// Create requests for all the deployers
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
auctionId: project.auctionId!,
lrn: deployerLrn,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment;
}
async createDeploymentFromData(
userId: string,
data: DeepPartial<Deployment>,
deployerLrn: string,
applicationRecordId: string,
applicationRecordData: ApplicationRecord,
): Promise<Deployment> {
const newDeployment = await this.db.addDeployment({ const newDeployment = await this.db.addDeployment({
project: data.project, project: data.project,
branch: data.branch, branch: data.branch,
@ -777,43 +594,60 @@ export class Service {
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId, id: userId,
}), }),
deployer: Object.assign(new Deployer(), {
deployerLrn,
}),
}); });
log(`Created deployment ${newDeployment.id}`); log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
);
return newDeployment; const environmentVariables =
} await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production,
});
async updateProjectWithDeployer( const environmentVariablesObj = environmentVariables.reduce(
projectId: string, (acc, env) => {
deployer: Deployer acc[env.key] = env.value;
): Promise<Deployer> {
const deploymentProject = await this.db.getProjects({
where: { id: projectId },
relations: ['deployers']
});
if (!deploymentProject[0].deployers) { return acc;
deploymentProject[0].deployers = []; },
{} as { [key: string]: string },
);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
});
} }
deploymentProject[0].deployers.push(deployer); const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.saveProject(deploymentProject[0]); await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return deployer; return newDeployment;
} }
async addProjectFromTemplate( async addProjectFromTemplate(
user: User, user: User,
organizationSlug: string, organizationSlug: string,
data: AddProjectFromTemplateInput, data: AddProjectFromTemplateInput,
lrn?: string,
auctionParams?: AuctionParams,
environmentVariables?: EnvironmentVariables[],
): Promise<Project | undefined> { ): Promise<Project | undefined> {
try { try {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
@ -844,9 +678,7 @@ export class Service {
repository: gitRepo.data.full_name, repository: gitRepo.data.full_name,
// TODO: Set selected template // TODO: Set selected template
template: 'webapp', template: 'webapp',
paymentAddress: data.paymentAddress, });
txHash: data.txHash
}, lrn, auctionParams, environmentVariables);
if (!project || !project.id) { if (!project || !project.id) {
throw new Error('Failed to create project from template'); throw new Error('Failed to create project from template');
@ -863,26 +695,18 @@ export class Service {
user: User, user: User,
organizationSlug: string, organizationSlug: string,
data: DeepPartial<Project>, data: DeepPartial<Project>,
lrn?: string,
auctionParams?: AuctionParams,
environmentVariables?: EnvironmentVariables[],
): Promise<Project | undefined> { ): Promise<Project | undefined> {
const organization = await this.db.getOrganization({ const organization = await this.db.getOrganization({
where: { where: {
slug: organizationSlug, slug: organizationSlug,
}, },
}); });
if (!organization) { if (!organization) {
throw new Error('Organization does not exist'); throw new Error('Organization does not exist');
} }
const project = await this.db.addProject(user, organization.id, data); const project = await this.db.addProject(user, organization.id, data);
if (environmentVariables) {
await this.addEnvironmentVariables(project.id, environmentVariables);
}
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
@ -895,59 +719,20 @@ export class Service {
per_page: 1, per_page: 1,
}); });
if (auctionParams) { // Create deployment with prod branch and latest commit
// Create deployment with prod branch and latest commit const deployment = await this.createDeployment(user.id, octokit, {
const deploymentData = { project,
project, branch: project.prodBranch,
branch: project.prodBranch, environment: Environment.Production,
environment: Environment.Production, domain: null,
domain: null, commitHash: latestCommit.sha,
commitHash: latestCommit.sha, commitMessage: latestCommit.commit.message,
commitMessage: latestCommit.commit.message, });
};
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
} else {
const deployer = await this.db.getDeployerByLRN(lrn!);
if (!deployer) {
log('Invalid deployer LRN');
return;
}
if (deployer.minimumPayment && project.txHash) {
const amountToBePaid = deployer?.minimumPayment.replace(/\D/g, '').toString();
const txResponse = await this.laconicRegistry.sendTokensToAccount(
deployer?.paymentAddress!,
amountToBePaid
);
const txHash = txResponse.transactionHash;
if (txHash) {
await this.updateProject(project.id, { txHash });
project.txHash = txHash;
log('Funds transferrend to deployer');
}
}
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
deployer
};
const newDeployment = await this.createDeployment(user.id, octokit, deploymentData);
// Update project with deployer
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
}
await this.createRepoHook(octokit, project); await this.createRepoHook(octokit, project);
console.log('projectid is', project.id);
return project; return project;
} }
@ -997,9 +782,6 @@ export class Service {
); );
const projects = await this.db.getProjects({ const projects = await this.db.getProjects({
where: { repository: repository.full_name }, where: { repository: repository.full_name },
relations: {
deployers: true,
}
}); });
if (!projects.length) { if (!projects.length) {
@ -1016,29 +798,18 @@ export class Service {
branch, branch,
}); });
const deployers = project.deployers; // Create deployment with branch and latest commit in GitHub data
if (!deployers) { await this.createDeployment(project.ownerId, octokit, {
log(`No deployer present for project ${project.id}`) project,
return; branch,
} environment:
project.prodBranch === branch
for (const deployer of deployers) { ? Environment.Production
// Create deployment with branch and latest commit in GitHub data : Environment.Preview,
await this.createDeployment(project.ownerId, octokit, domain,
{ commitHash: headCommit.id,
project, commitMessage: headCommit.message,
branch, });
environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message,
deployer: deployer
},
);
}
} }
} }
@ -1075,7 +846,6 @@ export class Service {
relations: { relations: {
project: true, project: true,
domain: true, domain: true,
deployer: true,
createdBy: true, createdBy: true,
}, },
where: { where: {
@ -1089,24 +859,15 @@ export class Service {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
let newDeployment: Deployment; const newDeployment = await this.createDeployment(user.id, octokit, {
project: oldDeployment.project,
if (oldDeployment.project.auctionId) { // TODO: Put isCurrent field in project
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer); branch: oldDeployment.branch,
} else { environment: Environment.Production,
newDeployment = await this.createDeployment(user.id, octokit, domain: oldDeployment.domain,
{ commitHash: oldDeployment.commitHash,
project: oldDeployment.project, commitMessage: oldDeployment.commitMessage,
// TODO: Put isCurrent field in project });
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage,
deployer: oldDeployment.deployer
}
);
}
return newDeployment; return newDeployment;
} }
@ -1152,18 +913,16 @@ export class Service {
}, },
relations: { relations: {
project: true, project: true,
deployer: true,
}, },
}); });
if (deployment && deployment.applicationDeploymentRecordId) { if (deployment && deployment.applicationDeploymentRecordId) {
// If deployment is current, remove deployment for project subdomain as well // If deployment is current, remove deployment for project subdomain as well
if (deployment.isCurrent) { if (deployment.isCurrent) {
const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}`; const currentDeploymentURL = `https://${deployment.project.subDomain}`;
// TODO: Store the latest DNS deployment record
const deploymentRecords = const deploymentRecords =
await this.laconicRegistry.getDeploymentRecordsByFilter({ await this.registry.getDeploymentRecordsByFilter({
application: deployment.applicationRecordId, application: deployment.applicationRecordId,
url: currentDeploymentURL, url: currentDeploymentURL,
}); });
@ -1176,24 +935,14 @@ export class Service {
return false; return false;
} }
// Multiple records are fetched, take the latest record await this.registry.createApplicationDeploymentRemovalRequest({
const latestRecord = deploymentRecords deploymentId: deploymentRecords[0].id,
.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())[0];
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: latestRecord.id,
deployerLrn: deployment.deployer.deployerLrn,
auctionId: deployment.project.auctionId,
payment: deployment.project.txHash
}); });
} }
const result = const result =
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId, deploymentId: deployment.applicationDeploymentRecordId,
deployerLrn: deployment.deployer.deployerLrn,
auctionId: deployment.project.auctionId,
payment: deployment.project.txHash
}); });
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(deployment.id, {
@ -1328,157 +1077,4 @@ export class Service {
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateUser(user, data); return this.db.updateUser(user, data);
} }
async getEnvVariables(
projectId: string,
): Promise<{ [key: string]: string }> {
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string },
);
return environmentVariablesObj;
}
async getAuctionData(
auctionId: string
): Promise<any> {
const auctions = await this.laconicRegistry.getAuctionData(auctionId);
return auctions[0];
}
async releaseDeployerFundsByProjectId(projectId: string): Promise<boolean> {
const project = await this.db.getProjectById(projectId);
if (!project || !project.auctionId) {
log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`);
return false;
}
const auction = await this.laconicRegistry.releaseDeployerFunds(project.auctionId);
if (auction.auction.fundsReleased) {
log(`Funds released for auction ${project.auctionId}`);
await this.db.updateProjectById(projectId, { fundsReleased: true });
return true;
}
log(`Error releasing funds for auction ${project.auctionId}`);
return false;
}
async returnUserFundsByProjectId(projectId: string, winningDeployersPresent: boolean) {
const project = await this.db.getProjectById(projectId);
if (!project || !project.auctionId) {
log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`);
return false;
}
const auction = await this.getAuctionData(project.auctionId);
const totalAuctionPrice = Number(auction.maxPrice.quantity) * auction.numProviders;
let amountToBeReturned;
if (winningDeployersPresent) {
amountToBeReturned = totalAuctionPrice - auction.winnerAddresses.length * Number(auction.winnerPrice.quantity);
} else {
amountToBeReturned = totalAuctionPrice;
}
if (amountToBeReturned !== 0) {
await this.laconicRegistry.sendTokensToAccount(
project.paymentAddress,
amountToBeReturned.toString()
);
}
}
async getDeployers(): Promise<Deployer[]> {
const dbDeployers = await this.db.getDeployers();
if (dbDeployers.length > 0) {
// Call asynchronously to fetch the records from the registry and update the DB
this.updateDeployersFromRegistry();
return dbDeployers;
} else {
// Fetch from the registry and populate empty DB
return await this.updateDeployersFromRegistry();
}
}
async updateDeployersFromRegistry(): Promise<Deployer[]> {
const deployerRecords = await this.laconicRegistry.getDeployerRecordsByFilter({});
await this.saveDeployersByDeployerRecords(deployerRecords);
return await this.db.getDeployers();
}
async saveDeployersByDeployerRecords(deployerRecords: DeployerRecord[]): Promise<Deployer[]> {
const deployers: Deployer[] = [];
for (const record of deployerRecords) {
if (record.names && record.names.length > 0) {
const deployerId = record.id;
const deployerLrn = record.names[0];
const deployerApiUrl = record.attributes.apiUrl;
const minimumPayment = record.attributes.minimumPayment;
const paymentAddress = record.attributes.paymentAddress;
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
const deployerData = {
deployerLrn,
deployerId,
deployerApiUrl,
baseDomain,
minimumPayment,
paymentAddress
};
// TODO: Update deployers table in a separate job
const deployer = await this.db.addDeployer(deployerData);
deployers.push(deployer);
}
}
return deployers;
}
async getAddress(): Promise<any> {
const account = await this.laconicRegistry.getAccount();
return account.address;
}
async verifyTx(txHash: string, amountSent: string, senderAddress: string): Promise<boolean> {
const txResponse = await this.laconicRegistry.getTxResponse(txHash);
if (!txResponse) {
log('Transaction response not found');
return false;
}
const transfer = txResponse.events.find(e => e.type === 'transfer' && e.attributes.some(a => a.key === 'msg_index'));
if (!transfer) {
log('No transfer event found');
return false;
}
const sender = transfer.attributes.find(a => a.key === 'sender')?.value;
const recipient = transfer.attributes.find(a => a.key === 'recipient')?.value;
const amount = transfer.attributes.find(a => a.key === 'amount')?.value;
const recipientAddress = await this.getAddress();
return amount === amountSent && sender === senderAddress && recipient === recipientAddress;
}
} }

View File

@ -29,8 +29,6 @@ export interface GitPushEventPayload {
export interface AppDeploymentRecordAttributes { export interface AppDeploymentRecordAttributes {
application: string; application: string;
auction: string;
deployer: string;
dns: string; dns: string;
meta: string; meta: string;
name: string; name: string;
@ -70,35 +68,4 @@ export interface AddProjectFromTemplateInput {
owner: string; owner: string;
name: string; name: string;
isPrivate: boolean; 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;
};
} }

View File

@ -1,14 +1,10 @@
import assert from 'assert';
import debug from 'debug';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { Octokit } from 'octokit';
import path from 'path'; import path from 'path';
import toml from 'toml'; import toml from 'toml';
import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import { Config } from './config'; import { Config } from './config';
import { DEFAULT_CONFIG_FILE_PATH } from './constants'; import { DEFAULT_CONFIG_FILE_PATH } from './constants';
import { PackageJSON } from './types';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
@ -80,64 +76,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
export const sleep = async (ms: number): Promise<void> => export const sleep = async (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms)); 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}`);
}
}
}

View File

@ -2,6 +2,8 @@ import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import { getConfig } from '../src/utils'; import { getConfig } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:delete-database'); const log = debug('snowball:delete-database');
@ -11,7 +13,7 @@ const deleteFile = async (filePath: string) => {
}; };
const main = async () => { const main = async () => {
const config = await getConfig(); const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
deleteFile(config.database.dbPath); deleteFile(config.database.dbPath);
}; };

View File

@ -1,7 +1,12 @@
[ [
{ {
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26", "id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
"name": "Deploy Tools", "name": "Snowball Tools",
"slug": "deploy-tools" "slug": "snowball-tools-1"
},
{
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
"name": "Laconic",
"slug": "laconic-2"
} }
] ]

View File

@ -16,6 +16,8 @@ import {
getEntities, getEntities,
loadAndSaveData loadAndSaveData
} from '../src/utils'; } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:initialize-database'); const log = debug('snowball:initialize-database');
@ -154,7 +156,7 @@ const generateTestData = async (dataSource: DataSource) => {
}; };
const main = async () => { const main = async () => {
const config = await getConfig(); const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const isDbPresent = await checkFileExists(config.database.dbPath); const isDbPresent = await checkFileExists(config.database.dbPath);
if (!isDbPresent) { if (!isDbPresent) {

View File

@ -1,40 +1,39 @@
import debug from 'debug'; import debug from 'debug';
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk'; import { Registry } from '@snowballtools/laconic-sdk';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { Config } from '../src/config';
import { getConfig } from '../src/utils'; import { getConfig } from '../src/utils';
const log = debug('snowball:initialize-registry'); const log = debug('snowball:initialize-registry');
const DENOM = 'alnt'; const DENOM = 'aphoton';
const BOND_AMOUNT = '1000000000'; const BOND_AMOUNT = '1000000000';
async function main () { async function main () {
const { registryConfig } = await getConfig(); const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
// TODO: Get authority names from args // TODO: Get authority names from args
const authorityNames = ['snowballtools', registryConfig.authority]; 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); const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId); log('bondId:', bondId);
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
await registry.createBond( await registry.createBond(
{ denom: DENOM, amount: BOND_AMOUNT }, { denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey, registryConfig.privateKey,
fee registryConfig.fee
); );
for await (const name of authorityNames) { 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); log('Reserved authority name:', name);
await registry.setAuthorityBond( await registry.setAuthorityBond(
{ name, bondId }, { name, bondId },
registryConfig.privateKey, registryConfig.privateKey,
fee registryConfig.fee
); );
log(`Bond ${bondId} set for authority ${name}`); log(`Bond ${bondId} set for authority ${name}`);
} }

View File

@ -2,20 +2,22 @@ import debug from 'debug';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import path from 'path'; import path from 'path';
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk'; import { Registry } from '@snowballtools/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils'; import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment'; import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records'); const log = debug('snowball:publish-deploy-records');
async function main() { async function main () {
const { registryConfig, database, misc } = await getConfig(); const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry( const registry = new Registry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
{ chainId: registryConfig.chainId } registryConfig.chainId
); );
const dataSource = new DataSource({ const dataSource = new DataSource({
@ -38,7 +40,7 @@ async function main() {
}); });
for await (const deployment of deployments) { for await (const deployment of deployments) {
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`; const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
const applicationDeploymentRecord = { const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord', type: 'ApplicationDeploymentRecord',
@ -59,8 +61,6 @@ async function main() {
url url
}; };
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord( const result = await registry.setRecord(
{ {
privateKey: registryConfig.privateKey, privateKey: registryConfig.privateKey,
@ -68,12 +68,12 @@ async function main() {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
fee registryConfig.fee
); );
// Remove deployment for project subdomain if deployment is for production environment // Remove deployment for project subdomain if deployment is for production environment
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`; applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
await registry.setRecord( await registry.setRecord(
{ {
@ -82,12 +82,12 @@ async function main() {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
fee registryConfig.fee
); );
} }
log('Application deployment record data:', applicationDeploymentRecord); log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.id}`); log(`Application deployment record published: ${result.data.id}`);
} }
} }

View File

@ -2,20 +2,22 @@ import debug from 'debug';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import path from 'path'; 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 { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deployment-removal-records'); const log = debug('snowball:publish-deployment-removal-records');
async function main () { async function main () {
const { registryConfig, database } = await getConfig(); const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry( const registry = new Registry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
{ chainId: registryConfig.chainId } registryConfig.chainId
); );
const dataSource = new DataSource({ const dataSource = new DataSource({
@ -45,8 +47,6 @@ async function main () {
request: deployment.applicationDeploymentRemovalRequestId, request: deployment.applicationDeploymentRemovalRequestId,
} }
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord( const result = await registry.setRecord(
{ {
privateKey: registryConfig.privateKey, privateKey: registryConfig.privateKey,
@ -54,11 +54,11 @@ async function main () {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
fee registryConfig.fee
); );
log('Application deployment removal record data:', applicationDeploymentRemovalRecord); log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
log(`Application deployment removal record published: ${result.id}`); log(`Application deployment removal record published: ${result.data.id}`);
} }
} }

View File

@ -1,3 +0,0 @@
REGISTRY_BOND_ID=
DEPLOYER_LRN=
AUTHORITY=

View File

@ -1 +0,0 @@
records/*.yml

View File

@ -17,7 +17,7 @@
```bash ```bash
./deploy-frontend.staging.sh ./deploy-frontend.staging.sh
``` ```
- To deploy frontend app to `dashboard.apps.snowballtools.com` - To deploy frontend app to `dashboard.apps.snowballtools.com`
```bash ```bash
@ -51,14 +51,14 @@
```bash ```bash
# Account balance # Account balance
yarn laconic registry account get yarn laconic cns account get
# Bond balance # Bond balance
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
``` ```
- Command to refill bond - Command to refill bond
```bash ```bash
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000 yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
``` ```

View File

@ -1,10 +1,9 @@
services: services:
registry: cns:
restEndpoint: 'http://console.laconic.com:1317' restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api' gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: gas: 1200000
fees: fees: 200000aphoton
gasPrice: 1

View File

@ -1,8 +1,9 @@
services: services:
registry: cns:
rpcEndpoint: https://laconicd-sapo.laconic.com restEndpoint: http://console.laconic.com:1317
gqlEndpoint: https://laconicd-sapo.laconic.com/api gqlEndpoint: http://console.laconic.com:9473/api
userKey: userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
bondId: bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
chainId: laconic-testnet-2 chainId: laconic_9000-1
gasPrice: 0.001alnt gas: 1200000
fees: 200000aphoton

View File

@ -1,18 +1,10 @@
#!/bin/bash #!/bin/bash
source .env
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Using AUTHORITY: $AUTHORITY"
# Repository URL # Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base" REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash for a branch # Get the latest commit hash from the repository
BRANCH_NAME="main" LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
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 # Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json) PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
@ -21,17 +13,40 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
CURRENT_DATE_TIME=$(date -u) CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=config.yml CONFIG_FILE=config.yml
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts # 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 # 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}') NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-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 if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1 NEW_APPLICATION_VERSION=0.0.1
fi fi
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version # Generate application-record.yml with incremented version
cat >./records/application-record.yml <<EOF cat >./records/application-record.yml <<EOF
record: record:
@ -40,16 +55,16 @@ record:
repository_ref: $LATEST_HASH repository_ref: $LATEST_HASH
repository: ["$REPO_URL"] repository: ["$REPO_URL"]
app_type: webapp app_type: webapp
name: deploy-frontend name: snowballtools-base-frontend
app_version: $PACKAGE_VERSION app_version: $PACKAGE_VERSION
EOF EOF
echo "Files generated successfully" echo "Files generated successfully."
RECORD_FILE=records/application-record.yml RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord # Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE) publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record" echo "FATAL: Failed to publish record"
@ -60,17 +75,17 @@ echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend" REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}" echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc exit $rc
fi fi
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash" echo "FATAL: Failed to set hash"
@ -78,16 +93,16 @@ if [ $rc -ne 0 ]; then
fi fi
sleep 2 sleep 2
# Set name if latest release # Set name if latest release
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release" echo "FATAL: Failed to set release"
exit $rc exit $rc
fi fi
echo "$REGISTRY_APP_LRN set for ApplicationRecord" echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_LRN # Check if record found for REGISTRY_APP_CRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN") query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name" echo "FATAL: Failed to query name"
@ -95,58 +110,14 @@ if [ $rc -ne 0 ]; then
fi fi
APP_RECORD=$(echo $query_response | jq '.[0]') APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then 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 exit 1
fi 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 RECORD_FILE=records/application-deployment-request.yml
sleep 2 sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE) deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request" echo "FATAL: Failed to query deployment request"
exit $rc exit $rc

View File

@ -0,0 +1,133 @@
#!/bin/bash
# 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.staging.yml
REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661"
# 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 cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-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-deployment-request.yml
cat >./staging-records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat >./staging-records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns 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_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${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 cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
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_CRN."
exit 1
fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns 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

View File

@ -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

View File

@ -4,6 +4,6 @@
"main": "index.js", "main": "index.js",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@cerc-io/laconic-registry-cli": "^0.2.9" "@snowballtools/laconic-registry-cli": "^0.1.13"
} }
} }

View File

@ -0,0 +1,18 @@
record:
type: ApplicationDeploymentRequest
version: "1.0.0"
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e

View File

@ -0,0 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8

View File

@ -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"

View File

@ -0,0 +1,24 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@0.0.0
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ Sat Jun 22 02:49:45 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601

View File

@ -0,0 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: 0.0.0

View File

@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
APP_NAME=deployment-test-app APP_NAME=deployment-test-app
# Get latest version from registry and increment application-record version # 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}') NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns 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 if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
@ -51,7 +51,7 @@ record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: '1.0.0'
name: $APP_NAME@$PACKAGE_VERSION name: $APP_NAME@$PACKAGE_VERSION
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
dns: deployment-ci-test dns: deployment-ci-test
config: config:
env: env:
@ -67,31 +67,31 @@ EOF
echo "Record files generated successfully." echo "Record files generated successfully."
# Publish ApplicationRecord # Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id') RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published" echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME" REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2 sleep 2
# Set name if latest release # Set name if latest release
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_LRN set for ApplicationRecord" echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record exists for REGISTRY_APP_LRN # Check if record exists for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]') APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then 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 exit 1
fi fi
sleep 2 sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id') DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published" echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID echo $DEPLOYMENT_REQUEST_ID
@ -102,7 +102,7 @@ MAX_RETRIES=20
# Check that a ApplicationDeploymentRecord is published # Check that a ApplicationDeploymentRecord is published
retry_count=0 retry_count=0
while true; do 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) deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
len_deployment_records=$(echo $deployment_records_response | jq 'length') len_deployment_records=$(echo $deployment_records_response | jq 'length')
# Check if number of records returned is 0 # Check if number of records returned is 0
@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
retry_count=0 retry_count=0
max_retries=10 max_retries=10
retry_interval=10 retry_interval=5
while true; do while true; do
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url) url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
if [ "$url_response" = "200" ]; then if [ "$url_response" = "200" ]; then
@ -170,14 +170,14 @@ record:
EOF EOF
sleep 2 sleep 2
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id') REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRemovalRequest published" echo "ApplicationDeploymentRemovalRequest published"
echo $REMOVAL_REQUEST_ID echo $REMOVAL_REQUEST_ID
# Check that an ApplicationDeploymentRemovalRecord is published # Check that an ApplicationDeploymentRemovalRecord is published
retry_count=0 retry_count=0
while true; do while true; do
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID) removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
len_removal_records=$(echo $removal_records_response | jq 'length') len_removal_records=$(echo $removal_records_response | jq 'length')
# Check if number of records returned is 0 # Check if number of records returned is 0

View File

@ -3,16 +3,15 @@ VITE_SERVER_URL='http://localhost:8000'
VITE_GITHUB_CLIENT_ID= VITE_GITHUB_CLIENT_ID=
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app" 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_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
VITE_WALLET_CONNECT_ID= VITE_WALLET_CONNECT_ID=
VITE_LIT_RELAY_API_KEY= VITE_LIT_RELAY_API_KEY=
VITE_ALCHEMY_API_KEY=
VITE_BUGSNAG_API_KEY= VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID= VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL= VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID=
VITE_LACONICD_CHAIN_ID=
VITE_WALLET_IFRAME_URL=

View File

@ -44,7 +44,7 @@ yarn dev
## Deployment ## Deployment
From the root of the project, From the root of the project, run:
### Staging ### Staging

View File

@ -1,21 +1,19 @@
<!doctype html> <!doctype html>
<html lang="en" class="dark dark:bg-background dark:text-foreground"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="laconic tools dashboard" /> <meta name="description" content="snowball tools dashboard" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#2d89ef" /> <meta name="msapplication-TileColor" content="#2d89ef" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<title>Laconic</title> <title>Snowball</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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "1.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000", "dev": "vite --port 3000",
@ -16,11 +16,8 @@
"@bugsnag/browser-performance": "^2.4.1", "@bugsnag/browser-performance": "^2.4.1",
"@bugsnag/js": "^7.22.7", "@bugsnag/js": "^7.22.7",
"@bugsnag/plugin-react": "^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-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16", "@fontsource/inter": "^5.0.16",
"@mui/material": "^6.1.3",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@ -30,6 +27,10 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@snowballtools/auth": "^0.2.0",
"@snowballtools/auth-lit": "^0.2.0",
"@snowballtools/js-sdk": "^0.1.1",
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
"@snowballtools/material-tailwind-react-fork": "^2.1.10", "@snowballtools/material-tailwind-react-fork": "^2.1.10",
"@snowballtools/smartwallet-alchemy-light": "^0.2.0", "@snowballtools/smartwallet-alchemy-light": "^0.2.0",
"@snowballtools/types": "^0.2.0", "@snowballtools/types": "^0.2.0",
@ -41,9 +42,9 @@
"@turnkey/http": "^2.10.0", "@turnkey/http": "^2.10.0",
"@turnkey/sdk-react": "^0.1.0", "@turnkey/sdk-react": "^0.1.0",
"@turnkey/webauthn-stamper": "^0.5.0", "@turnkey/webauthn-stamper": "^0.5.0",
"@walletconnect/ethereum-provider": "^2.16.1", "@walletconnect/ethereum-provider": "^2.12.2",
"@web3modal/siwe": "4.0.5", "@web3modal/siwe": "^4.0.5",
"@web3modal/wagmi": "4.0.5", "@web3modal/wagmi": "^4.0.5",
"assert": "^2.1.0", "assert": "^2.1.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@ -64,12 +65,11 @@
"react-oauth-popup": "^1.0.5", "react-oauth-popup": "^1.0.5",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-timer-hook": "^3.0.7", "react-timer-hook": "^3.0.7",
"siwe": "2.1.4", "siwe": "^2.1.4",
"tailwind-variants": "^0.2.0", "tailwind-variants": "^0.2.0",
"usehooks-ts": "^2.15.1", "usehooks-ts": "^2.15.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"viem": "^2.7.11", "viem": "^2.7.11",
"wagmi": "2.5.7",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1 +0,0 @@
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -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

View File

@ -1,10 +1 @@
<svg width="115" height="20" viewBox="0 0 115 20" fill="none" xmlns="http://www.w3.org/2000/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>
<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>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,5 +1,5 @@
{ {
"name": "Laconic Tools Dashboard", "name": "Snowball Tools Dashboard",
"short_name": "snowball tools", "short_name": "snowball tools",
"icons": [ "icons": [
{ {

View File

@ -12,7 +12,7 @@ import Index from './pages';
import AuthPage from './pages/AuthPage'; import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout'; import { DashboardLayout } from './pages/org-slug/layout';
import Web3Provider from 'context/Web3Provider'; import Web3Provider from 'context/Web3Provider';
import { BASE_URL } from 'utils/constants'; import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -50,26 +50,25 @@ const router = createBrowserRouter([
path: '/login', path: '/login',
element: <AuthPage />, element: <AuthPage />,
}, },
{
path: '/signup',
element: <AuthPage />,
},
]); ]);
function App() { function App() {
// Hacky way of checking session // Hacky way of checking session
// TODO: Handle redirect backs // TODO: Handle redirect backs
useEffect(() => { useEffect(() => {
fetch(`${BASE_URL}/auth/session`, { fetch(`${baseUrl}/auth/session`, {
credentials: 'include', credentials: 'include',
}).then((res) => { }).then((res) => {
const path = window.location.pathname;
if (res.status !== 200) { if (res.status !== 200) {
localStorage.clear(); localStorage.clear();
const path = window.location.pathname;
if (path !== '/login') { if (path !== '/login' && path !== '/signup') {
window.location.pathname = '/login'; window.location.pathname = '/login';
} }
} else {
if (path === '/login') {
window.location.pathname = '/';
}
} }
}); });
}, []); }, []);

View File

@ -1,14 +1,13 @@
import { import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO, VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO, VITE_GITHUB_PWA_TEMPLATE_REPO,
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO,
} from 'utils/constants'; } from 'utils/constants';
export default [ export default [
{ {
id: '1', id: '1',
name: 'Progressive Web App (PWA)', name: 'Progressive Web App (PWA)',
icon: 'web', icon: 'pwa',
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`, repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false, isComingSoon: false,
}, },
@ -21,9 +20,23 @@ export default [
}, },
{ {
id: '3', id: '3',
name: 'Next.js + React + TailwindCSS', name: 'Kotlin',
icon: 'web', icon: 'kotlin',
repoFullName: `${VITE_GITHUB_NEXT_APP_TEMPLATE_REPO}`, repoFullName: '',
isComingSoon: false, isComingSoon: true,
},
{
id: '4',
name: 'React Native',
icon: 'react-native',
repoFullName: '',
isComingSoon: true,
},
{
id: '5',
name: 'Swift',
icon: 'swift',
repoFullName: '',
isComingSoon: true,
}, },
]; ];

View File

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Heading } from './shared/Heading';
interface LogoProps { interface LogoProps {
orgSlug?: string; orgSlug?: string;
@ -8,7 +9,14 @@ export const Logo = ({ orgSlug }: LogoProps) => {
return ( return (
<Link to={`/${orgSlug}`}> <Link to={`/${orgSlug}`}>
<div className="flex items-center gap-3 px-0 lg:px-2"> <div className="flex items-center gap-3 px-0 lg:px-2">
<img src="/logo.svg" alt="Snowball Logo" /> <img
src="/logo.svg"
alt="Snowball Logo"
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
/>
<Heading className="lg:text-[24px] text-[19px] font-semibold">
Snowball
</Heading>
</div> </div>
</Link> </Link>
); );

View File

@ -10,7 +10,7 @@ const SearchBar: React.ForwardRefRenderFunction<
return ( return (
<div className="relative flex w-full"> <div className="relative flex w-full">
<Input <Input
leftIcon={<SearchIcon className="text-foreground-secondary" />} leftIcon={<SearchIcon />}
onChange={onChange} onChange={onChange}
value={value} value={value}
type="search" type="search"

View File

@ -24,8 +24,8 @@ const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
<div <div
className={`text-sm ${ className={`text-sm ${
activeStep === stepperValue.step activeStep === stepperValue.step
? 'text-black font-semibold dark:text-foreground' ? 'text-black font-semibold'
: 'text-gray-600 dark:text-foreground-secondary' : 'text-gray-600'
}`} }`}
> >
{stepperValue.label} {stepperValue.label}

View File

@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useStopwatch } from 'react-timer-hook'; import { useStopwatch } from 'react-timer-hook';
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond'; import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
@ -13,19 +12,14 @@ const setStopWatchOffset = (time: string) => {
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> { interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date; offsetTimestamp: Date;
isPaused: boolean;
} }
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => { const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
const { totalSeconds, pause, start } = useStopwatch({ const { totalSeconds } = useStopwatch({
autoStart: true, autoStart: true,
offsetTimestamp: offsetTimestamp, offsetTimestamp: offsetTimestamp,
}); });
useEffect(() => {
isPaused ? pause() : start();
}, [isPaused]);
return <FormatMillisecond time={totalSeconds * 1000} {...props} />; return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
}; };

View File

@ -10,16 +10,11 @@ import {
LinkChainIcon, LinkChainIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { TagProps } from 'components/shared/Tag'; import { TagProps } from 'components/shared/Tag';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps { interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
deployment: Deployment; deployment: Deployment;
newDeployment?: Deployment; newDeployment?: Deployment;
domains: Domain[]; domains: Domain[];
isConfirmButtonLoading?: boolean;
} }
export const ChangeStateToProductionDialog = ({ export const ChangeStateToProductionDialog = ({
@ -29,7 +24,6 @@ export const ChangeStateToProductionDialog = ({
open, open,
handleCancel, handleCancel,
handleConfirm, handleConfirm,
isConfirmButtonLoading,
...props ...props
}: ChangeStateToProductionDialogProps) => { }: ChangeStateToProductionDialogProps) => {
const currentChip = { const currentChip = {
@ -47,15 +41,6 @@ export const ChangeStateToProductionDialog = ({
handleCancel={handleCancel} handleCancel={handleCancel}
open={open} open={open}
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonTitle={isConfirmButtonLoading ? 'Redeploying' : 'Redeploy'}
confirmButtonProps={{
disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
),
}}
> >
<div className="flex flex-col gap-7"> <div className="flex flex-col gap-7">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">

View File

@ -1,47 +0,0 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
isConfirmButtonLoading?: boolean;
}
export const DeleteDeploymentDialog = ({
open,
handleCancel,
handleConfirm,
isConfirmButtonLoading,
...props
}: DeleteDeploymentDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Delete deployment?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle={
isConfirmButtonLoading
? 'Deleting deployment'
: 'Yes, delete deployment'
}
handleConfirm={handleConfirm}
confirmButtonProps={{
variant: 'danger',
disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
),
}}
>
<p className="text-sm text-elements-high-em">
Once deleted, the deployment will not be accessible.
</p>
</ConfirmDialog>
);
};

View File

@ -4,9 +4,7 @@ export const projectCardTheme = tv({
slots: { slots: {
wrapper: [ wrapper: [
'bg-surface-card', 'bg-surface-card',
'dark:bg-overlay2',
'shadow-card', 'shadow-card',
'dark:shadow-background',
'rounded-2xl', 'rounded-2xl',
'flex', 'flex',
'flex-col', 'flex-col',
@ -19,16 +17,10 @@ export const projectCardTheme = tv({
'text-sm', 'text-sm',
'font-medium', 'font-medium',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'tracking-[-0.006em]', 'tracking-[-0.006em]',
'truncate', 'truncate',
], ],
description: [ description: ['text-xs', 'text-elements-low-em', 'truncate'],
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
'truncate',
],
icons: ['flex', 'items-center', 'gap-1'], icons: ['flex', 'items-center', 'gap-1'],
lowerContent: [ lowerContent: [
'transition-colors', 'transition-colors',
@ -40,7 +32,6 @@ export const projectCardTheme = tv({
'gap-2', 'gap-2',
'rounded-b-2xl', 'rounded-b-2xl',
'group-hover:bg-surface-card-hovered', 'group-hover:bg-surface-card-hovered',
'dark:group-hover:bg-overlay3',
], ],
latestDeployment: ['flex', 'items-center', 'gap-2'], latestDeployment: ['flex', 'items-center', 'gap-2'],
deploymentStatusContainer: [ deploymentStatusContainer: [
@ -51,15 +42,10 @@ export const projectCardTheme = tv({
'justify-center', 'justify-center',
], ],
deploymentStatus: ['w-1', 'h-1', 'rounded-full'], deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
deploymentName: [ deploymentName: ['text-xs', 'text-elements-low-em'],
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
deploymentText: [ deploymentText: [
'text-xs', 'text-xs',
'text-elements-low-em', 'text-elements-low-em',
'dark:text-foreground-secondary',
'font-mono', 'font-mono',
'flex', 'flex',
'items-center', 'items-center',
@ -67,11 +53,9 @@ export const projectCardTheme = tv({
], ],
wavyBorder: [ wavyBorder: [
'bg-surface-card', 'bg-surface-card',
'dark:bg-background',
'transition-colors', 'transition-colors',
'duration-150', 'duration-150',
'group-hover:bg-surface-card-hovered', 'group-hover:bg-surface-card-hovered',
'dark:group-hover:bg-overlay2',
], ],
}, },
variants: { variants: {
@ -83,7 +67,7 @@ export const projectCardTheme = tv({
deploymentStatus: ['bg-orange-400'], deploymentStatus: ['bg-orange-400'],
}, },
failure: { failure: {
deploymentStatus: ['bg-error'], deploymentStatus: ['bg-rose-500'],
}, },
pending: { pending: {
deploymentStatus: ['bg-gray-500'], deploymentStatus: ['bg-gray-500'],

View File

@ -88,7 +88,7 @@ export const ProjectCard = ({
</div> </div>
{/* Icons */} {/* Icons */}
<div className={theme.icons()}> <div className={theme.icons()}>
{hasError && <WarningDiamondIcon className="text-error" />} {hasError && <WarningDiamondIcon className="text-elements-danger" />}
<Menu placement="bottom-end"> <Menu placement="bottom-end">
<MenuHandler> <MenuHandler>
<Button <Button
@ -101,15 +101,12 @@ export const ProjectCard = ({
<HorizontalDotIcon /> <HorizontalDotIcon />
</Button> </Button>
</MenuHandler> </MenuHandler>
<MenuList className="dark:bg-overlay3 dark:shadow-background dark:border-none"> <MenuList>
<MenuItem <MenuItem onClick={navigateToSettingsOnClick}>
onClick={navigateToSettingsOnClick}
className="text-foreground"
>
Project settings Project settings
</MenuItem> </MenuItem>
<MenuItem <MenuItem
className="text-error" className="text-red-500"
onClick={navigateToSettingsOnClick} onClick={navigateToSettingsOnClick}
> >
Delete project Delete project

View File

@ -59,12 +59,12 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
}, [fetchProjects, debouncedInputValue]); }, [fetchProjects, debouncedInputValue]);
return ( return (
<div className="relative w-full lg:w-fit dark:bg-overlay"> <div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} /> <SearchBar {...getInputProps()} />
<div <div
{...getMenuProps({}, { suppressRefError: true })} {...getMenuProps({}, { suppressRefError: true })}
className={cn( className={cn(
'flex flex-col shadow-dropdown rounded-xl dark:bg-overlay2 bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50', 'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen }, { hidden: !inputValue || !isOpen },
)} )}
> >

View File

@ -13,10 +13,10 @@ export const ProjectSearchBarEmpty = ({
{...props} {...props}
className={cn('flex items-center px-2 py-2 gap-3', className)} className={cn('flex items-center px-2 py-2 gap-3', className)}
> >
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning dark:bg-red-50 text-error"> <div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
<InfoRoundFilledIcon size={16} /> <InfoRoundFilledIcon size={16} />
</div> </div>
<p className="text-elements-low-em text-sm dark:text-foreground-secondary tracking-[-0.006em]"> <p className="text-elements-low-em text-sm tracking-[-0.006em]">
No projects matching this name No projects matching this name
</p> </p>
</div> </div>

View File

@ -1,60 +0,0 @@
import {
Select,
Option,
Spinner,
} from '@snowballtools/material-tailwind-react-fork';
const AccountsDropdown = ({
accounts,
isDataReceived,
onAccountChange,
}: {
accounts: string[];
isDataReceived: boolean;
onAccountChange: (selectedAccount: string) => void;
}) => {
return (
<div className="p-6 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6 shadow-md">
{isDataReceived ? (
!accounts.length ? (
<div className="text-center">
<p className="text-gray-700 dark:text-gray-300 mb-4">
No accounts found. Please visit{' '}
<a
href="https://store.laconic.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline dark:text-blue-400"
>
store.laconic.com
</a>{' '}
to create a wallet.
</p>
</div>
) : (
<div>
<Select
label="Select Account"
defaultValue={accounts[0]}
onChange={(value) => value && onAccountChange(value)}
className="dark:bg-overlay2 dark:text-foreground"
aria-label="Wallet Account Selector"
>
{accounts.map((account, index) => (
<Option key={index} value={account}>
{account}
</Option>
))}
</Select>
</div>
)
) : (
<div className="flex items-center justify-center h-12">
<Spinner className="h-6 w-6" />
</div>
)}
</div>
);
};
export default AccountsDropdown;

View File

@ -1,630 +0,0 @@
import { useCallback, useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { FormProvider, FieldValues } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
import {
AddEnvironmentVariableInput,
AuctionParams,
Deployer,
} from 'gql-client';
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext';
import IFrameModal from './IFrameModal';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
import AccountsDropdown from './AccountsDropdown';
type ConfigureDeploymentFormValues = {
option: string;
lrn?: string;
numProviders?: number;
maxPrice?: string;
};
type ConfigureFormValues = ConfigureDeploymentFormValues &
EnvironmentVariablesFormValues;
const DEFAULT_MAX_PRICE = '10000';
const TX_APPROVAL_TIMEOUT_MS = 60000;
const Configure = () => {
const [isLoading, setIsLoading] = useState(false);
const [deployers, setDeployers] = useState<Deployer[]>([]);
const [selectedAccount, setSelectedAccount] = useState<string>();
const [accounts, setAccounts] = useState<string[]>([]);
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentDone, setIsPaymentDone] = useState(false);
const [isFrameVisible, setIsFrameVisible] = useState(false);
const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false);
const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId');
const queryParams = new URLSearchParams(location.search);
const owner = queryParams.get('owner');
const name = queryParams.get('name');
const defaultBranch = queryParams.get('defaultBranch');
const fullName = queryParams.get('fullName');
const orgSlug = queryParams.get('orgSlug');
const templateOwner = queryParams.get('templateOwner');
const templateRepo = queryParams.get('templateRepo');
const isPrivate = queryParams.get('isPrivate') === 'true';
const navigate = useNavigate();
const { toast, dismiss } = useToast();
const client = useGQLClient();
const methods = useForm<ConfigureFormValues>({
defaultValues: {
option: 'Auction',
maxPrice: DEFAULT_MAX_PRICE,
lrn: '',
numProviders: 1,
variables: [],
},
});
const selectedOption = methods.watch('option');
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
const createProject = async (
data: FieldValues,
envVariables: AddEnvironmentVariableInput[],
senderAddress: string,
txHash: string,
): Promise<string> => {
setIsLoading(true);
let projectId: string | null = null;
try {
let lrn: string | undefined;
let auctionParams: AuctionParams | undefined;
if (data.option === 'LRN') {
lrn = data.lrn;
} else if (data.option === 'Auction') {
auctionParams = {
numProviders: Number(data.numProviders!),
maxPrice: data.maxPrice!.toString(),
};
}
if (templateId) {
const projectData: any = {
templateOwner,
templateRepo,
owner,
name,
isPrivate,
paymentAddress: senderAddress,
txHash,
};
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug!,
projectData,
lrn,
auctionParams,
envVariables,
);
projectId = addProjectFromTemplate.id;
} else {
const { addProject } = await client.addProject(
orgSlug!,
{
name: `${owner}-${name}`,
prodBranch: defaultBranch!,
repository: fullName!,
template: 'webapp',
paymentAddress: senderAddress,
txHash,
},
lrn,
auctionParams,
envVariables,
);
projectId = addProject.id;
}
} catch (error) {
console.error('Error creating project:', error);
toast({
id: 'error-creating-project',
title: 'Error creating project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
if (projectId) {
return projectId;
} else {
throw new Error('Project creation failed');
}
};
const verifyTx = async (
senderAddress: string,
txHash: string,
amount: string,
): Promise<boolean> => {
const isValid = await client.verifyTx(
txHash,
`${amount.toString()}alnt`,
senderAddress,
);
return isValid;
};
const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => {
try {
const deployerLrn = createFormData.lrn;
const deployer = deployers.find(
(deployer) => deployer.deployerLrn === deployerLrn,
);
let amount: string;
let senderAddress: string;
let txHash: string | null = null;
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
toast({
id: 'no-payment-required',
title: 'No payment required. Deploying app...',
variant: 'info',
onDismiss: dismiss,
});
txHash = '';
senderAddress = '';
} else {
if (!selectedAccount) return;
senderAddress = selectedAccount;
if (createFormData.option === 'LRN') {
amount = deployer?.minimumPayment!;
} else {
amount = (
createFormData.numProviders * createFormData.maxPrice
).toString();
}
const amountToBePaid = amount.replace(/\D/g, '').toString();
txHash = await cosmosSendTokensHandler(senderAddress, amountToBePaid);
if (!txHash) {
toast({
id: 'unsuccessful-tx',
title: 'Transaction rejected',
variant: 'error',
onDismiss: dismiss,
});
setIsFrameVisible(false);
setIsPaymentLoading(false);
throw new Error('Transaction rejected');
}
// Validate transaction hash
const isTxHashValid = await verifyTx(
senderAddress,
txHash,
amountToBePaid,
);
setIsPaymentLoading(false);
if (isTxHashValid) {
toast({
id: 'payment-successful',
title: 'Payment successful',
variant: 'success',
onDismiss: dismiss,
});
setIsPaymentDone(true);
} else {
toast({
id: 'invalid-tx-hash',
title: 'Transaction validation failed',
variant: 'error',
onDismiss: dismiss,
});
throw new Error('Transaction validation failed');
}
}
const environmentVariables = createFormData.variables.map(
(variable: any) => {
return {
key: variable.key,
value: variable.value,
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
},
);
const projectId = await createProject(
createFormData,
environmentVariables,
senderAddress,
txHash!,
);
await client.getEnvironmentVariables(projectId);
if (templateId) {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
);
} else {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
);
}
} catch (error: any) {
toast({
id: 'error-deploying-app',
title: 'Error deploying app',
variant: 'error',
onDismiss: dismiss,
});
throw new Error(error);
}
},
[client, createProject, dismiss, toast],
);
const fetchDeployers = useCallback(async () => {
const res = await client.getDeployers();
setDeployers(res.deployers);
}, [client]);
const onAccountChange = useCallback((account: string) => {
setSelectedAccount(account);
}, []);
const onDeployerChange = useCallback(
(selectedLrn: string) => {
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
setSelectedDeployer(deployer);
},
[deployers],
);
const cosmosSendTokensHandler = useCallback(
async (selectedAccount: string, amount: string) => {
if (!selectedAccount) {
throw new Error('Account not selected');
}
const senderAddress = selectedAccount;
const snowballAddress = await client.getAddress();
let timeoutId;
try {
setIsPaymentDone(false);
setIsPaymentLoading(true);
await requestTx(senderAddress, snowballAddress, amount);
const txHash = await new Promise<string | null>((resolve, reject) => {
const handleTxStatus = async (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'TRANSACTION_RESPONSE') {
const txResponse = event.data.data;
resolve(txResponse);
} else if (event.data.type === 'ERROR') {
console.error('Error from wallet:', event.data.message);
reject(new Error('Transaction failed'));
toast({
id: 'error-transaction',
title: 'Error during transaction',
variant: 'error',
onDismiss: dismiss,
});
}
setIsFrameVisible(false);
window.removeEventListener('message', handleTxStatus);
};
window.addEventListener('message', handleTxStatus);
// Set a timeout, consider unsuccessful after 1 min
timeoutId = setTimeout(() => {
reject(new Error('Transaction timeout'));
window.removeEventListener('message', handleTxStatus);
toast({
id: 'transaction-timeout',
title: 'The transaction request timed out. Please try again',
variant: 'error',
onDismiss: dismiss,
});
setIsFrameVisible(false);
setIsPaymentLoading(false);
}, TX_APPROVAL_TIMEOUT_MS);
});
return txHash;
} catch (error) {
console.error('Error in transaction:', error);
throw new Error('Error in transaction');
} finally {
clearTimeout(timeoutId);
}
},
[client, dismiss, toast],
);
const requestTx = async (
sender: string,
recipient: string,
amount: string,
) => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (!iframe.contentWindow) {
console.error('Iframe not found or not loaded');
throw new Error('Iframe not found or not loaded');
}
iframe.contentWindow.postMessage(
{
type: 'REQUEST_TX',
chainId: VITE_LACONICD_CHAIN_ID,
fromAddress: sender,
toAddress: recipient,
amount,
},
VITE_WALLET_IFRAME_URL,
);
setIsFrameVisible(true);
};
useEffect(() => {
fetchDeployers();
}, []);
return (
<div className="space-y-7 px-4 py-6">
<div className="flex justify-between mb-6">
<div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium">
Configure deployment
</Heading>
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondaryu"
>
The app can be deployed by setting the deployer LRN for a single
deployment or by creating a deployer auction for multiple
deployments
</Heading>
</div>
</div>
<div className="flex flex-col gap-6 lg:gap-8 w-full">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleFormSubmit)}>
<div className="flex flex-col justify-start gap-4 mb-6">
<Controller
name="option"
control={methods.control}
render={({ field: { value, onChange } }) => (
<Select
value={value}
onChange={(event) => onChange(event.target.value)}
size="small"
displayEmpty
className="dark:bg-overlay2 dark:text-foreground"
sx={{
fontFamily: 'inherit',
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#e0e0e0',
borderRadius: '8px',
},
}}
>
<MenuItem value="Auction">Create Auction</MenuItem>
<MenuItem value="LRN">Deployer LRN</MenuItem>
</Select>
)}
/>
</div>
{selectedOption === 'LRN' && (
<div className="flex flex-col justify-start gap-4 mb-6">
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
>
The app will be deployed by the configured deployer
</Heading>
<Controller
name="lrn"
control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange }, fieldState }) => (
<FormControl fullWidth error={Boolean(fieldState.error)}>
<span className="text-sm dark:text-foreground text-elements-high-em dark:text-foreground mb-4">
Select deployer LRN
</span>
<Select
value={value}
onChange={(event) => {
onChange(event.target.value);
onDeployerChange(event.target.value);
}}
displayEmpty
size="small"
className="dark:bg-overlay2 dark:text-foreground"
>
{deployers.map((deployer) => (
<MenuItem
key={deployer.deployerLrn}
value={deployer.deployerLrn}
>
{`${deployer.deployerLrn} ${deployer.minimumPayment ? `(${deployer.minimumPayment})` : ''}`}
</MenuItem>
))}
</Select>
{fieldState.error && (
<FormHelperText>
{fieldState.error.message}
</FormHelperText>
)}
</FormControl>
)}
/>
</div>
)}
{selectedOption === 'Auction' && (
<>
<div className="flex flex-col justify-start gap-4 mb-6">
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
>
Set the number of deployers and maximum price for each
deployment
</Heading>
<span className="text-sm text-elements-high-em dark:text-foreground">
Number of Deployers
</span>
<Controller
name="numProviders"
control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Input
type="number"
value={value}
onChange={(e) => onChange(e)}
/>
)}
/>
</div>
<div className="flex flex-col justify-start gap-4 mb-6">
<span className="text-sm text-elements-high-em dark:text-foreground">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<Heading as="h4" className="md:text-lg font-medium mb-3">
Environment Variables
</Heading>
<div className="p-4 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6">
<EnvironmentVariablesForm />
</div>
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
<div>
<Button
{...buttonSize}
type="submit"
disabled={isLoading || !selectedDeployer || !selectedAccount}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{isLoading ? 'Deploying' : 'Deploy'}
</Button>
</div>
) : (
<>
<AccountsDropdown
accounts={accounts}
onAccountChange={onAccountChange}
isDataReceived={isAccountsDataReceived}
/>
{accounts.length > 0 && (
<div>
<Button
{...buttonSize}
type="submit"
shape="default"
disabled={
isLoading || isPaymentLoading || !selectedAccount
}
rightIcon={
isLoading || isPaymentLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{!isPaymentDone
? isPaymentLoading
? 'Transaction Requested'
: 'Pay and Deploy'
: isLoading
? 'Deploying'
: 'Deploy'}
</Button>
</div>
)}
</>
)}
</form>
</FormProvider>
<IFrameModal
setAccounts={setAccounts}
setIsDataReceived={setIsAccountsDataReceived}
isVisible={isFrameVisible}
/>
</div>
</div>
);
};
export default Configure;

View File

@ -6,6 +6,7 @@ import { Button } from '../../shared/Button';
import { import {
GitIcon, GitIcon,
EllipsesIcon, EllipsesIcon,
SnowballIcon,
GithubIcon, GithubIcon,
GitTeaIcon, GitTeaIcon,
} from '../../shared/CustomIcon'; } from '../../shared/CustomIcon';
@ -14,9 +15,8 @@ import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard'; import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants'; import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
import { LaconicIcon } from 'components/shared/CustomIcon/LaconicIcon';
const SCOPES = 'public_repo user'; const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`; const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface { interface ConnectAccountInterface {
@ -46,24 +46,20 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
// TODO: Use correct height // TODO: Use correct height
return ( return (
<div className="dark:bg-overlay bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl"> <div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="flex flex-col items-center max-w-[420px]"> <div className="flex flex-col items-center max-w-[420px]">
{/** Icons */} {/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7"> <div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<IconWithFrame icon={<GitIcon />} hasHighlight={false} /> <IconWithFrame icon={<GitIcon />} />
<EllipsesIcon className="items-center gap-1.5 flex" /> <EllipsesIcon className="items-center gap-1.5 flex" />
<IconWithFrame <IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
className="bg-background"
icon={<LaconicIcon />}
hasHighlight={false}
/>
</div> </div>
{/** Text */} {/** Text */}
<div className="flex flex-col gap-1.5 mb-6"> <div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium dark:text-foreground"> <Heading className="text-xl font-medium">
Connect to your Git account Connect to your Git account
</Heading> </Heading>
<p className="text-center text-elements-mid-em dark:text-foreground-secondary"> <p className="text-center text-elements-mid-em">
Once connected, you can import a repository from your account or Once connected, you can import a repository from your account or
start with one of our templates. start with one of our templates.
</p> </p>
@ -74,14 +70,14 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
url={GITHUB_OAUTH_URL} url={GITHUB_OAUTH_URL}
onCode={handleCode} onCode={handleCode}
onClose={() => {}} onClose={() => {}}
title="Laconic" title="Snowball"
width={1000} width={1000}
height={1000} height={1000}
> >
<Button <Button
className="w-full sm:w-auto" className="w-full sm:w-auto"
leftIcon={<GithubIcon />} leftIcon={<GithubIcon />}
variant="primary" variant="tertiary"
> >
Connect to GitHub Connect to GitHub
</Button> </Button>
@ -89,7 +85,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
<Button <Button
className="w-full sm:w-auto" className="w-full sm:w-auto"
leftIcon={<GitTeaIcon />} leftIcon={<GitTeaIcon />}
variant="primary" variant="tertiary"
> >
Connect to GitTea Connect to GitTea
</Button> </Button>

View File

@ -1,7 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import axios from 'axios';
import { Deployment } from 'gql-client';
import { DeployStep, DeployStatus } from './DeployStep'; import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
@ -9,37 +7,13 @@ import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button'; import { Button } from '../../shared/Button';
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon'; import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog'; import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
import { useGQLClient } from 'context/GQLClientContext';
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
type RequestState =
| 'SUBMITTED'
| 'DEPLOYING'
| 'DEPLOYED'
| 'REMOVED'
| 'CANCELLED'
| 'ERROR';
type Record = {
id: string;
createTime: string;
app: string;
lastState: RequestState;
lastUpdate: string;
logAvailable: boolean;
};
const TIMEOUT_DURATION = 5000;
const Deploy = () => { const Deploy = () => {
const client = useGQLClient();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId'); const projectId = searchParams.get('projectId');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [deployment, setDeployment] = useState<Deployment>();
const [record, setRecord] = useState<Record>();
const handleOpen = () => setOpen(!open); const handleOpen = () => setOpen(!open);
const navigate = useNavigate(); const navigate = useNavigate();
@ -49,67 +23,13 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`); navigate(`/${orgSlug}/projects/create`);
}, []); }, []);
const isDeploymentFailed = useMemo(() => {
if (!record) {
return false;
}
// Not checking for `REMOVED` status as this status is received for a brief period before receiving `DEPLOYED` status
if (record.lastState === 'CANCELLED' || record.lastState === 'ERROR') {
return true;
} else {
return false;
}
}, [record]);
const fetchDeploymentRecords = useCallback(async () => {
if (!deployment) {
return;
}
try {
const response = await axios.get(
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
);
const record: Record = response.data;
setRecord(record);
} catch (err: any) {
console.log('Error fetching data from deployer', err);
}
}, [deployment]);
const fetchDeployment = useCallback(async () => {
if (!projectId) {
return;
}
const { deployments } = await client.getDeployments(projectId);
setDeployment(deployments[0]);
}, [client, projectId]);
useEffect(() => { useEffect(() => {
fetchDeployment(); const timerID = setTimeout(() => {
fetchDeploymentRecords();
const interval = setInterval(() => {
fetchDeploymentRecords();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployment, fetchDeploymentRecords]);
useEffect(() => {
if (!record) {
return;
}
if (record.lastState === 'DEPLOYED') {
navigate(`/${orgSlug}/projects/create/success/${projectId}`); navigate(`/${orgSlug}/projects/create/success/${projectId}`);
} }, TIMEOUT_DURATION);
}, [record]);
return () => clearInterval(timerID);
}, []);
return ( return (
<div className="space-y-7"> <div className="space-y-7">
@ -122,7 +42,6 @@ const Deploy = () => {
<ClockOutlineIcon size={16} className="text-elements-mid-em" /> <ClockOutlineIcon size={16} className="text-elements-mid-em" />
<Stopwatch <Stopwatch
offsetTimestamp={setStopWatchOffset(Date.now().toString())} offsetTimestamp={setStopWatchOffset(Date.now().toString())}
isPaused={isDeploymentFailed}
/> />
</div> </div>
</div> </div>
@ -141,36 +60,30 @@ const Deploy = () => {
/> />
</div> </div>
{!isDeploymentFailed ? ( <div>
<div> <DeployStep
<DeployStep title="Building"
title={record ? 'Submitted' : 'Submitting'} status={DeployStatus.COMPLETE}
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING} step="1"
step="1" processTime="72000"
/> />
<DeployStep
<DeployStep title="Deployment summary"
title={ status={DeployStatus.PROCESSING}
record && record.lastState === 'DEPLOYED' step="2"
? 'Deployed' startTime={Date.now().toString()}
: 'Deploying' />
} <DeployStep
status={ title="Running checks"
!record status={DeployStatus.NOT_STARTED}
? DeployStatus.NOT_STARTED step="3"
: record.lastState === 'DEPLOYED' />
? DeployStatus.COMPLETE <DeployStep
: DeployStatus.PROCESSING title="Assigning domains"
} status={DeployStatus.NOT_STARTED}
step="2" step="4"
startTime={Date.now().toString()} />
/> </div>
</div>
) : (
<div>
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,16 +1,27 @@
import { useState } from 'react';
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import FormatMillisecond from '../../FormatMilliSecond';
import processLogs from '../../../assets/process-logs.json';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { import {
CheckRoundFilledIcon, CheckRoundFilledIcon,
ClockOutlineIcon, ClockOutlineIcon,
CopyIcon,
LoaderIcon, LoaderIcon,
MinusCircleIcon,
PlusIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useIntersectionObserver } from 'usehooks-ts';
enum DeployStatus { enum DeployStatus {
PROCESSING = 'progress', PROCESSING = 'progress',
COMPLETE = 'complete', COMPLETE = 'complete',
NOT_STARTED = 'notStarted', NOT_STARTED = 'notStarted',
ERROR = 'error',
} }
interface DeployStepsProps { interface DeployStepsProps {
@ -21,11 +32,35 @@ interface DeployStepsProps {
processTime?: string; processTime?: string;
} }
const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => { const DeployStep = ({
step,
status,
title,
startTime,
processTime,
}: DeployStepsProps) => {
const [isOpen, setIsOpen] = useState(false);
const { toast, dismiss } = useToast();
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
threshold: 1,
});
const disableCollapse = status !== DeployStatus.COMPLETE;
return ( return (
<div className="border-b border-border-separator"> <div className="border-b border-border-separator">
{/* Collapisble trigger */}
<button <button
className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')} className={cn(
'flex justify-between w-full py-5 gap-2',
disableCollapse && 'cursor-auto',
)}
tabIndex={disableCollapse ? -1 : undefined}
onClick={() => {
if (!disableCollapse) {
setIsOpen((val) => !val);
}
}}
> >
<div className={cn('grow flex items-center gap-3')}> <div className={cn('grow flex items-center gap-3')}>
{/* Icon */} {/* Icon */}
@ -36,7 +71,13 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
</div> </div>
)} )}
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<LoaderIcon className="animate-spin text-elements-link dark:text-foreground" /> <LoaderIcon className="animate-spin text-elements-link" />
)}
{status === DeployStatus.COMPLETE && (
<div className="text-controls-primary">
{!isOpen && <PlusIcon size={24} />}
{isOpen && <MinusCircleIcon size={24} />}
</div>
)} )}
</div> </div>
@ -44,8 +85,7 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
<span <span
className={cn( className={cn(
'text-left text-sm md:text-base', 'text-left text-sm md:text-base',
status === DeployStatus.PROCESSING && status === DeployStatus.PROCESSING && 'text-elements-link',
'text-elements-link dark:text-foreground',
)} )}
> >
{title} {title}
@ -55,14 +95,8 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
{/* Timer */} {/* Timer */}
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ClockOutlineIcon <ClockOutlineIcon size={16} className="text-elements-low-em" />
size={16} <Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
className="text-elements-low-em dark:text-foreground-secondary"
/>
<Stopwatch
offsetTimestamp={setStopWatchOffset(startTime!)}
isPaused={false}
/>
</div> </div>
)} )}
{status === DeployStatus.COMPLETE && ( {status === DeployStatus.COMPLETE && (
@ -73,9 +107,51 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
size={15} size={15}
/> />
</div> </div>
<FormatMillisecond time={Number(processTime)} />{' '}
</div> </div>
)} )}
</button> </button>
{/* Collapsible */}
<Collapse open={isOpen}>
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
{/* Logs */}
{processLogs.map((log, key) => {
return (
<p className="font-mono" key={key}>
{log}
</p>
);
})}
{/* End of logs ref used for hiding gradient overlay */}
<div ref={ref} />
{/* Overflow gradient overlay */}
{!hideGradientOverlay && (
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
)}
{/* Copy log button */}
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
<Button
size="xs"
onClick={() => {
navigator.clipboard.writeText(processLogs.join('\n'));
toast({
title: 'Logs copied',
variant: 'success',
id: 'logs',
onDismiss: dismiss,
});
}}
leftIcon={<CopyIcon size={16} />}
>
Copy log
</Button>
</div>
</div>
</Collapse>
</div> </div>
); );
}; };

View File

@ -1,88 +0,0 @@
import { useCallback, useEffect } from 'react';
import { Box, Modal } from '@mui/material';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
const IFrameModal = ({
setAccounts,
setIsDataReceived,
isVisible,
}: {
setAccounts: (accounts: string[]) => void;
setIsDataReceived: (isReceived: boolean) => void;
isVisible: boolean;
}) => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
setIsDataReceived(true);
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
setAccounts(event.data.data);
} else if (event.data.type === 'ERROR') {
console.error('Error from wallet:', event.data.message);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
const getDataFromWallet = useCallback(() => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (!iframe.contentWindow) {
console.error('Iframe not found or not loaded');
return;
}
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: VITE_LACONICD_CHAIN_ID,
},
VITE_WALLET_IFRAME_URL,
);
}, []);
return (
<Modal open={isVisible} disableEscapeKeyDown keepMounted>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90%',
maxWidth: '1200px',
height: '600px',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: 24,
borderRadius: '8px',
outline: 'none',
bgcolor: 'background.paper',
}}
>
<iframe
onLoad={getDataFromWallet}
id="walletIframe"
src={`${VITE_WALLET_IFRAME_URL}/wallet-embed`}
width="100%"
height="100%"
sandbox="allow-scripts allow-same-origin"
className="border rounded-md shadow-sm"
></iframe>
</Box>
</Modal>
);
};
export default IFrameModal;

View File

@ -1,8 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { SegmentedControls } from 'components/shared/SegmentedControls'; import { SegmentedControls } from 'components/shared/SegmentedControls';
import { useState } from 'react'; import { useState } from 'react';
import { useMediaQuery } from 'usehooks-ts';
import { import {
GithubIcon, GithubIcon,
LockIcon, LockIcon,
@ -10,7 +8,7 @@ import {
TemplateIconType, TemplateIconType,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { relativeTimeISO } from 'utils/time'; import { relativeTimeISO } from 'utils/time';
import templates from 'assets/templates'; import { useMediaQuery } from 'usehooks-ts';
export const MockConnectGitCard = () => { export const MockConnectGitCard = () => {
const [segmentedControlsValue, setSegmentedControlsValue] = const [segmentedControlsValue, setSegmentedControlsValue] =
@ -48,6 +46,29 @@ export const MockConnectGitCard = () => {
}, },
]; ];
const TEMPLATE_CONTENT = [
{
name: 'Web app',
icon: 'web',
},
{
name: 'Progressive Web App (PWA)',
icon: 'pwa',
},
{
name: 'React Native',
icon: 'react-native',
},
{
name: 'Kotlin',
icon: 'kotlin',
},
{
name: 'Swift',
icon: 'swift',
},
];
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
if (segmentedControlsValue === 'import') { if (segmentedControlsValue === 'import') {
return ( return (
@ -65,7 +86,7 @@ export const MockConnectGitCard = () => {
} }
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0"> <div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
{templates.map((template, index) => ( {TEMPLATE_CONTENT.map((template, index) => (
<MockTemplateCard key={index} {...template} /> <MockTemplateCard key={index} {...template} />
))} ))}
</div> </div>
@ -73,7 +94,7 @@ export const MockConnectGitCard = () => {
}, [segmentedControlsValue]); }, [segmentedControlsValue]);
return ( return (
<div className="relative dark:bg-overlay bg-base-bg shadow-card dark:shadow-background rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2"> <div className="relative bg-base-bg shadow-card rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2">
{/* Content */} {/* Content */}
<SegmentedControls <SegmentedControls
value={segmentedControlsValue} value={segmentedControlsValue}
@ -85,7 +106,7 @@ export const MockConnectGitCard = () => {
{renderContent} {renderContent}
{/* Shade */} {/* Shade */}
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent" /> <div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white to-transparent" />
</div> </div>
); );
}; };
@ -100,18 +121,18 @@ const MockProjectCard = ({
visibility?: string; visibility?: string;
}) => { }) => {
return ( return (
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative"> <div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative">
{/* Icon container */} {/* Icon container */}
<div className="w-10 h-10 bg-base-bg dark:bg-background rounded-md justify-center items-center flex"> <div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
<GithubIcon /> <GithubIcon />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex flex-1 gap-3 flex-wrap"> <div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<p className="text-elements-high-em text-sm dark:text-foreground font-medium tracking-[-0.006em]"> <p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
{full_name} {full_name}
</p> </p>
<p className="text-elements-low-em text-xs dark:text-foreground-secondary"> <p className="text-elements-low-em text-xs">
{updated_at && relativeTimeISO(updated_at)} {updated_at && relativeTimeISO(updated_at)}
</p> </p>
</div> </div>
@ -128,13 +149,13 @@ const MockProjectCard = ({
const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => { const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => {
return ( return (
<div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized dark:hover:bg-background relative rounded-2xl group relative cursor-default"> <div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized rounded-2xl group relative cursor-default">
{/* Icon */} {/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg dark:bg-background border border-border-interactive/10 shadow-card-sm"> <div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
<TemplateIcon type={icon as TemplateIconType} /> <TemplateIcon type={icon as TemplateIconType} />
</div> </div>
{/* Name */} {/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em dark:text-foreground"> <p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
{name} {name}
</p> </p>
</div> </div>

View File

@ -38,9 +38,36 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
}); });
} }
navigate( try {
`configure?owner=${repository.owner?.login}&name=${repository.name}&defaultBranch=${repository.default_branch}&fullName=${repository.full_name}&orgSlug=${orgSlug}`, setIsLoading(true);
); const { addProject } = await client.addProject(orgSlug, {
name: `${repository.owner?.login}-${repository.name}`,
prodBranch: repository.default_branch as string,
repository: repository.full_name,
// TODO: Compute template from repo
template: 'webapp',
});
if (addProject) {
navigate(`import?projectId=${addProject.id}`);
} else {
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
}
} catch (error) {
console.error((error as Error).message);
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
}, [client, repository, orgSlug, setIsLoading, navigate, toast]); }, [client, repository, orgSlug, setIsLoading, navigate, toast]);
return ( return (
@ -49,21 +76,21 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
onClick={createProject} onClick={createProject}
> >
{/* Icon container */} {/* Icon container */}
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex dark:bg-overlay"> <div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
<GithubIcon /> <GithubIcon />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex flex-1 gap-3 flex-wrap"> <div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="text-elements-high-em dark:text-foreground text-sm font-medium tracking-[-0.006em]"> <p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
{repository.full_name} {repository.full_name}
</p> </p>
<p className="text-elements-low-em dark:text-foreground-secondary text-xs"> <p className="text-elements-low-em text-xs">
{repository.updated_at && relativeTimeISO(repository.updated_at)} {repository.updated_at && relativeTimeISO(repository.updated_at)}
</p> </p>
</div> </div>
{repository.visibility === 'private' && ( {repository.visibility === 'private' && (
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 dark:text-error h-fit"> <div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
<LockIcon /> <LockIcon />
Private Private
</div> </div>

View File

@ -64,9 +64,9 @@ export const RepositoryList = () => {
// Check if selected account is an organization // Check if selected account is an organization
if (selectedAccount.value === gitUser.login) { if (selectedAccount.value === gitUser.login) {
query = query + ` user:${selectedAccount.value}`; query = query + ` user:${selectedAccount}`;
} else { } else {
query = query + ` org:${selectedAccount.value}`; query = query + ` org:${selectedAccount}`;
} }
const result = await octokit.rest.search.repos({ const result = await octokit.rest.search.repos({
@ -166,9 +166,7 @@ export const RepositoryList = () => {
</div> </div>
) : ( ) : (
<div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center"> <div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center">
<p className="text-elements-high-em dark:text-foreground font-sans"> <p className="text-elements-high-em font-sans">No repository found</p>
No repository found
</p>
<Button <Button
variant="tertiary" variant="tertiary"
leftIcon={<RefreshIcon />} leftIcon={<RefreshIcon />}

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback } from 'react';
import { import {
Deployment, Deployment,
DeploymentStatus, DeploymentStatus,
@ -6,15 +6,6 @@ import {
Environment, Environment,
Project, Project,
} from 'gql-client'; } from 'gql-client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Tooltip,
} from '@mui/material';
import { Avatar } from 'components/shared/Avatar'; import { Avatar } from 'components/shared/Avatar';
import { import {
BranchStrokeIcon, BranchStrokeIcon,
@ -27,23 +18,12 @@ import {
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { OverflownText } from 'components/shared/OverflownText'; import { OverflownText } from 'components/shared/OverflownText';
import { Tag, TagTheme } from 'components/shared/Tag'; import { Tag, TagTheme } from 'components/shared/Tag';
import { Button } from 'components/shared/Button';
import { getInitials } from 'utils/geInitials'; import { getInitials } from 'utils/geInitials';
import { relativeTimeMs } from 'utils/time'; import { relativeTimeMs } from 'utils/time';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants'; import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format'; import { formatAddress } from '../../../../utils/format';
import { DeploymentMenu } from './DeploymentMenu'; import { DeploymentMenu } from './DeploymentMenu';
const DEPLOYMENT_LOGS_STYLE = {
backgroundColor: 'rgba(0,0,0, .9)',
padding: '2em',
borderRadius: '0.5em',
marginLeft: '0.5em',
marginRight: '0.5em',
color: 'gray',
fontSize: 'small',
};
interface DeployDetailsCardProps { interface DeployDetailsCardProps {
deployment: Deployment; deployment: Deployment;
currentDeployment: Deployment; currentDeployment: Deployment;
@ -68,14 +48,6 @@ const DeploymentDetailsCard = ({
project, project,
prodBranchDomains, prodBranchDomains,
}: DeployDetailsCardProps) => { }: DeployDetailsCardProps) => {
const [openDialog, setOpenDialog] = useState<boolean>(false);
const [deploymentLogs, setDeploymentLogs] = useState<string>(
'No deployment logs available',
);
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
const getIconByDeploymentStatus = (status: DeploymentStatus) => { const getIconByDeploymentStatus = (status: DeploymentStatus) => {
if ( if (
status === DeploymentStatus.Building || status === DeploymentStatus.Building ||
@ -92,47 +64,26 @@ const DeploymentDetailsCard = ({
} }
}; };
const fetchDeploymentLogs = useCallback(async () => {
setDeploymentLogs('Loading logs...');
handleOpenDialog();
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
(res) => res.json(),
);
if (!statusRes.logAvailable) {
setDeploymentLogs(statusRes.lastState);
} else {
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
res.text(),
);
setDeploymentLogs(logsRes);
}
}, [deployment.deployer, deployment.applicationDeploymentRequestId]);
const renderDeploymentStatus = useCallback( const renderDeploymentStatus = useCallback(
(className?: string) => { (className?: string) => {
return ( return (
<Tooltip title="Click to view build logs"> <div className={className}>
<div className={className} style={{ cursor: 'pointer' }}> <Tag
<Tag leftIcon={getIconByDeploymentStatus(deployment.status)}
leftIcon={getIconByDeploymentStatus(deployment.status)} size="xs"
size="xs" type={STATUS_COLORS[deployment.status] ?? 'neutral'}
type={STATUS_COLORS[deployment.status] ?? 'neutral'} >
onClick={fetchDeploymentLogs} {deployment.status}
> </Tag>
{deployment.status} </div>
</Tag>
</div>
</Tooltip>
); );
}, },
[deployment.status, deployment.commitHash, fetchDeploymentLogs], [deployment.status, deployment.commitHash],
); );
return ( return (
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative"> <div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]"> <div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full"> <div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
{/* DEPLOYMENT URL */} {/* DEPLOYMENT URL */}
{deployment.url && ( {deployment.url && (
@ -145,12 +96,7 @@ const DeploymentDetailsCard = ({
</OverflownText> </OverflownText>
</Heading> </Heading>
)} )}
{deployment.deployer.deployerLrn && ( <span className="text-sm text-elements-low-em tracking-tight">
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
Deployer LRN: {deployment.deployer.deployerLrn}
</span>
)}
<span className="text-sm text-elements-low-em tracking-tight block">
{deployment.environment === Environment.Production {deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}` ? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'} : 'Preview'}
@ -216,20 +162,6 @@ const DeploymentDetailsCard = ({
prodBranchDomains={prodBranchDomains} prodBranchDomains={prodBranchDomains}
/> />
</div> </div>
<Dialog
open={openDialog}
onClose={handleCloseDialog}
fullWidth
maxWidth="md"
>
<DialogTitle>Deployment logs</DialogTitle>
<DialogContent style={DEPLOYMENT_LOGS_STYLE}>
{deploymentLogs && <pre>{deploymentLogs}</pre>}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Close</Button>
</DialogActions>
</Dialog>
</div> </div>
); );
}; };

View File

@ -23,7 +23,6 @@ import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> { interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
deployment: Deployment; deployment: Deployment;
@ -47,16 +46,12 @@ export const DeploymentMenu = ({
const [changeToProduction, setChangeToProduction] = useState(false); const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false); const [redeployToProduction, setRedeployToProduction] = useState(false);
const [deleteDeploymentDialog, setDeleteDeploymentDialog] = useState(false);
const [isConfirmDeleteLoading, setIsConfirmDeleteLoading] = useState(false);
const [rollbackDeployment, setRollbackDeployment] = useState(false); const [rollbackDeployment, setRollbackDeployment] = useState(false);
const [assignDomainDialog, setAssignDomainDialog] = useState(false); const [assignDomainDialog, setAssignDomainDialog] = useState(false);
const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] =
useState(false);
const updateDeployment = async () => { const updateDeployment = async () => {
const isUpdated = await client.updateDeploymentToProd(deployment.id); const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated.updateDeploymentToProd) { if (isUpdated) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_changed_to_production', id: 'deployment_changed_to_production',
@ -76,8 +71,7 @@ export const DeploymentMenu = ({
const redeployToProd = async () => { const redeployToProd = async () => {
const isRedeployed = await client.redeployToProd(deployment.id); const isRedeployed = await client.redeployToProd(deployment.id);
setConfirmButtonLoadingLoading(false); if (isRedeployed) {
if (isRedeployed.redeployToProd) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'redeployed_to_production', id: 'redeployed_to_production',
@ -100,7 +94,7 @@ export const DeploymentMenu = ({
project.id, project.id,
deployment.id, deployment.id,
); );
if (isRollbacked.rollbackDeployment) { if (isRollbacked) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_rolled_back', id: 'deployment_rolled_back',
@ -120,15 +114,11 @@ export const DeploymentMenu = ({
const deleteDeployment = async () => { const deleteDeployment = async () => {
const isDeleted = await client.deleteDeployment(deployment.id); const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) {
setIsConfirmDeleteLoading(false);
setDeleteDeploymentDialog((preVal) => !preVal);
if (isDeleted.deleteDeployment) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_removal_requested', id: 'deployment_deleted',
title: 'Deployment removal requested', title: 'Deployment deleted',
variant: 'success', variant: 'success',
onDismiss: dismiss, onDismiss: dismiss,
}); });
@ -212,7 +202,7 @@ export const DeploymentMenu = ({
</MenuItem> </MenuItem>
<MenuItem <MenuItem
className="hover:bg-base-bg-emphasized flex items-center gap-3" className="hover:bg-base-bg-emphasized flex items-center gap-3"
onClick={() => setDeleteDeploymentDialog((preVal) => !preVal)} onClick={() => deleteDeployment()}
> >
<CrossCircleIcon /> Delete deployment <CrossCircleIcon /> Delete deployment
</MenuItem> </MenuItem>
@ -238,13 +228,11 @@ export const DeploymentMenu = ({
open={redeployToProduction} open={redeployToProduction}
confirmButtonTitle="Redeploy" confirmButtonTitle="Redeploy"
handleConfirm={async () => { handleConfirm={async () => {
setConfirmButtonLoadingLoading(true);
await redeployToProd(); await redeployToProd();
setRedeployToProduction((preVal) => !preVal); setRedeployToProduction((preVal) => !preVal);
}} }}
deployment={deployment} deployment={deployment}
domains={deployment.domain ? [deployment.domain] : []} domains={deployment.domain ? [deployment.domain] : []}
isConfirmButtonLoading={isConfirmButtonLoading}
/> />
{Boolean(currentDeployment) && ( {Boolean(currentDeployment) && (
<ChangeStateToProductionDialog <ChangeStateToProductionDialog
@ -265,15 +253,6 @@ export const DeploymentMenu = ({
open={assignDomainDialog} open={assignDomainDialog}
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)} handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
/> />
<DeleteDeploymentDialog
open={deleteDeploymentDialog}
handleConfirm={async () => {
setIsConfirmDeleteLoading(true);
await deleteDeployment();
}}
handleCancel={() => setDeleteDeploymentDialog((preVal) => !preVal)}
isConfirmButtonLoading={isConfirmDeleteLoading}
/>
</> </>
); );
}; };

View File

@ -16,7 +16,7 @@ export const Activity = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium">Activity</Heading> <Heading className="text-lg leading-6 font-medium">Activity</Heading>
<Button variant="tertiary" size="sm"> <Button variant="tertiary" size="sm">
SEE ALL See all
</Button> </Button>
</div> </div>
<div className="mt-5"> <div className="mt-5">

View File

@ -1,182 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { Auction, Deployer, Project } from 'gql-client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import {
CheckRoundFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Tag } from 'components/shared';
const WAIT_DURATION = 5000;
const DIALOG_STYLE = {
padding: '2em',
borderRadius: '0.5em',
marginLeft: '0.5em',
marginRight: '0.5em',
color: 'gray',
fontSize: 'small',
};
export const AuctionCard = ({ project }: { project: Project }) => {
const [auctionStatus, setAuctionStatus] = useState<string>('');
const [deployers, setDeployers] = useState<Deployer[]>([]);
const [fundsStatus, setFundsStatus] = useState<boolean>(false);
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
const [openDialog, setOpenDialog] = useState<boolean>(false);
const client = useGQLClient();
const getIconByAuctionStatus = (status: string) =>
status === 'completed' ? (
<CheckRoundFilledIcon />
) : (
<LoadingIcon className="animate-spin" />
);
const checkAuctionStatus = useCallback(async () => {
const result = await client.getAuctionData(project.auctionId);
setAuctionStatus(result.status);
setAuctionDetails(result);
}, [project.auctionId, project.deployers, project.fundsReleased]);
const fetchUpdatedProject = useCallback(async () => {
const updatedProject = await client.getProject(project.id);
setDeployers(updatedProject.project!.deployers!);
setFundsStatus(updatedProject.project!.fundsReleased!);
}, [project.id]);
const fetchData = useCallback(async () => {
await Promise.all([checkAuctionStatus(), fetchUpdatedProject()]);
}, [checkAuctionStatus, fetchUpdatedProject]);
useEffect(() => {
fetchData();
const timerId = setInterval(() => {
fetchData();
}, WAIT_DURATION);
return () => clearInterval(timerId);
}, [fetchData]);
const renderAuctionStatus = useCallback(
() => (
<Tag
leftIcon={getIconByAuctionStatus(auctionStatus)}
size="xs"
type={auctionStatus === 'completed' ? 'positive' : 'emphasized'}
>
{auctionStatus.toUpperCase()}
</Tag>
),
[auctionStatus],
);
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
return (
<>
<div className="p-3 gap-2 rounded-xl border dark:border-overlay3 border-gray-200 transition-colors hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col mt-8">
<div className="flex justify-between items-center">
<Heading className="text-lg leading-6 font-medium">
Auction details
</Heading>
<Button onClick={handleOpenDialog} variant="tertiary" size="sm">
VIEW DETAILS
</Button>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Id
</span>
<span className="text-elements-mid-em dark:text-foreground text-sm text-right">
{project.auctionId}
</span>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Status
</span>
<div className="ml-2">{renderAuctionStatus()}</div>
</div>
{auctionStatus === 'completed' && (
<>
{deployers?.length > 0 ? (
<div>
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer LRNs
</span>
{deployers.map((deployer, index) => (
<p
key={index}
className="text-elements-mid-em dark:text-foreground text-sm"
>
{'\u2022'} {deployer.deployerLrn}
</p>
))}
<div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer Funds Status
</span>
<div className="ml-2">
<Tag
size="xs"
type={fundsStatus ? 'positive' : 'emphasized'}
>
{fundsStatus ? 'RELEASED' : 'WAITING'}
</Tag>
</div>
</div>
</div>
) : (
<div className="mt-3">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
No winning deployers
</span>
</div>
)}
</>
)}
</div>
<Dialog
open={openDialog}
onClose={handleCloseDialog}
fullWidth
maxWidth="md"
PaperProps={{
className: 'dark:bg-overlay2',
}}
>
<DialogTitle className="dark:text-foreground">
Auction Details
</DialogTitle>
<DialogContent style={DIALOG_STYLE}>
{auctionDetails && (
<pre className="dark:text-foreground-secondary">
{JSON.stringify(auctionDetails, null, 2)}
</pre>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} shape="default">
CLOSE
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@ -18,7 +18,7 @@ export const OverviewInfo = ({
return ( return (
<div className="flex justify-between gap-2 py-3 text-sm items-center"> <div className="flex justify-between gap-2 py-3 text-sm items-center">
<div className="flex gap-2 items-center text-elements-high-em dark:text-foreground-secondary"> <div className="flex gap-2 items-center text-elements-high-em">
{styledIcon} {styledIcon}
{label} {label}
</div> </div>

View File

@ -93,11 +93,11 @@ const AddMemberDialog = ({
/> />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button onClick={handleOpen} variant="danger" shape="default"> <Button onClick={handleOpen} variant="secondary">
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={!isValid} shape="default"> <Button type="submit" disabled={!isValid}>
SEND INVITE Send invite
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</form> </form>

View File

@ -61,9 +61,7 @@ const DeleteProjectDialog = ({
<Input <Input
label={ label={
"Deleting your project is irreversible. Enter your project's name " + "Deleting your project is irreversible. Enter your project's name " +
'"' +
project.name + project.name +
'"' +
' below to confirm you want to permanently delete it:' ' below to confirm you want to permanently delete it:'
} }
id="input" id="input"

View File

@ -24,7 +24,7 @@ const DisplayEnvironmentVariables = ({
return ( return (
<> <>
<div <div
className="flex gap-4 p-2 dark:text-foreground" className="flex gap-4 p-2"
onClick={() => setOpenCollapse((cur) => !cur)} onClick={() => setOpenCollapse((cur) => !cur)}
> >
{openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />} {openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />}
@ -33,7 +33,7 @@ const DisplayEnvironmentVariables = ({
</div> </div>
<Collapse open={openCollapse}> <Collapse open={openCollapse}>
{variables.length === 0 ? ( {variables.length === 0 ? (
<div className="bg-slate-100 dark:bg-overlay2 dark:text-foreground rounded-xl flex-col p-4"> <div className="bg-slate-100 rounded-xl flex-col p-4">
No environment variables added yet. Once you add them, they'll show No environment variables added yet. Once you add them, they'll show
up here. up here.
</div> </div>

View File

@ -17,7 +17,6 @@ const GitSelectionSection = ({
<div className="grow">Github</div> <div className="grow">Github</div>
<div>{'>'}</div> <div>{'>'}</div>
</div> </div>
{/*
<div <div
className="flex gap-4 border-b-2 border-gray-200 cursor-pointer p-1" className="flex gap-4 border-b-2 border-gray-200 cursor-pointer p-1"
onClick={() => {}} onClick={() => {}}
@ -26,7 +25,6 @@ const GitSelectionSection = ({
<div className="grow">Gitea</div> <div className="grow">Gitea</div>
<div>{'>'}</div> <div>{'>'}</div>
</div> </div>
*/}
</> </>
); );
}; };

View File

@ -80,7 +80,7 @@ const MemberCard = ({
return ( return (
<div <div
className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'} dark:text-foreground`} className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
> >
<div className="basis-1/2"> <div className="basis-1/2">
{member.name && ( {member.name && (

View File

@ -102,8 +102,8 @@ const SetupDomain = () => {
)} )}
<div className="self-stretch"> <div className="self-stretch">
<Button disabled={!isValid} type="submit" shape="default"> <Button disabled={!isValid} type="submit">
NEXT Next
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -30,12 +30,7 @@ export const avatarTheme = tv(
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'], fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
}, },
blue: { blue: {
fallback: [ fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'],
'text-elements-info',
'bg-base-bg-emphasized-info',
'dark:text-foreground',
'dark:bg-primary',
],
}, },
}, },
size: { size: {

View File

@ -16,7 +16,6 @@ export const buttonTheme = tv(
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
'transition-colors', 'transition-colors',
'duration-150', 'duration-150',
'font-mono',
], ],
variants: { variants: {
size: { size: {
@ -29,7 +28,7 @@ export const buttonTheme = tv(
true: 'w-full', true: 'w-full',
}, },
shape: { shape: {
default: 'rounded', default: 'rounded-lg',
rounded: 'rounded-full', rounded: 'rounded-full',
}, },
iconOnly: { iconOnly: {
@ -40,21 +39,21 @@ export const buttonTheme = tv(
'text-elements-on-primary', 'text-elements-on-primary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-primary', 'bg-controls-primary',
'shadow-button', 'shadow-button',
'hover:bg-primary-hovered', 'hover:bg-controls-primary-hovered',
'focus-visible:bg-primary-hovered', 'focus-visible:bg-controls-primary-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled', 'disabled:bg-controls-disabled',
'disabled:border-transparent', 'disabled:border-transparent',
'disabled:shadow-none', 'disabled:shadow-none',
], ],
secondary: [ secondary: [
'text-primary', 'text-elements-on-secondary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-secondary', 'bg-controls-secondary',
'hover:bg-overlay2', 'hover:bg-controls-secondary-hovered',
'focus-visible:bg-controls-secondary-hovered', 'focus-visible:bg-controls-secondary-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled', 'disabled:bg-controls-disabled',
@ -78,12 +77,10 @@ export const buttonTheme = tv(
], ],
ghost: [ ghost: [
'text-elements-on-tertiary', 'text-elements-on-tertiary',
'dark:text-foreground-secondary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-transparent', 'bg-transparent',
'hover:bg-controls-tertiary-hovered', 'hover:bg-controls-tertiary-hovered',
'dark:hover:bg-overlay2',
'hover:border-border-interactive-hovered', 'hover:border-border-interactive-hovered',
'focus-visible:bg-controls-tertiary-hovered', 'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered', 'focus-visible:border-border-interactive-hovered',
@ -96,7 +93,7 @@ export const buttonTheme = tv(
'text-elements-on-danger', 'text-elements-on-danger',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-error', 'bg-border-danger',
'hover:bg-controls-danger-hovered', 'hover:bg-controls-danger-hovered',
'focus-visible:bg-controls-danger-hovered', 'focus-visible:bg-controls-danger-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',

View File

@ -40,11 +40,11 @@ abbr[title] {
} }
.react-calendar__tile { .react-calendar__tile {
@apply h-12 w-12 text-elements-high-em dark:text-foreground; @apply h-12 w-12 text-elements-high-em;
} }
.react-calendar__tile:hover { .react-calendar__tile:hover {
@apply bg-base-bg-emphasized dark:bg-overlay3 rounded-lg; @apply bg-base-bg-emphasized rounded-lg;
} }
.react-calendar__tile:focus-visible { .react-calendar__tile:focus-visible {
@ -52,7 +52,7 @@ abbr[title] {
} }
.react-calendar__tile--now { .react-calendar__tile--now {
@apply bg-base-bg-emphasized dark:bg-overlay3 text-elements-high-em rounded-lg; @apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
} }
.react-calendar__tile--now:hover { .react-calendar__tile--now:hover {
@ -77,7 +77,7 @@ abbr[title] {
/* Range -- START */ /* Range -- START */
.react-calendar__tile--range { .react-calendar__tile--range {
@apply bg-controls-secondary dark:bg-overlay3 text-elements-on-secondary rounded-none; @apply bg-controls-secondary text-elements-on-secondary rounded-none;
} }
.react-calendar__tile--range:hover { .react-calendar__tile--range:hover {
@ -89,7 +89,7 @@ abbr[title] {
} }
.react-calendar__tile--rangeStart { .react-calendar__tile--rangeStart {
@apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg; @apply bg-controls-primary text-elements-on-primary rounded-lg;
} }
.react-calendar__tile--rangeStart:hover { .react-calendar__tile--rangeStart:hover {
@ -101,7 +101,7 @@ abbr[title] {
} }
.react-calendar__tile--rangeEnd { .react-calendar__tile--rangeEnd {
@apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg; @apply bg-controls-primary text-elements-on-primary rounded-lg;
} }
.react-calendar__tile--rangeEnd:hover { .react-calendar__tile--rangeEnd:hover {

View File

@ -5,9 +5,7 @@ export const calendarTheme = tv({
wrapper: [ wrapper: [
'max-w-[352px]', 'max-w-[352px]',
'bg-surface-floating', 'bg-surface-floating',
'dark:bg-overlay2',
'shadow-dropdown', 'shadow-dropdown',
'dark:shadow-background',
'rounded-xl', 'rounded-xl',
], ],
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'], calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
@ -30,12 +28,9 @@ export const calendarTheme = tv({
'border', 'border',
'border-border-interactive', 'border-border-interactive',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'shadow-field', 'shadow-field',
'bg-white', 'bg-white',
'dark:bg-overlay3',
'hover:bg-base-bg-alternate', 'hover:bg-base-bg-alternate',
'dark:hover:bg-foreground-secondary',
'focus-visible:bg-base-bg-alternate', 'focus-visible:bg-base-bg-alternate',
], ],
footer: [ footer: [

View File

@ -280,7 +280,6 @@ export const Calendar = ({
showNavigation={false} showNavigation={false}
selectRange={selectRange} selectRange={selectRange}
onChange={handleChange} onChange={handleChange}
// tileClassName="dark:text-foreground-secondary dark:hover:bg-overlay3"
onClickMonth={(date) => handleChangeNavigation('month', date)} onClickMonth={(date) => handleChangeNavigation('month', date)}
onClickYear={(date) => handleChangeNavigation('year', date)} onClickYear={(date) => handleChangeNavigation('year', date)}
/> />
@ -298,20 +297,19 @@ export const Calendar = ({
) : ( ) : (
<> <>
{value && ( {value && (
<Button variant="danger" onClick={handleReset} shape="default"> <Button variant="danger" onClick={handleReset}>
RESET Reset
</Button> </Button>
)} )}
<div className="space-x-3"> <div className="space-x-3">
<Button variant="tertiary" onClick={onCancel} shape="default"> <Button variant="tertiary" onClick={onCancel}>
CANCEL Cancel
</Button> </Button>
<Button <Button
disabled={!value} disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)} onClick={() => (value ? onSelect?.(value) : null)}
shape="default"
> >
SELECT Select
</Button> </Button>
</div> </div>
</> </>

View File

@ -11,9 +11,7 @@ export const getCheckboxVariant = tv({
'focus-visible:text-controls-disabled', 'focus-visible:text-controls-disabled',
'group-focus-visible:text-controls-disabled', 'group-focus-visible:text-controls-disabled',
'data-[state=checked]:text-elements-on-primary', 'data-[state=checked]:text-elements-on-primary',
'dark:data-[state=checked]:text-foreground',
'data-[state=checked]:group-focus-visible:text-elements-on-primary', 'data-[state=checked]:group-focus-visible:text-elements-on-primary',
'dark:data-[state=checked]:group-focus-visible:text-foreground',
'data-[state=indeterminate]:text-elements-on-primary', 'data-[state=indeterminate]:text-elements-on-primary',
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active', 'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
], ],
@ -25,7 +23,6 @@ export const getCheckboxVariant = tv({
'border', 'border',
'border-border-interactive/10', 'border-border-interactive/10',
'bg-controls-tertiary', 'bg-controls-tertiary',
'dark:bg-background',
'rounded-md', 'rounded-md',
'transition-all', 'transition-all',
'duration-150', 'duration-150',
@ -33,13 +30,9 @@ export const getCheckboxVariant = tv({
'shadow-button', 'shadow-button',
'group-hover:border-border-interactive/[0.14]', 'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-tertiary', 'group-hover:bg-controls-tertiary',
'dark:group-hover:bg-overlay',
'data-[state=checked]:bg-controls-primary', 'data-[state=checked]:bg-controls-primary',
'dark:data-[state=checked]:bg-primary',
'data-[state=checked]:hover:bg-controls-primary-hovered', 'data-[state=checked]:hover:bg-controls-primary-hovered',
'dark:data-[state=checked]:hover:bg-primary-hovered',
'data-[state=checked]:focus-visible:bg-controls-primary-hovered', 'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
'dark:data-[state=checked]:focus-visible:bg-primary-hovered',
'data-[disabled]:bg-controls-disabled', 'data-[disabled]:bg-controls-disabled',
'data-[disabled]:shadow-none', 'data-[disabled]:shadow-none',
'data-[disabled]:hover:border-border-interactive/10', 'data-[disabled]:hover:border-border-interactive/10',
@ -50,17 +43,12 @@ export const getCheckboxVariant = tv({
'text-sm', 'text-sm',
'tracking-[-0.006em]', 'tracking-[-0.006em]',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'flex', 'flex',
'flex-col', 'flex-col',
'gap-1', 'gap-1',
'px-1', 'px-1',
], ],
description: [ description: ['text-xs', 'text-elements-low-em'],
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
}, },
variants: { variants: {
disabled: { disabled: {

Some files were not shown because too many files have changed in this diff Show More