forked from cerc-io/snowballtools-base
Compare commits
10 Commits
096318cf13
...
ea9a56eb65
Author | SHA1 | Date | |
---|---|---|---|
ea9a56eb65 | |||
05bd766133 | |||
0f18bc978e | |||
519e318190 | |||
63969ae25a | |||
b449c299dc | |||
2a35ec1cd5 | |||
be90fc76c1 | |||
3fa60f3cdf | |||
3d9aedeb7e |
@ -15,6 +15,7 @@ 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_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_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_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'
|
||||||
|
@ -20,16 +20,6 @@
|
|||||||
clientId = ""
|
clientId = ""
|
||||||
clientSecret = ""
|
clientSecret = ""
|
||||||
|
|
||||||
[google]
|
|
||||||
clientId = ""
|
|
||||||
clientSecret = ""
|
|
||||||
|
|
||||||
[turnkey]
|
|
||||||
apiBaseUrl = "https://api.turnkey.com"
|
|
||||||
apiPrivateKey = ""
|
|
||||||
apiPublicKey = ""
|
|
||||||
defaultOrganizationId = ""
|
|
||||||
|
|
||||||
[registryConfig]
|
[registryConfig]
|
||||||
fetchDeploymentRecordDelay = 5000
|
fetchDeploymentRecordDelay = 5000
|
||||||
checkAuctionStatusDelay = 5000
|
checkAuctionStatusDelay = 5000
|
||||||
@ -51,6 +41,3 @@
|
|||||||
revealFee = "100000"
|
revealFee = "100000"
|
||||||
revealsDuration = "120s"
|
revealsDuration = "120s"
|
||||||
denom = "alnt"
|
denom = "alnt"
|
||||||
|
|
||||||
[misc]
|
|
||||||
projectDomain = "apps.snowballtools.com"
|
|
||||||
|
@ -51,17 +51,12 @@ export interface AuctionConfig {
|
|||||||
denom: string;
|
denom: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiscConfig {
|
|
||||||
projectDomain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server: ServerConfig;
|
server: ServerConfig;
|
||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
auction: AuctionConfig;
|
auction: AuctionConfig;
|
||||||
misc: MiscConfig;
|
|
||||||
turnkey: {
|
turnkey: {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
apiPublicKey: string;
|
apiPublicKey: string;
|
||||||
|
@ -13,7 +13,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, MiscConfig } from './config';
|
import { DatabaseConfig } 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';
|
||||||
@ -34,9 +34,8 @@ 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, { projectDomain }: MiscConfig) {
|
constructor({ dbPath }: DatabaseConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@ -44,8 +43,6 @@ export class Database {
|
|||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false
|
logging: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.projectDomain = projectDomain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
@ -143,6 +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.deployers', 'deployers')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
@ -491,13 +489,13 @@ export class Database {
|
|||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveProject (project: Project): Promise<Project> {
|
async saveProject(project: Project): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
return projectRepository.save(project);
|
return projectRepository.save(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectById (
|
async updateProjectById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: DeepPartial<Project>
|
data: DeepPartial<Project>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -583,17 +581,22 @@ export class Database {
|
|||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployer (data: DeepPartial<Deployer>): Promise<Deployer> {
|
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
const newDomain = await deployerRepository.save(data);
|
const newDomain = await deployerRepository.save(data);
|
||||||
|
|
||||||
return newDomain;
|
return newDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployerById (deployerId: string): Promise<Deployer | null> {
|
async getDeployers(): Promise<Deployer[]> {
|
||||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployers = await deployerRepository.find();
|
||||||
|
return deployers;
|
||||||
|
}
|
||||||
|
|
||||||
const deployer = await deployerRepository.findOne({ where: { deployerId } });
|
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
||||||
|
|
||||||
return deployer;
|
return deployer;
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import { Project } from './Project';
|
|||||||
@Entity()
|
@Entity()
|
||||||
export class Deployer {
|
export class Deployer {
|
||||||
@PrimaryColumn('varchar')
|
@PrimaryColumn('varchar')
|
||||||
deployerId!: string;
|
deployerLrn!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
deployerLrn!: string;
|
deployerId!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
deployerApiUrl!: string;
|
deployerApiUrl!: string;
|
||||||
@ -15,6 +15,12 @@ export class Deployer {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
baseDomain!: string;
|
baseDomain!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
minimumPayment!: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
paymentAddress!: string | null;
|
||||||
|
|
||||||
@ManyToMany(() => Project, (project) => project.deployers)
|
@ManyToMany(() => Project, (project) => project.deployers)
|
||||||
projects!: Project[];
|
projects!: Project[];
|
||||||
}
|
}
|
||||||
|
@ -38,19 +38,17 @@ export interface ApplicationDeploymentRequest {
|
|||||||
auction?: 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;
|
||||||
@ -129,7 +127,7 @@ export class Deployment {
|
|||||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||||
|
|
||||||
@ManyToOne(() => Deployer)
|
@ManyToOne(() => Deployer)
|
||||||
@JoinColumn({ name: 'deployerId' })
|
@JoinColumn({ name: 'deployerLrn' })
|
||||||
deployer!: Deployer;
|
deployer!: Deployer;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
@ -52,6 +52,10 @@ export class Project {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
auctionId!: string | null;
|
auctionId!: string | null;
|
||||||
|
|
||||||
|
// Tx hash for sending coins from snowball to deployer
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
txHash!: string | null;
|
||||||
|
|
||||||
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
deployers!: Deployer[]
|
deployers!: Deployer[]
|
||||||
@ -66,6 +70,10 @@ 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'
|
||||||
})
|
})
|
||||||
|
@ -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, misc } = await getConfig();
|
const { server, database, gitHub, registryConfig } = 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, misc);
|
const db = new Database(database);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
|
@ -5,7 +5,8 @@ import { Octokit } from 'octokit';
|
|||||||
import { inc as semverInc } from 'semver';
|
import { inc as semverInc } from 'semver';
|
||||||
import { DeepPartial } from 'typeorm';
|
import { DeepPartial } from 'typeorm';
|
||||||
|
|
||||||
import { Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||||
|
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
@ -15,7 +16,7 @@ import {
|
|||||||
ApplicationDeploymentRemovalRequest
|
ApplicationDeploymentRemovalRequest
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
||||||
import { getConfig, getRepoDetails, sleep } from './utils';
|
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
@ -108,14 +109,16 @@ export class Registry {
|
|||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setRecord(
|
||||||
privateKey: this.registryConfig.privateKey,
|
{
|
||||||
record: applicationRecord,
|
privateKey: this.registryConfig.privateKey,
|
||||||
bondId: this.registryConfig.bondId
|
record: applicationRecord,
|
||||||
},
|
bondId: this.registryConfig.bondId
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Published application record ${result.id}`);
|
log(`Published application record ${result.id}`);
|
||||||
@ -126,33 +129,39 @@ export class Registry {
|
|||||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setName(
|
||||||
cid: result.id,
|
{
|
||||||
lrn
|
cid: result.id,
|
||||||
},
|
lrn
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setName(
|
||||||
cid: result.id,
|
{
|
||||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
cid: result.id,
|
||||||
},
|
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setName(
|
||||||
cid: result.id,
|
{
|
||||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
cid: result.id,
|
||||||
},
|
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -183,19 +192,21 @@ export class Registry {
|
|||||||
const auctionConfig = config.auction;
|
const auctionConfig = config.auction;
|
||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
const auctionResult = await this.registry.createProviderAuction(
|
const auctionResult = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.createProviderAuction(
|
||||||
commitFee: auctionConfig.commitFee,
|
{
|
||||||
commitsDuration: auctionConfig.commitsDuration,
|
commitFee: auctionConfig.commitFee,
|
||||||
revealFee: auctionConfig.revealFee,
|
commitsDuration: auctionConfig.commitsDuration,
|
||||||
revealsDuration: auctionConfig.revealsDuration,
|
revealFee: auctionConfig.revealFee,
|
||||||
denom: auctionConfig.denom,
|
revealsDuration: auctionConfig.revealsDuration,
|
||||||
maxPrice: auctionParams.maxPrice,
|
denom: auctionConfig.denom,
|
||||||
numProviders: auctionParams.numProviders,
|
maxPrice: auctionParams.maxPrice,
|
||||||
},
|
numProviders: auctionParams.numProviders,
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
)
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (!auctionResult.auction) {
|
if (!auctionResult.auction) {
|
||||||
throw new Error('Error creating auction');
|
throw new Error('Error creating auction');
|
||||||
@ -208,14 +219,16 @@ export class Registry {
|
|||||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setRecord(
|
||||||
privateKey: this.registryConfig.privateKey,
|
{
|
||||||
record: applicationDeploymentAuction,
|
privateKey: this.registryConfig.privateKey,
|
||||||
bondId: this.registryConfig.bondId
|
record: applicationDeploymentAuction,
|
||||||
},
|
bondId: this.registryConfig.bondId
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||||
@ -231,10 +244,11 @@ export class Registry {
|
|||||||
deployment: Deployment,
|
deployment: Deployment,
|
||||||
appName: string,
|
appName: string,
|
||||||
repository: string,
|
repository: string,
|
||||||
auctionId?: string,
|
auctionId?: string | null,
|
||||||
lrn: string,
|
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;
|
||||||
@ -268,21 +282,25 @@ export class Registry {
|
|||||||
}),
|
}),
|
||||||
deployer: data.lrn,
|
deployer: data.lrn,
|
||||||
...(data.auctionId && { auction: data.auctionId }),
|
...(data.auctionId && { auction: data.auctionId }),
|
||||||
|
...(data.payment && { payment: data.payment }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setRecord(
|
||||||
privateKey: this.registryConfig.privateKey,
|
{
|
||||||
record: applicationDeploymentRequest,
|
privateKey: this.registryConfig.privateKey,
|
||||||
bondId: this.registryConfig.bondId
|
record: applicationDeploymentRequest,
|
||||||
},
|
bondId: this.registryConfig.bondId
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Application deployment request record published: ${result.id}`);
|
log(`Application deployment request record published: ${result.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
@ -306,7 +324,11 @@ export class Registry {
|
|||||||
paymentAddress: auctionWinner,
|
paymentAddress: auctionWinner,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const record of records) {
|
const newRecords = records.filter(record => {
|
||||||
|
return record.names !== null && record.names.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const record of newRecords) {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
deployerRecords.push(record);
|
deployerRecords.push(record);
|
||||||
break;
|
break;
|
||||||
@ -321,12 +343,14 @@ export class Registry {
|
|||||||
auctionId: string
|
auctionId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
const auction = await this.registry.releaseFunds(
|
const auction = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.releaseFunds(
|
||||||
auctionId
|
{
|
||||||
},
|
auctionId
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return auction;
|
return auction;
|
||||||
@ -410,6 +434,8 @@ export class Registry {
|
|||||||
async createApplicationDeploymentRemovalRequest(data: {
|
async createApplicationDeploymentRemovalRequest(data: {
|
||||||
deploymentId: string;
|
deploymentId: string;
|
||||||
deployerLrn: string;
|
deployerLrn: string;
|
||||||
|
auctionId?: string | null;
|
||||||
|
payment?: string | null;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRemovalRequestId: string;
|
applicationDeploymentRemovalRequestId: string;
|
||||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||||
@ -418,19 +444,23 @@ export class Registry {
|
|||||||
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
|
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 fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setRecord(
|
||||||
privateKey: this.registryConfig.privateKey,
|
{
|
||||||
record: applicationDeploymentRemovalRequest,
|
privateKey: this.registryConfig.privateKey,
|
||||||
bondId: this.registryConfig.bondId
|
record: applicationDeploymentRemovalRequest,
|
||||||
},
|
bondId: this.registryConfig.bondId
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Application deployment removal request record published: ${result.id}`);
|
log(`Application deployment removal request record published: ${result.id}`);
|
||||||
@ -464,6 +494,40 @@ export class Registry {
|
|||||||
return this.registry.getAuctionsByIds([auctionId]);
|
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 {
|
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 `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
|
@ -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 }) => {
|
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||||
return service.getProjectById(projectId);
|
return service.getProjectById(context.user, projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (
|
projectsInOrganization: async (
|
||||||
@ -76,6 +76,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
) => {
|
) => {
|
||||||
return service.getAuctionData(auctionId);
|
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
|
||||||
@ -217,7 +236,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
data: AddProjectFromTemplateInput;
|
data: AddProjectFromTemplateInput;
|
||||||
lrn: string;
|
lrn: string;
|
||||||
auctionParams: AuctionParams,
|
auctionParams: AuctionParams;
|
||||||
environmentVariables: EnvironmentVariables[];
|
environmentVariables: EnvironmentVariables[];
|
||||||
},
|
},
|
||||||
context: any,
|
context: any,
|
||||||
|
@ -77,6 +77,8 @@ type Project {
|
|||||||
fundsReleased: Boolean
|
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!]
|
||||||
@ -134,11 +136,14 @@ type EnvironmentVariable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Deployer {
|
type Deployer {
|
||||||
deployerId: String!
|
|
||||||
deployerLrn: String!
|
deployerLrn: String!
|
||||||
|
deployerId: String!
|
||||||
deployerApiUrl: String!
|
deployerApiUrl: String!
|
||||||
|
minimumPayment: String
|
||||||
|
paymentAddress: String
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
|
baseDomain: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthResult {
|
type AuthResult {
|
||||||
@ -157,6 +162,8 @@ input AddProjectFromTemplateInput {
|
|||||||
owner: String!
|
owner: String!
|
||||||
name: String!
|
name: String!
|
||||||
isPrivate: Boolean!
|
isPrivate: Boolean!
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input AddProjectInput {
|
input AddProjectInput {
|
||||||
@ -164,6 +171,8 @@ input AddProjectInput {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
template: String
|
template: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
@ -257,6 +266,9 @@ type Query {
|
|||||||
searchProjects(searchText: String!): [Project!]
|
searchProjects(searchText: String!): [Project!]
|
||||||
getAuctionData(auctionId: String!): Auction!
|
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 {
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
AppDeploymentRecord,
|
AppDeploymentRecord,
|
||||||
AppDeploymentRemovalRecord,
|
AppDeploymentRemovalRecord,
|
||||||
AuctionParams,
|
AuctionParams,
|
||||||
|
DeployerRecord,
|
||||||
EnvironmentVariables,
|
EnvironmentVariables,
|
||||||
GitPushEventPayload,
|
GitPushEventPayload,
|
||||||
} from './types';
|
} from './types';
|
||||||
@ -211,6 +212,9 @@ export class Service {
|
|||||||
if (!deployment.project.fundsReleased) {
|
if (!deployment.project.fundsReleased) {
|
||||||
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
|
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
|
||||||
|
|
||||||
|
// Return remaining amount to owner
|
||||||
|
await this.returnUserFundsByProjectId(deployment.projectId, true);
|
||||||
|
|
||||||
await this.db.updateProjectById(deployment.projectId, {
|
await this.db.updateProjectById(deployment.projectId, {
|
||||||
fundsReleased,
|
fundsReleased,
|
||||||
});
|
});
|
||||||
@ -308,36 +312,17 @@ export class Service {
|
|||||||
|
|
||||||
if (!deployerRecords) {
|
if (!deployerRecords) {
|
||||||
log(`No winning deployer for auction ${project!.auctionId}`);
|
log(`No winning deployer for auction ${project!.auctionId}`);
|
||||||
|
|
||||||
|
// Return all funds to the owner
|
||||||
|
await this.returnUserFundsByProjectId(project.id, false)
|
||||||
} else {
|
} else {
|
||||||
const deployerIds = [];
|
const deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
|
||||||
|
for (const deployer of deployers) {
|
||||||
for (const record of deployerRecords) {
|
log(`Creating deployment for deployer ${deployer.deployerLrn}`);
|
||||||
const deployerId = record.id;
|
await this.createDeploymentFromAuction(project, deployer);
|
||||||
const deployerLrn = record.names[0];
|
|
||||||
|
|
||||||
deployerIds.push(deployerId);
|
|
||||||
|
|
||||||
const deployerApiUrl = record.attributes.apiUrl;
|
|
||||||
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
|
|
||||||
|
|
||||||
const deployerData = {
|
|
||||||
deployerId,
|
|
||||||
deployerLrn,
|
|
||||||
deployerApiUrl,
|
|
||||||
baseDomain
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the deployer in the DB
|
|
||||||
const deployer = await this.db.addDeployer(deployerData);
|
|
||||||
|
|
||||||
// Update project with deployer
|
// Update project with deployer
|
||||||
await this.updateProjectWithDeployer(project.id, deployer);
|
await this.updateProjectWithDeployer(project.id, deployer);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const deployer of deployerIds) {
|
|
||||||
log(`Creating deployment for deployer LRN ${deployer}`);
|
|
||||||
await this.createDeploymentFromAuction(project, deployer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,8 +407,13 @@ export class Service {
|
|||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectById(projectId: string): Promise<Project | null> {
|
async getProjectById(user: User, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,12 +634,12 @@ export class Service {
|
|||||||
|
|
||||||
let deployer;
|
let deployer;
|
||||||
if (deployerLrn) {
|
if (deployerLrn) {
|
||||||
deployer = await this.createDeployerFromLRN(deployerLrn);
|
deployer = await this.db.getDeployerByLRN(deployerLrn);
|
||||||
} else {
|
} else {
|
||||||
deployer = data.deployer;
|
deployer = data.deployer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerId!, applicationRecordId, applicationRecordData);
|
const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData);
|
||||||
|
|
||||||
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
|
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
|
||||||
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
|
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
|
||||||
@ -663,7 +653,9 @@ export class Service {
|
|||||||
repository: repoUrl,
|
repository: repoUrl,
|
||||||
environmentVariables: environmentVariablesObj,
|
environmentVariables: environmentVariablesObj,
|
||||||
dns: `${newDeployment.project.name}`,
|
dns: `${newDeployment.project.name}`,
|
||||||
lrn: deployer!.deployerLrn!
|
lrn: deployer!.deployerLrn!,
|
||||||
|
payment: data.project.txHash,
|
||||||
|
auctionId: data.project.auctionId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,6 +667,8 @@ export class Service {
|
|||||||
lrn: deployer!.deployerLrn!,
|
lrn: deployer!.deployerLrn!,
|
||||||
environmentVariables: environmentVariablesObj,
|
environmentVariables: environmentVariablesObj,
|
||||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
||||||
|
payment: data.project.txHash,
|
||||||
|
auctionId: data.project.auctionId
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, {
|
await this.db.updateDeploymentById(newDeployment.id, {
|
||||||
@ -687,7 +681,7 @@ export class Service {
|
|||||||
|
|
||||||
async createDeploymentFromAuction(
|
async createDeploymentFromAuction(
|
||||||
project: DeepPartial<Project>,
|
project: DeepPartial<Project>,
|
||||||
deployerId: string
|
deployer: Deployer
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
const octokit = await this.getOctokit(project.ownerId!);
|
const octokit = await this.getOctokit(project.ownerId!);
|
||||||
const [owner, repo] = project.repository!.split('/');
|
const [owner, repo] = project.repository!.split('/');
|
||||||
@ -713,7 +707,6 @@ export class Service {
|
|||||||
const applicationRecordId = record.id;
|
const applicationRecordId = record.id;
|
||||||
const applicationRecordData = record.attributes;
|
const applicationRecordData = record.attributes;
|
||||||
|
|
||||||
const deployer = await this.db.getDeployerById(deployerId);
|
|
||||||
const deployerLrn = deployer!.deployerLrn
|
const deployerLrn = deployer!.deployerLrn
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
// Create deployment with prod branch and latest commit
|
||||||
@ -726,7 +719,7 @@ export class Service {
|
|||||||
commitMessage: latestCommit.commit.message,
|
commitMessage: latestCommit.commit.message,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerId, applicationRecordId, applicationRecordData);
|
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData);
|
||||||
|
|
||||||
const environmentVariablesObj = await this.getEnvVariables(project!.id!);
|
const environmentVariablesObj = await this.getEnvVariables(project!.id!);
|
||||||
// To set project DNS
|
// To set project DNS
|
||||||
@ -767,7 +760,7 @@ export class Service {
|
|||||||
async createDeploymentFromData(
|
async createDeploymentFromData(
|
||||||
userId: string,
|
userId: string,
|
||||||
data: DeepPartial<Deployment>,
|
data: DeepPartial<Deployment>,
|
||||||
deployerId: string,
|
deployerLrn: string,
|
||||||
applicationRecordId: string,
|
applicationRecordId: string,
|
||||||
applicationRecordData: ApplicationRecord,
|
applicationRecordData: ApplicationRecord,
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
@ -785,7 +778,7 @@ export class Service {
|
|||||||
id: userId,
|
id: userId,
|
||||||
}),
|
}),
|
||||||
deployer: Object.assign(new Deployer(), {
|
deployer: Object.assign(new Deployer(), {
|
||||||
deployerId,
|
deployerLrn,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -794,30 +787,6 @@ export class Service {
|
|||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDeployerFromLRN(deployerLrn: string): Promise<Deployer | null> {
|
|
||||||
const records = await this.laconicRegistry.getRecordsByName(deployerLrn);
|
|
||||||
|
|
||||||
if (records.length === 0) {
|
|
||||||
log('No records found for deployer LRN:', deployerLrn);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deployerId = records[0].id;
|
|
||||||
const deployerApiUrl = records[0].attributes.apiUrl;
|
|
||||||
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
|
|
||||||
|
|
||||||
const deployerData = {
|
|
||||||
deployerId,
|
|
||||||
deployerLrn,
|
|
||||||
deployerApiUrl,
|
|
||||||
baseDomain
|
|
||||||
};
|
|
||||||
|
|
||||||
const deployer = await this.db.addDeployer(deployerData);
|
|
||||||
|
|
||||||
return deployer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateProjectWithDeployer(
|
async updateProjectWithDeployer(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
deployer: Deployer
|
deployer: Deployer
|
||||||
@ -875,6 +844,8 @@ 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);
|
}, lrn, auctionParams, environmentVariables);
|
||||||
|
|
||||||
if (!project || !project.id) {
|
if (!project || !project.id) {
|
||||||
@ -924,21 +895,53 @@ export class Service {
|
|||||||
per_page: 1,
|
per_page: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (auctionParams) {
|
if (auctionParams) {
|
||||||
|
// 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 { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
|
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
|
||||||
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
|
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
|
||||||
} else {
|
} else {
|
||||||
const newDeployment = await this.createDeployment(user.id, octokit, deploymentData, lrn);
|
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
|
// Update project with deployer
|
||||||
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
|
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
|
||||||
}
|
}
|
||||||
@ -1089,7 +1092,7 @@ export class Service {
|
|||||||
let newDeployment: Deployment;
|
let newDeployment: Deployment;
|
||||||
|
|
||||||
if (oldDeployment.project.auctionId) {
|
if (oldDeployment.project.auctionId) {
|
||||||
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer.deployerId);
|
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer);
|
||||||
} else {
|
} else {
|
||||||
newDeployment = await this.createDeployment(user.id, octokit,
|
newDeployment = await this.createDeployment(user.id, octokit,
|
||||||
{
|
{
|
||||||
@ -1179,14 +1182,18 @@ export class Service {
|
|||||||
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
||||||
deploymentId: latestRecord.id,
|
deploymentId: latestRecord.id,
|
||||||
deployerLrn: deployment.deployer.deployerLrn
|
deployerLrn: deployment.deployer.deployerLrn,
|
||||||
|
auctionId: deployment.project.auctionId,
|
||||||
|
payment: deployment.project.txHash
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
||||||
deploymentId: deployment.applicationDeploymentRecordId,
|
deploymentId: deployment.applicationDeploymentRecordId,
|
||||||
deployerLrn: deployment.deployer.deployerLrn
|
deployerLrn: deployment.deployer.deployerLrn,
|
||||||
|
auctionId: deployment.project.auctionId,
|
||||||
|
payment: deployment.project.txHash
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateDeploymentById(deployment.id, {
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
@ -1369,4 +1376,109 @@ export class Service {
|
|||||||
|
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,8 @@ export interface AddProjectFromTemplateInput {
|
|||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuctionParams {
|
export interface AuctionParams {
|
||||||
@ -92,6 +94,7 @@ export interface DeployerRecord {
|
|||||||
expiryTime: string;
|
expiryTime: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
|
minimumPayment: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
paymentAddress: string;
|
paymentAddress: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
@ -120,3 +120,24 @@ export const getRepoDetails = async (
|
|||||||
repoUrl
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||||
"name": "Snowball Tools",
|
"name": "Deploy Tools",
|
||||||
"slug": "snowball-tools-1"
|
"slug": "deploy-tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||||
|
@ -38,7 +38,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@ -73,7 +73,7 @@ async function main() {
|
|||||||
|
|
||||||
// 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.baseDomain}`;
|
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
await registry.setRecord(
|
await registry.setRecord(
|
||||||
{
|
{
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
REGISTRY_BOND_ID=
|
REGISTRY_BOND_ID=
|
||||||
DEPLOYER_LRN=
|
DEPLOYER_LRN=
|
||||||
|
AUTHORITY=
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
registry:
|
registry:
|
||||||
rpcEndpoint: http://laconicd.laconic.com:26657
|
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||||
gqlEndpoint: http://laconicd.laconic.com:9473/api
|
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||||
userKey: 08c0d30ed23706330468e6936316a3bc3e69e451e394f05027ad56119bb485b9
|
userKey:
|
||||||
bondId: 820587f916d9a6a056f1e6a5a250151d9fa0c1e771347a6b8bb3d6f2090fd11b
|
bondId:
|
||||||
chainId: laconic_9000-2
|
chainId: laconic_9000-2
|
||||||
gas:
|
|
||||||
fees:
|
|
||||||
gasPrice: 1alnt
|
gasPrice: 1alnt
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
source .env
|
source .env
|
||||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
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"
|
||||||
@ -21,40 +22,13 @@ CONFIG_FILE=config.yml
|
|||||||
# 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 "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}')
|
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
|
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: lrn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
deployer: $DEPLOYER_LRN
|
|
||||||
dns: dashboard
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowball-backend.pwa.laconic.com
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
|
||||||
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_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
|
||||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.pwa.laconic.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
|
# Generate application-record.yml with incremented version
|
||||||
cat >./records/application-record.yml <<EOF
|
cat >./records/application-record.yml <<EOF
|
||||||
record:
|
record:
|
||||||
@ -63,11 +37,11 @@ record:
|
|||||||
repository_ref: $LATEST_HASH
|
repository_ref: $LATEST_HASH
|
||||||
repository: ["$REPO_URL"]
|
repository: ["$REPO_URL"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: snowballtools-base-frontend
|
name: deploy-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
|
||||||
|
|
||||||
@ -83,7 +57,7 @@ echo "ApplicationRecord published"
|
|||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/snowballtools-base-frontend"
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-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 registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
@ -122,6 +96,45 @@ if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
|||||||
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')
|
||||||
|
# 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_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
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
@ -41,6 +41,7 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_github_pwa_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_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: "1.0.0"
|
version: '1.0.0'
|
||||||
name: snowballtools-base-frontend@0.1.8
|
name: deploy-frontend@1.0.0
|
||||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
application: lrn://vaasl/applications/deploy-frontend@1.0.0
|
||||||
dns: dashboard
|
dns: deploy
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
@ -4,5 +4,5 @@ record:
|
|||||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: snowballtools-base-frontend
|
name: deploy-frontend
|
||||||
app_version: 0.1.8
|
app_version: 1.0.0
|
||||||
|
@ -15,3 +15,5 @@ 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_TURNKEY_ORGANIZATION_ID=
|
||||||
|
|
||||||
|
VITE_LACONICD_CHAIN_ID=
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "vite --port 3000",
|
||||||
@ -30,10 +30,6 @@
|
|||||||
"@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",
|
||||||
|
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@ -12,14 +13,19 @@ 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, ...props }: StopwatchProps) => {
|
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
||||||
const { totalSeconds } = useStopwatch({
|
const { totalSeconds, pause, start } = 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} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,15 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { FormProvider, FieldValues } from 'react-hook-form';
|
import { FormProvider, FieldValues } from 'react-hook-form';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import { AddEnvironmentVariableInput, AuctionParams } from 'gql-client';
|
import {
|
||||||
|
AddEnvironmentVariableInput,
|
||||||
|
AuctionParams,
|
||||||
|
Deployer,
|
||||||
|
} from 'gql-client';
|
||||||
|
|
||||||
|
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowRightCircleFilledIcon,
|
ArrowRightCircleFilledIcon,
|
||||||
@ -11,12 +17,13 @@ import {
|
|||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Heading } from '../../shared/Heading';
|
import { Heading } from '../../shared/Heading';
|
||||||
import { Button } from '../../shared/Button';
|
import { Button } from '../../shared/Button';
|
||||||
import { Select, SelectOption } from 'components/shared/Select';
|
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
import { EnvironmentVariablesFormValues } from 'types/types';
|
||||||
|
import ConnectWallet from './ConnectWallet';
|
||||||
|
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
type ConfigureDeploymentFormValues = {
|
type ConfigureDeploymentFormValues = {
|
||||||
option: string;
|
option: string;
|
||||||
@ -28,8 +35,18 @@ type ConfigureDeploymentFormValues = {
|
|||||||
type ConfigureFormValues = ConfigureDeploymentFormValues &
|
type ConfigureFormValues = ConfigureDeploymentFormValues &
|
||||||
EnvironmentVariablesFormValues;
|
EnvironmentVariablesFormValues;
|
||||||
|
|
||||||
|
const DEFAULT_MAX_PRICE = '10000';
|
||||||
|
|
||||||
const Configure = () => {
|
const Configure = () => {
|
||||||
|
const { signClient, session, accounts } = useWalletConnectClient();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState<string>();
|
||||||
|
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
||||||
|
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
||||||
|
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const templateId = searchParams.get('templateId');
|
const templateId = searchParams.get('templateId');
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
@ -48,7 +65,13 @@ const Configure = () => {
|
|||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
const methods = useForm<ConfigureFormValues>({
|
const methods = useForm<ConfigureFormValues>({
|
||||||
defaultValues: { option: 'LRN' },
|
defaultValues: {
|
||||||
|
option: 'Auction',
|
||||||
|
maxPrice: DEFAULT_MAX_PRICE,
|
||||||
|
lrn: '',
|
||||||
|
numProviders: 1,
|
||||||
|
variables: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedOption = methods.watch('option');
|
const selectedOption = methods.watch('option');
|
||||||
@ -59,6 +82,8 @@ const Configure = () => {
|
|||||||
const createProject = async (
|
const createProject = async (
|
||||||
data: FieldValues,
|
data: FieldValues,
|
||||||
envVariables: AddEnvironmentVariableInput[],
|
envVariables: AddEnvironmentVariableInput[],
|
||||||
|
senderAddress: string,
|
||||||
|
txHash: string,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
let projectId: string | null = null;
|
let projectId: string | null = null;
|
||||||
@ -83,6 +108,8 @@ const Configure = () => {
|
|||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
||||||
@ -102,6 +129,8 @@ const Configure = () => {
|
|||||||
prodBranch: defaultBranch!,
|
prodBranch: defaultBranch!,
|
||||||
repository: fullName!,
|
repository: fullName!,
|
||||||
template: 'webapp',
|
template: 'webapp',
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
},
|
},
|
||||||
lrn,
|
lrn,
|
||||||
auctionParams,
|
auctionParams,
|
||||||
@ -129,48 +158,220 @@ const Configure = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(
|
const handleFormSubmit = useCallback(
|
||||||
async (createFormData: FieldValues) => {
|
async (createFormData: FieldValues) => {
|
||||||
const environmentVariables = createFormData.variables.map(
|
try {
|
||||||
(variable: any) => {
|
const deployerLrn = createFormData.lrn;
|
||||||
return {
|
const deployer = deployers.find(
|
||||||
key: variable.key,
|
(deployer) => deployer.deployerLrn === deployerLrn,
|
||||||
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(
|
let amount: string;
|
||||||
createFormData,
|
let senderAddress: string;
|
||||||
environmentVariables,
|
let txHash: string;
|
||||||
);
|
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
||||||
|
toast({
|
||||||
|
id: 'no-payment-required',
|
||||||
|
title: 'No payment required. Deploying app...',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
await client.getEnvironmentVariables(projectId);
|
txHash = '';
|
||||||
|
senderAddress = '';
|
||||||
|
} else {
|
||||||
|
if (!selectedAccount) return;
|
||||||
|
|
||||||
if (templateId) {
|
senderAddress = selectedAccount.split(':')[2];
|
||||||
createFormData.option === 'Auction'
|
|
||||||
? navigate(
|
if (createFormData.option === 'LRN') {
|
||||||
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
amount = deployer?.minimumPayment!;
|
||||||
)
|
} else {
|
||||||
: navigate(
|
amount = (
|
||||||
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
|
createFormData.numProviders * createFormData.maxPrice
|
||||||
);
|
).toString();
|
||||||
} else {
|
}
|
||||||
createFormData.option === 'Auction'
|
|
||||||
? navigate(
|
const amountToBePaid = amount.replace(/\D/g, '').toString();
|
||||||
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
|
||||||
)
|
const txHashResponse = await cosmosSendTokensHandler(
|
||||||
: navigate(
|
selectedAccount,
|
||||||
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
|
amountToBePaid,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!txHashResponse) {
|
||||||
|
console.error('Tx not successful');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash = txHashResponse;
|
||||||
|
|
||||||
|
const isTxHashValid = await verifyTx(
|
||||||
|
senderAddress,
|
||||||
|
txHash,
|
||||||
|
amountToBePaid.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTxHashValid === false) {
|
||||||
|
console.error('Invalid Tx hash', txHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
id: 'error-deploying-app',
|
||||||
|
title: 'Error deploying app',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client, createProject, dismiss, toast],
|
[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 (!signClient || !session || !selectedAccount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainId = selectedAccount.split(':')[1];
|
||||||
|
const senderAddress = selectedAccount.split(':')[2];
|
||||||
|
const snowballAddress = await client.getAddress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsPaymentDone(false);
|
||||||
|
setIsPaymentLoading(true);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'sending-payment-request',
|
||||||
|
title: 'Check your wallet and approve payment request',
|
||||||
|
variant: 'loading',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: { signature: string } = await signClient.request({
|
||||||
|
topic: session.topic,
|
||||||
|
chainId: `cosmos:${chainId}`,
|
||||||
|
request: {
|
||||||
|
method: 'cosmos_sendTokens',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
from: senderAddress,
|
||||||
|
to: snowballAddress,
|
||||||
|
value: amount,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Error completing transaction');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'payment-successful',
|
||||||
|
title: 'Payment successful',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsPaymentDone(true);
|
||||||
|
|
||||||
|
return result.signature;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error sending tokens', error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'error-sending-tokens',
|
||||||
|
title: 'Error sending tokens',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsPaymentDone(false);
|
||||||
|
} finally {
|
||||||
|
setIsPaymentLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[session, signClient, toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeployers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-7 px-4 py-6">
|
<div className="space-y-7 px-4 py-6">
|
||||||
<div className="flex justify-between mb-6">
|
<div className="flex justify-between mb-6">
|
||||||
@ -195,24 +396,21 @@ const Configure = () => {
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Select
|
<Select
|
||||||
label="Configuration Options"
|
value={value}
|
||||||
value={
|
onChange={(event) => onChange(event.target.value)}
|
||||||
{
|
size="small"
|
||||||
value: value || 'LRN',
|
displayEmpty
|
||||||
label:
|
sx={{
|
||||||
value === 'Auction'
|
fontFamily: 'inherit',
|
||||||
? 'Create Auction'
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
: 'Deployer LRN',
|
borderColor: '#e0e0e0',
|
||||||
} as SelectOption
|
borderRadius: '8px',
|
||||||
}
|
},
|
||||||
onChange={(value) =>
|
}}
|
||||||
onChange((value as SelectOption).value)
|
>
|
||||||
}
|
<MenuItem value="Auction">Create Auction</MenuItem>
|
||||||
options={[
|
<MenuItem value="LRN">Deployer LRN</MenuItem>
|
||||||
{ value: 'LRN', label: 'Deployer LRN' },
|
</Select>
|
||||||
{ value: 'Auction', label: 'Create Auction' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -225,15 +423,39 @@ const Configure = () => {
|
|||||||
>
|
>
|
||||||
The app will be deployed by the configured deployer
|
The app will be deployed by the configured deployer
|
||||||
</Heading>
|
</Heading>
|
||||||
<span className="text-sm text-elements-high-em">
|
|
||||||
Enter LRN for deployer
|
|
||||||
</span>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="lrn"
|
name="lrn"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange }, fieldState }) => (
|
||||||
<Input value={value} onChange={onChange} />
|
<FormControl fullWidth error={Boolean(fieldState.error)}>
|
||||||
|
<span className="text-sm text-elements-high-em mb-4">
|
||||||
|
Select deployer LRN
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
onDeployerChange(event.target.value);
|
||||||
|
}}
|
||||||
|
displayEmpty
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{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>
|
</div>
|
||||||
@ -257,7 +479,11 @@ const Configure = () => {
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Input type="number" value={value} onChange={onChange} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -284,22 +510,57 @@ const Configure = () => {
|
|||||||
<EnvironmentVariablesForm />
|
<EnvironmentVariablesForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
|
||||||
<Button
|
<div>
|
||||||
{...buttonSize}
|
<Button
|
||||||
type="submit"
|
{...buttonSize}
|
||||||
disabled={isLoading}
|
type="submit"
|
||||||
rightIcon={
|
disabled={isLoading || !selectedDeployer || !selectedAccount}
|
||||||
isLoading ? (
|
rightIcon={
|
||||||
<LoadingIcon className="animate-spin" />
|
isLoading ? (
|
||||||
) : (
|
<LoadingIcon className="animate-spin" />
|
||||||
<ArrowRightCircleFilledIcon />
|
) : (
|
||||||
)
|
<ArrowRightCircleFilledIcon />
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
{isLoading ? 'Deploying repo' : 'Deploy repo'}
|
>
|
||||||
</Button>
|
{isLoading ? 'Deploying' : 'Deploy'}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
||||||
|
Connect to your wallet
|
||||||
|
</Heading>
|
||||||
|
<ConnectWallet onAccountChange={onAccountChange} />
|
||||||
|
{accounts && accounts?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
{...buttonSize}
|
||||||
|
type="submit"
|
||||||
|
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>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
|
import { Button } from '../../shared/Button';
|
||||||
|
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
|
const ConnectWallet = ({
|
||||||
|
onAccountChange,
|
||||||
|
}: {
|
||||||
|
onAccountChange: (selectedAccount: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { onConnect, accounts } = useWalletConnectClient();
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
await onConnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-slate-100 rounded-lg mb-6">
|
||||||
|
{!accounts ? (
|
||||||
|
<div>
|
||||||
|
<Button type={'button'} onClick={handleConnect}>
|
||||||
|
Connect Wallet
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
label="Select Account"
|
||||||
|
defaultValue={accounts[0].address}
|
||||||
|
onChange={(value) => {
|
||||||
|
value && onAccountChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accounts.map((account, index) => (
|
||||||
|
<Option key={index} value={account.address}>
|
||||||
|
{account.address.split(':').slice(1).join(':')}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectWallet;
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } 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';
|
||||||
@ -7,13 +9,37 @@ 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();
|
||||||
@ -23,13 +49,67 @@ const Deploy = () => {
|
|||||||
navigate(`/${orgSlug}/projects/create`);
|
navigate(`/${orgSlug}/projects/create`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const isDeploymentFailed = useMemo(() => {
|
||||||
const timerID = setTimeout(() => {
|
if (!record) {
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
return false;
|
||||||
}, TIMEOUT_DURATION);
|
}
|
||||||
|
|
||||||
return () => clearInterval(timerID);
|
// 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(() => {
|
||||||
|
fetchDeployment();
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}, [record]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-7">
|
<div className="space-y-7">
|
||||||
@ -42,6 +122,7 @@ 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>
|
||||||
@ -60,30 +141,36 @@ const Deploy = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{!isDeploymentFailed ? (
|
||||||
<DeployStep
|
<div>
|
||||||
title="Building"
|
<DeployStep
|
||||||
status={DeployStatus.COMPLETE}
|
title={record ? 'Submitted' : 'Submitting'}
|
||||||
step="1"
|
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
|
||||||
processTime="72000"
|
step="1"
|
||||||
/>
|
/>
|
||||||
<DeployStep
|
|
||||||
title="Deployment summary"
|
<DeployStep
|
||||||
status={DeployStatus.PROCESSING}
|
title={
|
||||||
step="2"
|
record && record.lastState === 'DEPLOYED'
|
||||||
startTime={Date.now().toString()}
|
? 'Deployed'
|
||||||
/>
|
: 'Deploying'
|
||||||
<DeployStep
|
}
|
||||||
title="Running checks"
|
status={
|
||||||
status={DeployStatus.NOT_STARTED}
|
!record
|
||||||
step="3"
|
? DeployStatus.NOT_STARTED
|
||||||
/>
|
: record.lastState === 'DEPLOYED'
|
||||||
<DeployStep
|
? DeployStatus.COMPLETE
|
||||||
title="Assigning domains"
|
: DeployStatus.PROCESSING
|
||||||
status={DeployStatus.NOT_STARTED}
|
}
|
||||||
step="4"
|
step="2"
|
||||||
/>
|
startTime={Date.now().toString()}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
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 {
|
||||||
@ -32,35 +21,11 @@ interface DeployStepsProps {
|
|||||||
processTime?: string;
|
processTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeployStep = ({
|
const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||||
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(
|
className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
|
||||||
'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 */}
|
||||||
@ -73,12 +38,6 @@ const DeployStep = ({
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<LoaderIcon className="animate-spin text-elements-link" />
|
<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>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
@ -96,7 +55,10 @@ const DeployStep = ({
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
<Stopwatch
|
||||||
|
offsetTimestamp={setStopWatchOffset(startTime!)}
|
||||||
|
isPaused={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status === DeployStatus.COMPLETE && (
|
{status === DeployStatus.COMPLETE && (
|
||||||
@ -107,51 +69,9 @@ const DeployStep = ({
|
|||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
Tooltip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import { Avatar } from 'components/shared/Avatar';
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
@ -91,33 +92,39 @@ const DeploymentDetailsCard = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDeploymentLogs = useCallback(async () => {
|
const fetchDeploymentLogs = async () => {
|
||||||
let url = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
|
setDeploymentLogs('Loading logs...');
|
||||||
const res = await fetch(url, { cache: 'no-store' });
|
|
||||||
handleOpenDialog();
|
handleOpenDialog();
|
||||||
if (res.ok) {
|
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
|
||||||
const logs = await res.text();
|
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
|
||||||
setDeploymentLogs(logs);
|
(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.deployerApiUrl,
|
|
||||||
deployment.applicationDeploymentRequestId,
|
|
||||||
handleOpenDialog,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderDeploymentStatus = useCallback(
|
const renderDeploymentStatus = useCallback(
|
||||||
(className?: string) => {
|
(className?: string) => {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<Tooltip title="Click to view build logs">
|
||||||
<Tag
|
<div className={className} style={{ cursor: 'pointer' }}>
|
||||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
<Tag
|
||||||
size="xs"
|
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
size="xs"
|
||||||
onClick={fetchDeploymentLogs}
|
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||||
>
|
onClick={fetchDeploymentLogs}
|
||||||
{deployment.status}
|
>
|
||||||
</Tag>
|
{deployment.status}
|
||||||
</div>
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[deployment.status, deployment.commitHash],
|
[deployment.status, deployment.commitHash],
|
||||||
|
@ -23,6 +23,7 @@ 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;
|
||||||
@ -46,6 +47,8 @@ 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] =
|
const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] =
|
||||||
@ -53,7 +56,7 @@ export const DeploymentMenu = ({
|
|||||||
|
|
||||||
const updateDeployment = async () => {
|
const updateDeployment = async () => {
|
||||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||||
if (isUpdated) {
|
if (isUpdated.updateDeploymentToProd) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'deployment_changed_to_production',
|
id: 'deployment_changed_to_production',
|
||||||
@ -74,7 +77,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);
|
setConfirmButtonLoadingLoading(false);
|
||||||
if (isRedeployed) {
|
if (isRedeployed.redeployToProd) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'redeployed_to_production',
|
id: 'redeployed_to_production',
|
||||||
@ -97,7 +100,7 @@ export const DeploymentMenu = ({
|
|||||||
project.id,
|
project.id,
|
||||||
deployment.id,
|
deployment.id,
|
||||||
);
|
);
|
||||||
if (isRollbacked) {
|
if (isRollbacked.rollbackDeployment) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'deployment_rolled_back',
|
id: 'deployment_rolled_back',
|
||||||
@ -116,15 +119,12 @@ export const DeploymentMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteDeployment = async () => {
|
const deleteDeployment = async () => {
|
||||||
toast({
|
|
||||||
id: 'deleting_deployment',
|
|
||||||
title: 'Deleting deployment....',
|
|
||||||
variant: 'success',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
|
|
||||||
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_removal_requested',
|
||||||
@ -212,7 +212,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={() => deleteDeployment()}
|
onClick={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
||||||
>
|
>
|
||||||
<CrossCircleIcon /> Delete deployment
|
<CrossCircleIcon /> Delete deployment
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -265,6 +265,15 @@ 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}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,6 +17,16 @@ import { Button, Heading, Tag } from 'components/shared';
|
|||||||
|
|
||||||
const WAIT_DURATION = 5000;
|
const WAIT_DURATION = 5000;
|
||||||
|
|
||||||
|
const DIALOG_STYLE = {
|
||||||
|
backgroundColor: 'rgba(0,0,0, .9)',
|
||||||
|
padding: '2em',
|
||||||
|
borderRadius: '0.5em',
|
||||||
|
marginLeft: '0.5em',
|
||||||
|
marginRight: '0.5em',
|
||||||
|
color: 'gray',
|
||||||
|
fontSize: 'small',
|
||||||
|
};
|
||||||
|
|
||||||
export const AuctionCard = ({ project }: { project: Project }) => {
|
export const AuctionCard = ({ project }: { project: Project }) => {
|
||||||
const [auctionStatus, setAuctionStatus] = useState<string>('');
|
const [auctionStatus, setAuctionStatus] = useState<string>('');
|
||||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||||
@ -36,27 +46,27 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
|||||||
const result = await client.getAuctionData(project.auctionId);
|
const result = await client.getAuctionData(project.auctionId);
|
||||||
setAuctionStatus(result.status);
|
setAuctionStatus(result.status);
|
||||||
setAuctionDetails(result);
|
setAuctionDetails(result);
|
||||||
setDeployers(project.deployers);
|
}, [project.auctionId, project.deployers, project.fundsReleased]);
|
||||||
setFundsStatus(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(() => {
|
useEffect(() => {
|
||||||
if (auctionStatus !== 'completed') {
|
fetchData();
|
||||||
checkAuctionStatus();
|
|
||||||
const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION);
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auctionStatus === 'completed') {
|
const timerId = setInterval(() => {
|
||||||
const fetchUpdatedProject = async () => {
|
fetchData();
|
||||||
// Wait for 5 secs since the project is not immediately updated with deployer LRNs
|
}, WAIT_DURATION);
|
||||||
await new Promise((resolve) => setTimeout(resolve, WAIT_DURATION));
|
|
||||||
const updatedProject = await client.getProject(project.id);
|
return () => clearInterval(timerId);
|
||||||
setDeployers(updatedProject.project?.deployers || []);
|
}, [fetchData]);
|
||||||
};
|
|
||||||
fetchUpdatedProject();
|
|
||||||
}
|
|
||||||
}, [auctionStatus, client]);
|
|
||||||
|
|
||||||
const renderAuctionStatus = useCallback(
|
const renderAuctionStatus = useCallback(
|
||||||
() => (
|
() => (
|
||||||
@ -86,13 +96,6 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-1">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Auction Status
|
|
||||||
</span>
|
|
||||||
<div className="ml-2">{renderAuctionStatus()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
Auction Id
|
Auction Id
|
||||||
@ -102,29 +105,49 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deployers?.length > 0 && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Deployer LRNs
|
|
||||||
</span>
|
|
||||||
{deployers.map((deployer, index) => (
|
|
||||||
<p key={index} className="text-elements-mid-em text-sm">
|
|
||||||
{'\u2022'} {deployer.deployerLrn}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-1">
|
<div className="flex justify-between items-center mt-1">
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
Deployer Funds Status
|
Auction Status
|
||||||
</span>
|
</span>
|
||||||
<div className="ml-2">
|
<div className="ml-2">{renderAuctionStatus()}</div>
|
||||||
<Tag size="xs" type={fundsStatus ? 'positive' : 'emphasized'}>
|
|
||||||
{fundsStatus ? 'RELEASED' : 'LOCKED'}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{auctionStatus === 'completed' && (
|
||||||
|
<>
|
||||||
|
{deployers?.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
Deployer LRNs
|
||||||
|
</span>
|
||||||
|
{deployers.map((deployer, index) => (
|
||||||
|
<p key={index} className="text-elements-mid-em text-sm">
|
||||||
|
{'\u2022'} {deployer.deployerLrn}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-1">
|
||||||
|
<span className="text-elements-high-em 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 text-sm font-medium tracking-tight">
|
||||||
|
No winning deployers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -134,7 +157,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
|||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
>
|
>
|
||||||
<DialogTitle>Auction Details</DialogTitle>
|
<DialogTitle>Auction Details</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent style={DIALOG_STYLE}>
|
||||||
{auctionDetails && (
|
{auctionDetails && (
|
||||||
<pre>{JSON.stringify(auctionDetails, null, 2)}</pre>
|
<pre>{JSON.stringify(auctionDetails, null, 2)}</pre>
|
||||||
)}
|
)}
|
||||||
|
210
packages/frontend/src/context/WalletConnectContext.tsx
Normal file
210
packages/frontend/src/context/WalletConnectContext.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import SignClient from '@walletconnect/sign-client';
|
||||||
|
import { getSdkError } from '@walletconnect/utils';
|
||||||
|
import { SessionTypes } from '@walletconnect/types';
|
||||||
|
|
||||||
|
import { walletConnectModal } from '../utils/web3modal';
|
||||||
|
import {
|
||||||
|
VITE_LACONICD_CHAIN_ID,
|
||||||
|
VITE_WALLET_CONNECT_ID,
|
||||||
|
} from 'utils/constants';
|
||||||
|
|
||||||
|
interface ClientInterface {
|
||||||
|
signClient: SignClient | undefined;
|
||||||
|
session: SessionTypes.Struct | undefined;
|
||||||
|
loadingSession: boolean;
|
||||||
|
onConnect: () => Promise<void>;
|
||||||
|
onDisconnect: () => Promise<void>;
|
||||||
|
onSessionDelete: () => void;
|
||||||
|
accounts: { address: string }[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext({} as ClientInterface);
|
||||||
|
|
||||||
|
export const useWalletConnectClient = () => {
|
||||||
|
return useContext(ClientContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WalletConnectClientProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: JSX.Element;
|
||||||
|
}) => {
|
||||||
|
const [signClient, setSignClient] = useState<SignClient>();
|
||||||
|
const [session, setSession] = useState<SessionTypes.Struct>();
|
||||||
|
const [loadingSession, setLoadingSession] = useState(true);
|
||||||
|
const [accounts, setAccounts] = useState<{ address: string }[]>();
|
||||||
|
|
||||||
|
const isSignClientInitializing = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => {
|
||||||
|
setSession(session);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const subscribeToEvents = useCallback(
|
||||||
|
async (client: SignClient) => {
|
||||||
|
client.on('session_update', ({ topic, params }) => {
|
||||||
|
const { namespaces } = params;
|
||||||
|
const currentSession = client.session.get(topic);
|
||||||
|
const updatedSession = { ...currentSession, namespaces };
|
||||||
|
setSession(updatedSession);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSession],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConnect = async () => {
|
||||||
|
const proposalNamespace = {
|
||||||
|
cosmos: {
|
||||||
|
methods: ['cosmos_sendTokens'],
|
||||||
|
chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`],
|
||||||
|
events: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { uri, approval } = await signClient!.connect({
|
||||||
|
requiredNamespaces: proposalNamespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uri) {
|
||||||
|
walletConnectModal.openModal({ uri });
|
||||||
|
const session = await approval();
|
||||||
|
onSessionConnect(session);
|
||||||
|
walletConnectModal.closeModal();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDisconnect = useCallback(async () => {
|
||||||
|
if (typeof signClient === 'undefined') {
|
||||||
|
throw new Error('WalletConnect is not initialized');
|
||||||
|
}
|
||||||
|
if (typeof session === 'undefined') {
|
||||||
|
throw new Error('Session is not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
await signClient.disconnect({
|
||||||
|
topic: session.topic,
|
||||||
|
reason: getSdkError('USER_DISCONNECTED'),
|
||||||
|
});
|
||||||
|
|
||||||
|
onSessionDelete();
|
||||||
|
}, [signClient, session]);
|
||||||
|
|
||||||
|
const onSessionDelete = () => {
|
||||||
|
setAccounts(undefined);
|
||||||
|
setSession(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPersistedState = useCallback(
|
||||||
|
async (signClient: SignClient) => {
|
||||||
|
if (typeof signClient === 'undefined') {
|
||||||
|
throw new Error('WalletConnect is not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof session !== 'undefined') return;
|
||||||
|
if (signClient.session.length) {
|
||||||
|
const lastKeyIndex = signClient.session.keys.length - 1;
|
||||||
|
const previousSsession = signClient.session.get(
|
||||||
|
signClient.session.keys[lastKeyIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
await onSessionConnect(previousSsession);
|
||||||
|
return previousSsession;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[session, onSessionConnect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const createClient = useCallback(async () => {
|
||||||
|
isSignClientInitializing.current = true;
|
||||||
|
try {
|
||||||
|
const signClient = await SignClient.init({
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
metadata: {
|
||||||
|
name: 'Deploy App',
|
||||||
|
description: '',
|
||||||
|
url: window.location.href,
|
||||||
|
icons: ['https://avatars.githubusercontent.com/u/92608123'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setSignClient(signClient);
|
||||||
|
await checkPersistedState(signClient);
|
||||||
|
await subscribeToEvents(signClient);
|
||||||
|
setLoadingSession(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error in createClient', e);
|
||||||
|
}
|
||||||
|
isSignClientInitializing.current = false;
|
||||||
|
}, [setSignClient, checkPersistedState, subscribeToEvents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!signClient && !isSignClientInitializing.current) {
|
||||||
|
createClient();
|
||||||
|
}
|
||||||
|
}, [signClient, createClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const populateAccounts = async () => {
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!session.namespaces['cosmos']) {
|
||||||
|
console.log('Accounts for cosmos namespace not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cosmosAddresses = session.namespaces['cosmos'].accounts;
|
||||||
|
|
||||||
|
const cosmosAccounts = cosmosAddresses.map((address) => ({
|
||||||
|
address,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const allAccounts = cosmosAccounts;
|
||||||
|
|
||||||
|
setAccounts(allAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
populateAccounts();
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!signClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
signClient.on('session_delete', onSessionDelete);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
signClient.off('session_delete', onSessionDelete);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider
|
||||||
|
value={{
|
||||||
|
signClient,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
onSessionDelete,
|
||||||
|
loadingSession,
|
||||||
|
session,
|
||||||
|
accounts,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,7 @@ import { ReactNode } from 'react';
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { SiweMessage, generateNonce } from 'siwe';
|
import { SiweMessage, generateNonce } from 'siwe';
|
||||||
import { WagmiProvider } from 'wagmi';
|
import { WagmiProvider } from 'wagmi';
|
||||||
import { arbitrum, mainnet } from 'wagmi/chains';
|
import { mainnet } from 'wagmi/chains';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
||||||
@ -31,12 +31,12 @@ const axiosInstance = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
const metadata = {
|
const metadata = {
|
||||||
name: 'Web3Modal',
|
name: 'Deploy App Auth',
|
||||||
description: 'Snowball Web3Modal',
|
description: '',
|
||||||
url: window.location.origin,
|
url: window.location.origin,
|
||||||
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
||||||
};
|
};
|
||||||
const chains = [mainnet, arbitrum] as const;
|
const chains = [mainnet] as const;
|
||||||
const config = defaultWagmiConfig({
|
const config = defaultWagmiConfig({
|
||||||
chains,
|
chains,
|
||||||
projectId: VITE_WALLET_CONNECT_ID,
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
@ -16,6 +16,7 @@ import { Toaster } from 'components/shared/Toast';
|
|||||||
import { LogErrorBoundary } from 'utils/log-error';
|
import { LogErrorBoundary } from 'utils/log-error';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
import Web3ModalProvider from './context/Web3Provider';
|
import Web3ModalProvider from './context/Web3Provider';
|
||||||
|
import { WalletConnectClientProvider } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
console.log(`v-0.0.9`);
|
console.log(`v-0.0.9`);
|
||||||
|
|
||||||
@ -31,14 +32,16 @@ const gqlClient = new GQLClient({ gqlEndpoint });
|
|||||||
root.render(
|
root.render(
|
||||||
<LogErrorBoundary>
|
<LogErrorBoundary>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<WalletConnectClientProvider>
|
||||||
<Web3ModalProvider>
|
<ThemeProvider>
|
||||||
<GQLClientProvider client={gqlClient}>
|
<Web3ModalProvider>
|
||||||
<App />
|
<GQLClientProvider client={gqlClient}>
|
||||||
<Toaster />
|
<App />
|
||||||
</GQLClientProvider>
|
<Toaster />
|
||||||
</Web3ModalProvider>
|
</GQLClientProvider>
|
||||||
</ThemeProvider>
|
</Web3ModalProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</WalletConnectClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</LogErrorBoundary>,
|
</LogErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { LoaderIcon } from 'components/shared/CustomIcon';
|
|
||||||
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
|
|
||||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconRight } from 'react-day-picker';
|
|
||||||
import { useSnowball } from 'utils/use-snowball';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreatePasskey = ({}: Props) => {
|
|
||||||
const snowball = useSnowball();
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
|
|
||||||
const auth = snowball.auth.passkey;
|
|
||||||
const loading = !!auth.state.loading;
|
|
||||||
|
|
||||||
async function createPasskey() {
|
|
||||||
await auth.register(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
|
||||||
<div className="w-16 h-16 p-2 bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex">
|
|
||||||
<KeyIcon />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-loose">
|
|
||||||
Create a passkey
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Passkeys allow you to sign in securely without using passwords.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
|
||||||
<div className="p-6 flex-col justify-center items-center gap-8 inline-flex">
|
|
||||||
<div className="self-stretch h-36 flex-col justify-center items-center gap-2 flex">
|
|
||||||
<div className="self-stretch h-[72px] flex-col justify-start items-start gap-2 flex">
|
|
||||||
<div className="self-stretch h-5 px-1 flex-col justify-start items-start gap-1 flex">
|
|
||||||
<div className="self-stretch text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Give it a name
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onInput={(e: any) => {
|
|
||||||
setName(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{auth.state.error ? (
|
|
||||||
<InlineNotification
|
|
||||||
title={auth.state.error.message}
|
|
||||||
variant="danger"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<InlineNotification
|
|
||||||
title={`Once you press the "Create passkeys" button, you'll receive a prompt to create the passkey.`}
|
|
||||||
variant="info"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading ? <LoaderIcon className="animate-spin" /> : <IconRight />
|
|
||||||
}
|
|
||||||
className="self-stretch"
|
|
||||||
disabled={!name || loading}
|
|
||||||
onClick={createPasskey}
|
|
||||||
>
|
|
||||||
Create Passkey
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -40,6 +40,8 @@ const deployment: Deployment = {
|
|||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
||||||
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com',
|
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com',
|
||||||
|
minimumPayment: '1000alnt',
|
||||||
|
baseDomain: 'pwa.example.com',
|
||||||
},
|
},
|
||||||
status: DeploymentStatus.Ready,
|
status: DeploymentStatus.Ready,
|
||||||
createdBy: {
|
createdBy: {
|
||||||
|
@ -92,9 +92,13 @@ const Id = () => {
|
|||||||
Open repo
|
Open repo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button {...buttonSize} className="h-11 transition-colors">
|
{(project.deployments.length > 0) &&
|
||||||
Go to app
|
<Link to={`https://${project.name.toLowerCase()}.${project.deployments[0].deployer.baseDomain}`}>
|
||||||
</Button>
|
<Button {...buttonSize} className="h-11 transition-colors">
|
||||||
|
Go to app
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder />
|
<WavyBorder />
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
ArrowRightCircleFilledIcon,
|
ArrowRightCircleFilledIcon,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Checkbox } from 'components/shared/Checkbox';
|
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
@ -42,6 +41,17 @@ const CreateRepo = () => {
|
|||||||
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const checkRepoExists = async (account: string, repoName: string) => {
|
||||||
|
try {
|
||||||
|
await octokit.rest.repos.get({ owner: account, repo: repoName });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Error handled by octokit error hook interceptor in Octokit context
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
assert(data.account);
|
assert(data.account);
|
||||||
@ -51,6 +61,18 @@ const CreateRepo = () => {
|
|||||||
assert(template.repoFullName, 'Template URL not provided');
|
assert(template.repoFullName, 'Template URL not provided');
|
||||||
const [owner, repo] = template.repoFullName.split('/');
|
const [owner, repo] = template.repoFullName.split('/');
|
||||||
|
|
||||||
|
const repoExists = await checkRepoExists(data.account, data.repoName);
|
||||||
|
if (repoExists) {
|
||||||
|
toast({
|
||||||
|
id: 'repo-exist-error',
|
||||||
|
title: 'Repository already exists with this name',
|
||||||
|
variant: 'warning',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
@ -82,7 +104,7 @@ const CreateRepo = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[octokit],
|
[octokit, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -169,15 +191,6 @@ const CreateRepo = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="isPrivate"
|
|
||||||
control={control}
|
|
||||||
render={({}) => (
|
|
||||||
<Checkbox label="Make this repo private" disabled={true} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
{...buttonSize}
|
{...buttonSize}
|
||||||
|
@ -129,16 +129,16 @@ const OverviewTabPanel = () => {
|
|||||||
<Heading className="text-lg leading-6 font-medium truncate">
|
<Heading className="text-lg leading-6 font-medium truncate">
|
||||||
{project.name}
|
{project.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
{project.baseDomains &&
|
{project.deployments &&
|
||||||
project.baseDomains.length > 0 &&
|
project.deployments.length > 0 &&
|
||||||
project.baseDomains.map((baseDomain, index) => (
|
project.deployments.map((deployment, index) => (
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href={`https://${project.name.toLowerCase()}.${baseDomain}`}
|
href={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
|
||||||
className="text-sm text-elements-low-em tracking-tight truncate"
|
className="text-sm text-elements-low-em tracking-tight truncate"
|
||||||
>
|
>
|
||||||
{baseDomain}
|
{deployment.deployer.baseDomain}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@ -180,14 +180,18 @@ const OverviewTabPanel = () => {
|
|||||||
|
|
||||||
{/* DEPLOYMENT */}
|
{/* DEPLOYMENT */}
|
||||||
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
||||||
|
{project.deployments &&
|
||||||
|
project.deployments.length > 0 &&
|
||||||
|
project.deployments.map((deployment) => (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Link to="#">
|
<Link to={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}>
|
||||||
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||||
{liveDomain?.name}{' '}
|
{`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
|
||||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</OverviewInfo>
|
</OverviewInfo>
|
||||||
|
|
||||||
{/* DEPLOYMENT DATE */}
|
{/* DEPLOYMENT DATE */}
|
||||||
|
@ -106,6 +106,8 @@ export const deployment0: Deployment = {
|
|||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
||||||
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
||||||
|
minimumPayment: '1000alnt',
|
||||||
|
baseDomain: 'pwa.example.com',
|
||||||
},
|
},
|
||||||
applicationDeploymentRequestId:
|
applicationDeploymentRequestId:
|
||||||
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
||||||
@ -132,8 +134,12 @@ export const project: Project = {
|
|||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
||||||
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
||||||
}
|
minimumPayment: '1000alnt',
|
||||||
|
baseDomain: 'pwa.example.com',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
paymentAddress: '0x657868687686rb4787987br8497298r79284797487',
|
||||||
|
txHash: '74btygeuydguygf838gcergurcbhuedbcjhu',
|
||||||
webhooks: ['beepboop'],
|
webhooks: ['beepboop'],
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
fundsReleased: true,
|
fundsReleased: true,
|
||||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof AddEnvironmentVariableRow> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof Config> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains/add/config',
|
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains/add/config',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@ const meta: Meta<typeof DeleteProjectDialog> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof SetupDomain> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains',
|
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -9,3 +9,4 @@ export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
|
|||||||
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
|
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
|
||||||
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
|
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
|
||||||
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
|
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
|
||||||
|
export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
|
||||||
|
@ -1,34 +1,36 @@
|
|||||||
import React from 'react';
|
// import React from 'react';
|
||||||
import Bugsnag from '@bugsnag/js';
|
// import Bugsnag from '@bugsnag/js';
|
||||||
import BugsnagPluginReact from '@bugsnag/plugin-react';
|
// import BugsnagPluginReact from '@bugsnag/plugin-react';
|
||||||
import BugsnagPerformance from '@bugsnag/browser-performance';
|
// import BugsnagPerformance from '@bugsnag/browser-performance';
|
||||||
|
|
||||||
import { VITE_BUGSNAG_API_KEY } from './constants';
|
// import { VITE_BUGSNAG_API_KEY } from './constants';
|
||||||
|
|
||||||
if (VITE_BUGSNAG_API_KEY) {
|
// if (VITE_BUGSNAG_API_KEY) {
|
||||||
Bugsnag.start({
|
// Bugsnag.start({
|
||||||
apiKey: VITE_BUGSNAG_API_KEY,
|
// apiKey: VITE_BUGSNAG_API_KEY,
|
||||||
plugins: [new BugsnagPluginReact()],
|
// plugins: [new BugsnagPluginReact()],
|
||||||
});
|
// });
|
||||||
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
|
// BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
|
// export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
|
||||||
|
|
||||||
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
|
// export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
|
||||||
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
// ? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
||||||
: ({ children }: any) => children;
|
// : ({ children }: any) => children;
|
||||||
|
|
||||||
export function logError(error: Error) {
|
// export function logError(error: Error) {
|
||||||
let errors: any[] = [error];
|
// let errors: any[] = [error];
|
||||||
let safety = 0;
|
// let safety = 0;
|
||||||
while (errors[errors.length - 1].cause && safety < 10) {
|
// while (errors[errors.length - 1].cause && safety < 10) {
|
||||||
errors.push('::caused by::', errors[errors.length - 1].cause);
|
// errors.push('::caused by::', errors[errors.length - 1].cause);
|
||||||
safety += 1;
|
// safety += 1;
|
||||||
}
|
// }
|
||||||
console.error(...errors);
|
// console.error(...errors);
|
||||||
|
|
||||||
if (VITE_BUGSNAG_API_KEY) {
|
// if (VITE_BUGSNAG_API_KEY) {
|
||||||
Bugsnag.notify(error);
|
// Bugsnag.notify(error);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
export const LogErrorBoundary = ({ children }: any) => children;
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { SiweMessage } from 'siwe';
|
|
||||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { BASE_URL } from './constants';
|
|
||||||
|
|
||||||
const domain = window.location.host;
|
|
||||||
const origin = window.location.origin;
|
|
||||||
|
|
||||||
export async function signInWithEthereum(
|
|
||||||
chainId: number,
|
|
||||||
action: 'signup' | 'login',
|
|
||||||
wallet: PKPEthersWallet,
|
|
||||||
) {
|
|
||||||
const message = await createSiweMessage(
|
|
||||||
chainId,
|
|
||||||
await wallet.getAddress(),
|
|
||||||
'Sign in with Ethereum to the app.',
|
|
||||||
);
|
|
||||||
const signature = await wallet.signMessage(message);
|
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}/auth/validate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ action, message, signature }),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
return (await res.json()) as { success: boolean; error?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSiweMessage(
|
|
||||||
chainId: number,
|
|
||||||
address: string,
|
|
||||||
statement: string,
|
|
||||||
) {
|
|
||||||
const message = new SiweMessage({
|
|
||||||
domain,
|
|
||||||
address,
|
|
||||||
statement,
|
|
||||||
uri: origin,
|
|
||||||
version: '1',
|
|
||||||
chainId,
|
|
||||||
nonce: uuid().replace(/[^a-z0-9]/g, ''),
|
|
||||||
});
|
|
||||||
return message.prepareMessage();
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Snowball, SnowballChain } from '@snowballtools/js-sdk';
|
|
||||||
import {
|
|
||||||
// LitAppleAuth,
|
|
||||||
LitGoogleAuth,
|
|
||||||
LitPasskeyAuth,
|
|
||||||
} from '@snowballtools/auth-lit';
|
|
||||||
import { VITE_LIT_RELAY_API_KEY } from './constants';
|
|
||||||
|
|
||||||
export const snowball = Snowball.withAuth({
|
|
||||||
google: LitGoogleAuth.configure({
|
|
||||||
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
|
||||||
}),
|
|
||||||
// apple: LitAppleAuth.configure({
|
|
||||||
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
|
||||||
// }),
|
|
||||||
passkey: LitPasskeyAuth.configure({
|
|
||||||
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
|
||||||
}),
|
|
||||||
}).create({
|
|
||||||
initialChain: SnowballChain.sepolia,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useSnowball() {
|
|
||||||
const [state, setState] = useState(100);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Subscribe and directly return the unsubscribe function
|
|
||||||
return snowball.subscribe(() => setState(state + 1));
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return snowball;
|
|
||||||
}
|
|
8
packages/frontend/src/utils/web3modal.ts
Normal file
8
packages/frontend/src/utils/web3modal.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { WalletConnectModal } from '@walletconnect/modal';
|
||||||
|
|
||||||
|
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
|
||||||
|
|
||||||
|
export const walletConnectModal = new WalletConnectModal({
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
chains: [],
|
||||||
|
});
|
@ -4,6 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./src",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsup src/index.ts --dts --format esm,cjs --sourcemap",
|
"build": "npx tsup src/index.ts --dts --format esm,cjs --sourcemap",
|
||||||
"lint": "tsc --noEmit"
|
"lint": "tsc --noEmit"
|
||||||
|
@ -424,4 +424,33 @@ export class GQLClient {
|
|||||||
|
|
||||||
return data.getAuctionData;
|
return data.getAuctionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDeployers(): Promise<types.GetDeployersResponse> {
|
||||||
|
const { data } = await this.client.query({
|
||||||
|
query: queries.getDeployers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAddress(): Promise<string> {
|
||||||
|
const { data } = await this.client.query({
|
||||||
|
query: queries.getAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTx(txHash: string, amount: string, senderAddress: string): Promise<boolean> {
|
||||||
|
const { data } = await this.client.query({
|
||||||
|
query: queries.verifyTx,
|
||||||
|
variables: {
|
||||||
|
txHash,
|
||||||
|
amount,
|
||||||
|
senderAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.verifyTx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,13 @@ query ($projectId: String!) {
|
|||||||
prodBranch
|
prodBranch
|
||||||
auctionId
|
auctionId
|
||||||
deployers {
|
deployers {
|
||||||
deployerApiUrl
|
|
||||||
deployerId
|
|
||||||
deployerLrn
|
deployerLrn
|
||||||
|
deployerId
|
||||||
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
framework
|
framework
|
||||||
repository
|
repository
|
||||||
@ -54,6 +57,9 @@ query ($projectId: String!) {
|
|||||||
commitHash
|
commitHash
|
||||||
createdAt
|
createdAt
|
||||||
environment
|
environment
|
||||||
|
deployer {
|
||||||
|
baseDomain
|
||||||
|
}
|
||||||
domain {
|
domain {
|
||||||
status
|
status
|
||||||
branch
|
branch
|
||||||
@ -81,10 +87,13 @@ query ($organizationSlug: String!) {
|
|||||||
framework
|
framework
|
||||||
auctionId
|
auctionId
|
||||||
deployers {
|
deployers {
|
||||||
deployerApiUrl
|
|
||||||
deployerId
|
|
||||||
deployerLrn
|
deployerLrn
|
||||||
|
deployerId
|
||||||
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
prodBranch
|
prodBranch
|
||||||
webhooks
|
webhooks
|
||||||
@ -145,9 +154,10 @@ query ($projectId: String!) {
|
|||||||
commitMessage
|
commitMessage
|
||||||
url
|
url
|
||||||
deployer {
|
deployer {
|
||||||
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerLrn,
|
deployerApiUrl
|
||||||
deployerApiUrl,
|
minimumPayment
|
||||||
}
|
}
|
||||||
environment
|
environment
|
||||||
isCurrent
|
isCurrent
|
||||||
@ -208,10 +218,13 @@ query ($searchText: String!) {
|
|||||||
framework
|
framework
|
||||||
auctionId
|
auctionId
|
||||||
deployers {
|
deployers {
|
||||||
deployerApiUrl
|
|
||||||
deployerId
|
|
||||||
deployerLrn
|
deployerLrn
|
||||||
|
deployerId
|
||||||
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
prodBranch
|
prodBranch
|
||||||
webhooks
|
webhooks
|
||||||
@ -307,3 +320,26 @@ query ($auctionId: String!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const getDeployers = gql`
|
||||||
|
query {
|
||||||
|
deployers {
|
||||||
|
deployerLrn
|
||||||
|
deployerId
|
||||||
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getAddress = gql`
|
||||||
|
query {
|
||||||
|
address
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const verifyTx = gql`
|
||||||
|
query ($txHash: String!, $amount: String!, $senderAddress: String!) {
|
||||||
|
verifyTx(txHash: $txHash, amount: $amount, senderAddress: $senderAddress)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { addProjectFromTemplate } from "./mutations";
|
|
||||||
// Note: equivalent to types present in GQL schema
|
// Note: equivalent to types present in GQL schema
|
||||||
|
|
||||||
export enum Role {
|
export enum Role {
|
||||||
@ -117,9 +116,11 @@ export type Deployment = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Deployer = {
|
export type Deployer = {
|
||||||
deployerApiUrl: string;
|
|
||||||
deployerId: string;
|
|
||||||
deployerLrn: string;
|
deployerLrn: string;
|
||||||
|
deployerId: string;
|
||||||
|
deployerApiUrl: string;
|
||||||
|
baseDomain: string;
|
||||||
|
minimumPayment: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrganizationMember = {
|
export type OrganizationMember = {
|
||||||
@ -178,6 +179,8 @@ export type Project = {
|
|||||||
framework: string;
|
framework: string;
|
||||||
deployers: [Deployer]
|
deployers: [Deployer]
|
||||||
auctionId: string;
|
auctionId: string;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
fundsReleased: boolean;
|
fundsReleased: boolean;
|
||||||
webhooks: string[];
|
webhooks: string[];
|
||||||
members: ProjectMember[];
|
members: ProjectMember[];
|
||||||
@ -233,6 +236,10 @@ export type GetDomainsResponse = {
|
|||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetDeployersResponse = {
|
||||||
|
deployers: Deployer[];
|
||||||
|
};
|
||||||
|
|
||||||
export type SearchProjectsResponse = {
|
export type SearchProjectsResponse = {
|
||||||
searchProjects: Project[];
|
searchProjects: Project[];
|
||||||
};
|
};
|
||||||
@ -303,6 +310,8 @@ export type AddProjectFromTemplateInput = {
|
|||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddProjectInput = {
|
export type AddProjectInput = {
|
||||||
@ -310,6 +319,8 @@ export type AddProjectInput = {
|
|||||||
repository: string;
|
repository: string;
|
||||||
prodBranch: string;
|
prodBranch: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateProjectInput = {
|
export type UpdateProjectInput = {
|
||||||
|
Loading…
Reference in New Issue
Block a user