Pass payment tx hash in deployment request (#19)

Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#19
This commit is contained in:
nabarun 2024-10-29 09:12:39 +00:00
parent 519e318190
commit 0f18bc978e
8 changed files with 121 additions and 56 deletions

View File

@ -18,6 +18,9 @@ export class Deployer {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
minimumPayment!: string | null; minimumPayment!: string | null;
@Column('varchar', { nullable: true })
paymentAddress!: string | null;
@ManyToMany(() => Project, (project) => project.deployers) @ManyToMany(() => Project, (project) => project.deployers)
projects!: Project[]; projects!: Project[];
} }

View File

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

View File

@ -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,12 +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') @Column('varchar')
paymentAddress!: string; paymentAddress!: string;
@Column('varchar')
txHash!: string;
@Column({ @Column({
type: 'simple-array' type: 'simple-array'
}) })

View File

@ -5,8 +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 { Account, 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 { IndexedTx } from '@cosmjs/stargate'; import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { import {
@ -244,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;
@ -281,6 +282,7 @@ 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);
@ -322,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;
@ -428,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;
@ -436,7 +444,9 @@ 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);
@ -484,19 +494,23 @@ export class Registry {
return this.registry.getAuctionsByIds([auctionId]); return this.registry.getAuctionsByIds([auctionId]);
} }
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<any> { async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); 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(() => await registryTransactionWithRetry(() =>
this.registry.sendCoins( laconicClient.sendTokens(account.address, receiverAddress,
[
{ {
amount,
denom: 'alnt', denom: 'alnt',
destinationAddress: receiverAddress amount
}, }
this.registryConfig.privateKey, ],
fee fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
)
); );
return txResponse;
} }
async getAccount(): Promise<Account> { async getAccount(): Promise<Account> {

View File

@ -140,6 +140,7 @@ type Deployer {
deployerId: String! deployerId: String!
deployerApiUrl: String! deployerApiUrl: String!
minimumPayment: String minimumPayment: String
paymentAddress: String
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
} }

View File

@ -648,7 +648,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
}); });
} }
@ -660,6 +662,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, {
@ -886,6 +890,7 @@ export class Service {
per_page: 1, per_page: 1,
}); });
if (auctionParams) {
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
const deploymentData = { const deploymentData = {
project, project,
@ -895,12 +900,43 @@ export class Service {
commitHash: latestCommit.sha, commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message, commitMessage: latestCommit.commit.message,
}; };
if (auctionParams) {
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);
} }
@ -1141,14 +1177,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, {
@ -1342,19 +1382,22 @@ export class Service {
} }
const auction = await this.getAuctionData(project.auctionId); const auction = await this.getAuctionData(project.auctionId);
const totalAuctionPrice = Number(auction.maxPrice.quantity) * auction.numProviders;
let amountToBeReturned; let amountToBeReturned;
if (winningDeployersPresent) { if (winningDeployersPresent) {
amountToBeReturned = auction.winnerPrice * auction.numProviders; amountToBeReturned = totalAuctionPrice - auction.winnerAddresses.length * Number(auction.winnerPrice.quantity);
} else { } else {
amountToBeReturned = auction.maxPrice * auction.numProviders; amountToBeReturned = totalAuctionPrice;
} }
if (amountToBeReturned !== 0) {
await this.laconicRegistry.sendTokensToAccount( await this.laconicRegistry.sendTokensToAccount(
project.paymentAddress, project.paymentAddress,
amountToBeReturned.toString() amountToBeReturned.toString()
); );
} }
}
async getDeployers(): Promise<Deployer[]> { async getDeployers(): Promise<Deployer[]> {
const dbDeployers = await this.db.getDeployers(); const dbDeployers = await this.db.getDeployers();
@ -1384,7 +1427,8 @@ 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 minimumPayment = record.attributes.minimumPayment;
const paymentAddress = record.attributes.paymentAddress;
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1); const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
const deployerData = { const deployerData = {
@ -1392,7 +1436,8 @@ export class Service {
deployerId, deployerId,
deployerApiUrl, deployerApiUrl,
baseDomain, baseDomain,
minimumPayment minimumPayment,
paymentAddress
}; };
// TODO: Update deployers table in a separate job // TODO: Update deployers table in a separate job

View File

@ -173,17 +173,13 @@ const Configure = () => {
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => { async (createFormData: FieldValues) => {
if (!selectedAccount) {
return;
}
const senderAddress = selectedAccount;
const deployerLrn = createFormData.lrn; const deployerLrn = createFormData.lrn;
const deployer = deployers.find( const deployer = deployers.find(
(deployer) => deployer.deployerLrn === deployerLrn, (deployer) => deployer.deployerLrn === deployerLrn,
); );
let amount: string; let amount: string;
let senderAddress: string;
let txHash: string; let txHash: string;
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) { if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
toast({ toast({
@ -194,7 +190,12 @@ const Configure = () => {
}); });
txHash = ''; txHash = '';
senderAddress = '';
} else { } else {
if (!selectedAccount) return;
senderAddress = selectedAccount.split(':')[2];
if (createFormData.option === 'LRN') { if (createFormData.option === 'LRN') {
amount = deployer?.minimumPayment!; amount = deployer?.minimumPayment!;
} else { } else {
@ -220,8 +221,9 @@ const Configure = () => {
const isTxHashValid = await verifyTx( const isTxHashValid = await verifyTx(
senderAddress, senderAddress,
txHash, txHash,
amount.toString(), amountToBePaid.toString(),
); );
if (isTxHashValid === false) { if (isTxHashValid === false) {
console.error('Invalid Tx hash', txHash); console.error('Invalid Tx hash', txHash);
return; return;
@ -243,7 +245,7 @@ const Configure = () => {
const projectId = await createProject( const projectId = await createProject(
createFormData, createFormData,
environmentVariables, environmentVariables,
senderAddress.split(':')[2], senderAddress,
txHash, txHash,
); );

View File

@ -442,7 +442,7 @@ export class GQLClient {
} }
async verifyTx(txHash: string, amount: string, senderAddress: string): Promise<boolean> { async verifyTx(txHash: string, amount: string, senderAddress: string): Promise<boolean> {
const { data: verifyTx } = await this.client.query({ const { data } = await this.client.query({
query: queries.verifyTx, query: queries.verifyTx,
variables: { variables: {
txHash, txHash,
@ -451,6 +451,6 @@ export class GQLClient {
} }
}); });
return verifyTx; return data.verifyTx;
} }
} }