Handle account sequence mismatch error (#13)
All checks were successful
Lint / lint (20.x) (push) Successful in 4m56s

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

- Handle failed txs due to `account sequence mismatch` error by creating a wrapper for all tx methods and retry the tx if `account sequence mismatch` error occurs

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: #13
This commit is contained in:
nabarun 2024-10-24 11:38:17 +00:00
parent 3d9aedeb7e
commit 3fa60f3cdf
8 changed files with 182 additions and 113 deletions

View File

@ -15,7 +15,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 +108,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 +128,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 +191,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 +218,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}`);
@ -274,14 +286,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: 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}`);
@ -322,12 +336,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;
@ -424,14 +440,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: 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}`);

View File

@ -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}`);
}
}
}

View File

@ -1,7 +1,10 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from 'components/shared/ConfirmDialog';
import { ArrowRightCircleFilledIcon, LoadingIcon } from 'components/shared/CustomIcon'; import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
interface DeleteDeploymentDialogProps extends ConfirmDialogProps { interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
isConfirmButtonLoading?: boolean; isConfirmButtonLoading?: boolean;
@ -20,7 +23,11 @@ export const DeleteDeploymentDialog = ({
dialogTitle="Delete deployment?" dialogTitle="Delete deployment?"
handleCancel={handleCancel} handleCancel={handleCancel}
open={open} open={open}
confirmButtonTitle={isConfirmButtonLoading ? "Deleting deployment" : "Yes, delete deployment"} confirmButtonTitle={
isConfirmButtonLoading
? 'Deleting deployment'
: 'Yes, delete deployment'
}
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonProps={{ confirmButtonProps={{
variant: 'danger', variant: 'danger',

View File

@ -3,7 +3,11 @@ 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, Deployer } from 'gql-client'; import {
AddEnvironmentVariableInput,
AuctionParams,
Deployer,
} from 'gql-client';
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material'; import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
@ -156,32 +160,32 @@ const Configure = () => {
if (templateId) { if (templateId) {
createFormData.option === 'Auction' createFormData.option === 'Auction'
? navigate( ? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, `/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
) )
: navigate( : navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`, `/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
); );
} else { } else {
createFormData.option === 'Auction' createFormData.option === 'Auction'
? navigate( ? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, `/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
) )
: navigate( : navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`, `/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
); );
} }
}, },
[client, createProject, dismiss, toast], [client, createProject, dismiss, toast],
); );
const fetchDeployers = useCallback(async () => { const fetchDeployers = useCallback(async () => {
const res = await client.getDeployers() const res = await client.getDeployers();
setDeployers(res.deployers) setDeployers(res.deployers);
}, [client]) }, [client]);
useEffect(() => { useEffect(() => {
fetchDeployers() fetchDeployers();
}, []) }, []);
return ( return (
<div className="space-y-7 px-4 py-6"> <div className="space-y-7 px-4 py-6">
@ -209,11 +213,18 @@ const Configure = () => {
<Select <Select
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
size='small' size="small"
displayEmpty displayEmpty
sx={{
fontFamily: 'inherit',
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#e0e0e0',
borderRadius: '8px',
},
}}
> >
<MenuItem value="LRN">Deployer LRN</MenuItem>
<MenuItem value="Auction">Create Auction</MenuItem> <MenuItem value="Auction">Create Auction</MenuItem>
<MenuItem value="LRN">Deployer LRN</MenuItem>
</Select> </Select>
)} )}
/> />
@ -240,15 +251,22 @@ const Configure = () => {
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
displayEmpty displayEmpty
size='small' size="small"
> >
{deployers.map((deployer) => ( {deployers.map((deployer) => (
<MenuItem key={deployer.deployerLrn} value={deployer.deployerLrn}> <MenuItem
key={deployer.deployerLrn}
value={deployer.deployerLrn}
>
{deployer.deployerLrn} {deployer.deployerLrn}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{fieldState.error && <FormHelperText>{fieldState.error.message}</FormHelperText>} {fieldState.error && (
<FormHelperText>
{fieldState.error.message}
</FormHelperText>
)}
</FormControl> </FormControl>
)} )}
/> />

View File

@ -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';
@ -104,16 +105,18 @@ const DeploymentDetailsCard = ({
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],
@ -185,8 +188,8 @@ const DeploymentDetailsCard = ({
type="orange" type="orange"
initials={getInitials(deployment.createdBy.name ?? '')} initials={getInitials(deployment.createdBy.name ?? '')}
className="lg:size-5 2xl:size-6" className="lg:size-5 2xl:size-6"
// TODO: Add avatarUrl // TODO: Add avatarUrl
// imageSrc={deployment.createdBy.avatarUrl} // imageSrc={deployment.createdBy.avatarUrl}
></Avatar> ></Avatar>
</div> </div>
<OverflownText <OverflownText

View File

@ -56,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',
@ -77,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',
@ -100,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',
@ -124,7 +124,7 @@ export const DeploymentMenu = ({
setIsConfirmDeleteLoading(false); setIsConfirmDeleteLoading(false);
setDeleteDeploymentDialog((preVal) => !preVal); setDeleteDeploymentDialog((preVal) => !preVal);
if (isDeleted) { if (isDeleted.deleteDeployment) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_removal_requested', id: 'deployment_removal_requested',

View File

@ -105,7 +105,6 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</span> </span>
</div> </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">
Auction Status Auction Status
@ -131,7 +130,10 @@ export const AuctionCard = ({ project }: { project: Project }) => {
Deployer Funds Status Deployer Funds Status
</span> </span>
<div className="ml-2"> <div className="ml-2">
<Tag size="xs" type={fundsStatus ? 'positive' : 'emphasized'}> <Tag
size="xs"
type={fundsStatus ? 'positive' : 'emphasized'}
>
{fundsStatus ? 'RELEASED' : 'LOCKED'} {fundsStatus ? 'RELEASED' : 'LOCKED'}
</Tag> </Tag>
</div> </div>

View File

@ -132,7 +132,7 @@ 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 ',
} },
], ],
webhooks: ['beepboop'], webhooks: ['beepboop'],
icon: 'Icon', icon: 'Icon',