Implement payments for app deployments (#17)
All checks were successful
Lint / lint (20.x) (push) Successful in 4m30s
All checks were successful
Lint / lint (20.x) (push) Successful in 4m30s
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75) - Implement funtionality to pay for deployments by connecting wallet using `WalletConnect` ![image](/attachments/842e33e8-7de6-4d91-9008-1c67a259b586) ![image](/attachments/94b2fe39-f753-4e99-a8c2-bda4c0b84897) Co-authored-by: IshaVenikar <ishavenikar7@gmail.com> Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Reviewed-on: #17
This commit is contained in:
parent
b449c299dc
commit
63969ae25a
@ -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'
|
||||||
|
@ -41,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';
|
||||||
|
@ -15,6 +15,9 @@ export class Deployer {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
baseDomain!: string;
|
baseDomain!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
minimumPayment!: string | null;
|
||||||
|
|
||||||
@ManyToMany(() => Project, (project) => project.deployers)
|
@ManyToMany(() => Project, (project) => project.deployers)
|
||||||
projects!: Project[];
|
projects!: Project[];
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,12 @@ export class Project {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
framework!: string | null;
|
framework!: string | null;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
paymentAddress!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
txHash!: 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,
|
||||||
|
@ -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, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||||
|
import { IndexedTx } from '@cosmjs/stargate';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
@ -483,6 +484,36 @@ export class Registry {
|
|||||||
return this.registry.getAuctionsByIds([auctionId]);
|
return this.registry.getAuctionsByIds([auctionId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<any> {
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.sendCoins(
|
||||||
|
{
|
||||||
|
amount,
|
||||||
|
denom: 'alnt',
|
||||||
|
destinationAddress: receiverAddress
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
@ -80,6 +80,21 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
deployers: async (_: any, __: any, context: any) => {
|
deployers: async (_: any, __: any, context: any) => {
|
||||||
return service.getDeployers();
|
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
|
||||||
@ -221,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!]
|
||||||
@ -137,6 +139,7 @@ type Deployer {
|
|||||||
deployerLrn: String!
|
deployerLrn: String!
|
||||||
deployerId: String!
|
deployerId: String!
|
||||||
deployerApiUrl: String!
|
deployerApiUrl: String!
|
||||||
|
minimumPayment: String
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
}
|
}
|
||||||
@ -157,6 +160,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 +169,8 @@ input AddProjectInput {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
template: String
|
template: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
@ -258,6 +265,8 @@ type Query {
|
|||||||
getAuctionData(auctionId: String!): Auction!
|
getAuctionData(auctionId: String!): Auction!
|
||||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||||
deployers: [Deployer]
|
deployers: [Deployer]
|
||||||
|
address: String!
|
||||||
|
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
@ -212,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,
|
||||||
});
|
});
|
||||||
@ -309,6 +312,9 @@ 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 deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
|
const deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
|
||||||
for (const deployer of deployers) {
|
for (const deployer of deployers) {
|
||||||
@ -829,6 +835,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) {
|
||||||
@ -1324,6 +1332,30 @@ 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);
|
||||||
|
|
||||||
|
let amountToBeReturned;
|
||||||
|
if (winningDeployersPresent) {
|
||||||
|
amountToBeReturned = auction.winnerPrice * auction.numProviders;
|
||||||
|
} else {
|
||||||
|
amountToBeReturned = auction.maxPrice * auction.numProviders;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.laconicRegistry.sendTokensToAccount(
|
||||||
|
project.paymentAddress,
|
||||||
|
amountToBeReturned.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getDeployers(): Promise<Deployer[]> {
|
async getDeployers(): Promise<Deployer[]> {
|
||||||
const dbDeployers = await this.db.getDeployers();
|
const dbDeployers = await this.db.getDeployers();
|
||||||
|
|
||||||
@ -1352,13 +1384,15 @@ export class Service {
|
|||||||
const deployerId = record.id;
|
const deployerId = record.id;
|
||||||
const deployerLrn = record.names[0];
|
const deployerLrn = record.names[0];
|
||||||
const deployerApiUrl = record.attributes.apiUrl;
|
const deployerApiUrl = record.attributes.apiUrl;
|
||||||
|
const minimumPayment = record.attributes.minimumPayment
|
||||||
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
|
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
|
||||||
|
|
||||||
const deployerData = {
|
const deployerData = {
|
||||||
deployerLrn,
|
deployerLrn,
|
||||||
deployerId,
|
deployerId,
|
||||||
deployerApiUrl,
|
deployerApiUrl,
|
||||||
baseDomain
|
baseDomain,
|
||||||
|
minimumPayment
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Update deployers table in a separate job
|
// TODO: Update deployers table in a separate job
|
||||||
@ -1369,4 +1403,32 @@ export class Service {
|
|||||||
|
|
||||||
return deployers;
|
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;
|
||||||
|
@ -127,6 +127,7 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||||
repository: "$REPO_URL"
|
repository: "$REPO_URL"
|
||||||
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
@ -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",
|
||||||
|
@ -17,15 +17,13 @@ interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
||||||
const { totalSeconds, pause } = useStopwatch({
|
const { totalSeconds, pause, start } = useStopwatch({
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
offsetTimestamp: offsetTimestamp,
|
offsetTimestamp: offsetTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPaused) {
|
isPaused ? pause() : start();
|
||||||
pause();
|
|
||||||
}
|
|
||||||
}, [isPaused]);
|
}, [isPaused]);
|
||||||
|
|
||||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||||
|
@ -22,6 +22,8 @@ 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;
|
||||||
@ -33,9 +35,17 @@ 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 [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');
|
||||||
@ -55,7 +65,12 @@ const Configure = () => {
|
|||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
const methods = useForm<ConfigureFormValues>({
|
const methods = useForm<ConfigureFormValues>({
|
||||||
defaultValues: { option: 'Auction' },
|
defaultValues: {
|
||||||
|
option: 'Auction',
|
||||||
|
maxPrice: DEFAULT_MAX_PRICE,
|
||||||
|
lrn: '',
|
||||||
|
numProviders: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedOption = methods.watch('option');
|
const selectedOption = methods.watch('option');
|
||||||
@ -66,6 +81,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;
|
||||||
@ -90,6 +107,8 @@ const Configure = () => {
|
|||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
||||||
@ -109,6 +128,8 @@ const Configure = () => {
|
|||||||
prodBranch: defaultBranch!,
|
prodBranch: defaultBranch!,
|
||||||
repository: fullName!,
|
repository: fullName!,
|
||||||
template: 'webapp',
|
template: 'webapp',
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
},
|
},
|
||||||
lrn,
|
lrn,
|
||||||
auctionParams,
|
auctionParams,
|
||||||
@ -136,8 +157,77 @@ 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) => {
|
||||||
|
if (!selectedAccount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderAddress = selectedAccount;
|
||||||
|
const deployerLrn = createFormData.lrn;
|
||||||
|
const deployer = deployers.find(
|
||||||
|
(deployer) => deployer.deployerLrn === deployerLrn,
|
||||||
|
);
|
||||||
|
|
||||||
|
let amount: string;
|
||||||
|
let txHash: string;
|
||||||
|
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
||||||
|
toast({
|
||||||
|
id: 'no-payment-required',
|
||||||
|
title: 'No payment required. Deploying app...',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
txHash = '';
|
||||||
|
} else {
|
||||||
|
if (createFormData.option === 'LRN') {
|
||||||
|
amount = deployer?.minimumPayment!;
|
||||||
|
} else {
|
||||||
|
amount = (
|
||||||
|
createFormData.numProviders * createFormData.maxPrice
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountToBePaid = amount.replace(/\D/g, '').toString();
|
||||||
|
|
||||||
|
const txHashResponse = await cosmosSendTokensHandler(
|
||||||
|
selectedAccount,
|
||||||
|
amountToBePaid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!txHashResponse) {
|
||||||
|
console.error('Tx not successful');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash = txHashResponse;
|
||||||
|
|
||||||
|
const isTxHashValid = await verifyTx(
|
||||||
|
senderAddress,
|
||||||
|
txHash,
|
||||||
|
amount.toString(),
|
||||||
|
);
|
||||||
|
if (isTxHashValid === false) {
|
||||||
|
console.error('Invalid Tx hash', txHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const environmentVariables = createFormData.variables.map(
|
const environmentVariables = createFormData.variables.map(
|
||||||
(variable: any) => {
|
(variable: any) => {
|
||||||
return {
|
return {
|
||||||
@ -153,6 +243,8 @@ const Configure = () => {
|
|||||||
const projectId = await createProject(
|
const projectId = await createProject(
|
||||||
createFormData,
|
createFormData,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
senderAddress.split(':')[2],
|
||||||
|
txHash,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.getEnvironmentVariables(projectId);
|
await client.getEnvironmentVariables(projectId);
|
||||||
@ -183,6 +275,83 @@ const Configure = () => {
|
|||||||
setDeployers(res.deployers);
|
setDeployers(res.deployers);
|
||||||
}, [client]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
fetchDeployers();
|
fetchDeployers();
|
||||||
}, []);
|
}, []);
|
||||||
@ -249,7 +418,10 @@ const Configure = () => {
|
|||||||
</span>
|
</span>
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
onDeployerChange(event.target.value);
|
||||||
|
}}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@ -258,7 +430,7 @@ const Configure = () => {
|
|||||||
key={deployer.deployerLrn}
|
key={deployer.deployerLrn}
|
||||||
value={deployer.deployerLrn}
|
value={deployer.deployerLrn}
|
||||||
>
|
>
|
||||||
{deployer.deployerLrn}
|
{`${deployer.deployerLrn} ${deployer.minimumPayment ? `(${deployer.minimumPayment})` : ''}`}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@ -291,7 +463,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>
|
||||||
@ -318,22 +494,56 @@ const Configure = () => {
|
|||||||
<EnvironmentVariablesForm />
|
<EnvironmentVariablesForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{selectedOption === 'LRN' &&
|
||||||
<Button
|
!selectedDeployer?.minimumPayment ? (
|
||||||
{...buttonSize}
|
<div>
|
||||||
type="submit"
|
<Button
|
||||||
disabled={isLoading}
|
{...buttonSize}
|
||||||
rightIcon={
|
type="submit"
|
||||||
isLoading ? (
|
disabled={isLoading}
|
||||||
<LoadingIcon className="animate-spin" />
|
rightIcon={
|
||||||
) : (
|
isLoading ? (
|
||||||
<ArrowRightCircleFilledIcon />
|
<LoadingIcon className="animate-spin" />
|
||||||
)
|
) : (
|
||||||
}
|
<ArrowRightCircleFilledIcon />
|
||||||
>
|
)
|
||||||
{isLoading ? 'Deploying repo' : 'Deploy repo'}
|
}
|
||||||
</Button>
|
>
|
||||||
</div>
|
{isLoading ? 'Deploying' : 'Deploy'}
|
||||||
|
</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}
|
||||||
|
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;
|
@ -93,20 +93,20 @@ const DeploymentDetailsCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchDeploymentLogs = async () => {
|
const fetchDeploymentLogs = async () => {
|
||||||
|
setDeploymentLogs('Loading logs...');
|
||||||
|
handleOpenDialog();
|
||||||
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
|
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
|
||||||
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
|
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
|
||||||
(res) => res.json(),
|
(res) => res.json(),
|
||||||
);
|
);
|
||||||
if (!statusRes.logAvailable) {
|
if (!statusRes.logAvailable) {
|
||||||
setDeploymentLogs(statusRes.lastState);
|
setDeploymentLogs(statusRes.lastState);
|
||||||
handleOpenDialog();
|
|
||||||
} else {
|
} else {
|
||||||
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
|
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
|
||||||
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
|
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
setDeploymentLogs(logsRes);
|
setDeploymentLogs(logsRes);
|
||||||
handleOpenDialog();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 { StargateClient } from '@cosmjs/stargate';
|
||||||
|
|
||||||
|
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 createCosmosClient = useCallback(async (endpoint: string) => {
|
||||||
|
return await StargateClient.connect(endpoint);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const cosmosAddresses = session.namespaces['cosmos'].accounts;
|
||||||
|
|
||||||
|
const cosmosAccounts = cosmosAddresses.map((address) => ({
|
||||||
|
address,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const allAccounts = cosmosAccounts;
|
||||||
|
|
||||||
|
setAccounts(allAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
populateAccounts();
|
||||||
|
}, [session, createCosmosClient]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -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,7 @@ 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',
|
||||||
},
|
},
|
||||||
status: DeploymentStatus.Ready,
|
status: DeploymentStatus.Ready,
|
||||||
createdBy: {
|
createdBy: {
|
||||||
|
@ -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';
|
||||||
|
|
||||||
@ -169,15 +168,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}
|
||||||
|
@ -106,6 +106,7 @@ 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',
|
||||||
},
|
},
|
||||||
applicationDeploymentRequestId:
|
applicationDeploymentRequestId:
|
||||||
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
||||||
@ -132,8 +133,11 @@ 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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
paymentAddress: '0x657868687686rb4787987br8497298r79284797487',
|
||||||
|
txHash: '74btygeuydguygf838gcergurcbhuedbcjhu',
|
||||||
webhooks: ['beepboop'],
|
webhooks: ['beepboop'],
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
fundsReleased: true,
|
fundsReleased: true,
|
||||||
|
@ -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,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: [],
|
||||||
|
});
|
@ -432,4 +432,25 @@ export class GQLClient {
|
|||||||
|
|
||||||
return data;
|
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: verifyTx } = await this.client.query({
|
||||||
|
query: queries.verifyTx,
|
||||||
|
variables: {
|
||||||
|
txHash,
|
||||||
|
amount,
|
||||||
|
senderAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return verifyTx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,10 @@ query ($projectId: String!) {
|
|||||||
deployerLrn
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerApiUrl
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
framework
|
framework
|
||||||
repository
|
repository
|
||||||
@ -84,7 +87,10 @@ query ($organizationSlug: String!) {
|
|||||||
deployerLrn
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerApiUrl
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
prodBranch
|
prodBranch
|
||||||
webhooks
|
webhooks
|
||||||
@ -148,6 +154,7 @@ query ($projectId: String!) {
|
|||||||
deployerLrn
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerApiUrl
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
environment
|
environment
|
||||||
isCurrent
|
isCurrent
|
||||||
@ -211,7 +218,10 @@ query ($searchText: String!) {
|
|||||||
deployerLrn
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerApiUrl
|
deployerApiUrl
|
||||||
|
minimumPayment
|
||||||
}
|
}
|
||||||
|
paymentAddress
|
||||||
|
txHash
|
||||||
fundsReleased
|
fundsReleased
|
||||||
prodBranch
|
prodBranch
|
||||||
webhooks
|
webhooks
|
||||||
@ -314,6 +324,19 @@ query {
|
|||||||
deployerLrn
|
deployerLrn
|
||||||
deployerId
|
deployerId
|
||||||
deployerApiUrl
|
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)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -119,6 +119,7 @@ export type Deployer = {
|
|||||||
deployerLrn: string;
|
deployerLrn: string;
|
||||||
deployerId: string;
|
deployerId: string;
|
||||||
deployerApiUrl: string;
|
deployerApiUrl: string;
|
||||||
|
minimumPayment: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrganizationMember = {
|
export type OrganizationMember = {
|
||||||
@ -177,6 +178,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[];
|
||||||
@ -306,6 +309,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 = {
|
||||||
@ -313,6 +318,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