Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/api/src/modules/notification/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class NotificationService implements INotificationService {
sessionId: member.id,
to: SocketUsernames.UI,
request_id: undefined,
type: SocketEvents.TRANSACTION_UPDATE,
type: SocketEvents.TRANSACTION,
data: {},
});
socketClient.disconnect();
Expand Down Expand Up @@ -259,7 +259,7 @@ export class NotificationService implements INotificationService {
sessionId: member.id,
to: SocketUsernames.UI,
request_id: undefined,
type: SocketEvents.TRANSACTION_UPDATE,
type: SocketEvents.TRANSACTION,
data: {},
});
socketClient.disconnect();
Expand Down
32 changes: 29 additions & 3 deletions packages/api/src/modules/predicate/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { INotificationService } from '../notification/types';

import { WorkspaceService } from '../workspace/services';
import {
ICheckPredicateBalancesRequest,
ICreatePredicateRequest,
IDeletePredicateRequest,
IFindByHashRequest,
Expand Down Expand Up @@ -69,12 +70,17 @@ export class PredicateController {
// If workspace is not provided, use user's single workspace as default
let effectiveWorkspace = workspace;
if (!workspace?.id) {
console.log('[PREDICATE_CREATE] No workspace provided, fetching user single workspace');
console.log(
'[PREDICATE_CREATE] No workspace provided, fetching user single workspace',
);
effectiveWorkspace = await new WorkspaceService()
.filter({ user: user.id, single: true })
.list()
.then((response: Workspace[]) => response[0]);
console.log('[PREDICATE_CREATE] Using single workspace:', effectiveWorkspace?.id);
console.log(
'[PREDICATE_CREATE] Using single workspace:',
effectiveWorkspace?.id,
);
}

const predicateService = new PredicateService();
Expand Down Expand Up @@ -153,7 +159,10 @@ export class PredicateController {
const predicate = await this.predicateService.findByAddress(address);

if (!predicate) {
console.log('[PREDICATE_FIND_BY_ADDRESS] Predicate NOT found for address:', address);
console.log(
'[PREDICATE_FIND_BY_ADDRESS] Predicate NOT found for address:',
address,
);
throw new NotFound({
type: ErrorTypes.NotFound,
title: 'Predicate not found',
Expand Down Expand Up @@ -423,4 +432,21 @@ export class PredicateController {
return error(e.error || e, e.statusCode);
}
}

async checkPredicateBalances({
params: { predicateId },
user,
network,
}: ICheckPredicateBalancesRequest) {
try {
await this.predicateService.checkBalances({
predicateId,
userId: user.id,
network,
});
return successful(null, Responses.Ok);
} catch (e) {
return error(e.error || e, e.statusCode);
}
}
}
13 changes: 12 additions & 1 deletion packages/api/src/modules/predicate/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const {
tooglePredicateVisibility,
update,
allocation,
checkPredicateBalances,
} = new PredicateController(predicateService, notificationsService);

router.use(authMiddleware);
Expand Down Expand Up @@ -83,6 +84,16 @@ router.put(
validateTooglePredicatePayload,
handleResponse(tooglePredicateVisibility),
);
router.get('/:predicateId/allocation', handleResponse(allocation));
router.get(
'/:predicateId/allocation',
validatePredicateIdParams,
handleResponse(allocation),
);
router.get(
'/check-balances/:predicateId',
validatePredicateIdParams,
permissionMiddlewareById,
handleResponse(checkPredicateBalances),
);

export default router;
112 changes: 106 additions & 6 deletions packages/api/src/modules/predicate/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ import GeneralError, { ErrorTypes } from '@utils/error/GeneralError';
import Internal from '@utils/error/Internal';

import App from '@src/server/app';
import {
calculateBalanceUSD,
calculateReservedCoins,
FuelProvider,
subCoins,
} from '@src/utils';
import { calculateReservedCoins, FuelProvider, subCoins } from '@src/utils';
import { IconUtils } from '@src/utils/icons';
import { bn, BN, Network, ZeroBytes32 } from 'fuels';
import { UserService } from '../user/service';
Expand All @@ -37,6 +32,12 @@ import {
IPredicatePayload,
IPredicateService,
} from './types';
import { BalanceCache } from '@src/server/storage/balance';
import { TransactionCache } from '@src/server/storage/transaction';
import { compareBalances } from '@src/utils/balance';
import { emitBalanceOutdatedPredicate } from '@src/socket/events';
import { SocketUsernames, SocketEvents } from '@src/socket/types';
import { ProviderWithCache } from '@src/utils/ProviderWithCache';

export class PredicateService implements IPredicateService {
private _ordination: IPredicateOrdination = {
Expand Down Expand Up @@ -871,4 +872,103 @@ export class PredicateService implements IPredicateService {
});
}
}

async checkBalances({
predicateId,
userId,
network,
}: {
predicateId: string;
userId: string;
network: Network;
}): Promise<void> {
try {
const predicate = await Predicate.createQueryBuilder('p')
.leftJoinAndSelect('p.workspace', 'workspace')
.select([
'p.id',
'p.predicateAddress',
'p.configurable',
'p.version',
'workspace.id',
])
.where('p.id = :predicateId', { predicateId })
.getOne();

if (!predicate) {
throw new NotFound({
type: ErrorTypes.NotFound,
title: 'Predicate not found',
detail: `No predicate found with id ${predicateId}`,
});
}

const balanceCache = BalanceCache.getInstance();
const instance = await this.instancePredicate(
predicate.configurable,
network.url,
predicate.version,
);

// Only proceed if provider is ProviderWithCache
if (!(instance.provider instanceof ProviderWithCache)) {
return;
}

// Get cached balance
const cachedBalances = await balanceCache.get(
predicate.predicateAddress,
network.chainId,
);

// Get current balance directly from blockchain (bypass cache)
const currentBalances = (
await (instance.provider as ProviderWithCache).getBalancesFromBlockchain(
predicate.predicateAddress,
)
).balances.filter(a => a.amount.gt(0));

if (cachedBalances) {
const _cachedBalances = cachedBalances.filter(a => a.amount.gt(0));

const hasChanged = compareBalances(_cachedBalances, currentBalances);

if (hasChanged) {
// Update cache with fresh data
await balanceCache.set(
predicate.predicateAddress,
currentBalances,
network.chainId,
network.url,
);

// Invalidate transaction cache
const transactionCache = TransactionCache.getInstance();
await transactionCache.invalidate(
predicate.predicateAddress,
network.chainId,
);

// Emit event to notify balance change
emitBalanceOutdatedPredicate(userId, {
sessionId: userId,
to: SocketUsernames.UI,
type: SocketEvents.BALANCE_OUTDATED_PREDICATE,
predicateId: predicate.id,
workspaceId: predicate.workspace.id,
});
}
}
} catch (error) {
if (error instanceof NotFound) {
throw error;
}

throw new Internal({
type: ErrorTypes.Internal,
title: 'Error on check predicate balance',
detail: error?.message || error,
});
}
}
}
12 changes: 12 additions & 0 deletions packages/api/src/modules/predicate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ interface IGetAllocationRequestSchema extends ValidatedRequestSchema {
};
}

interface ICheckPredicateBalancesRequestSchema extends ValidatedRequestSchema {
[ContainerTypes.Params]: {
predicateId: string;
};
}

export type ICreatePredicateRequest = AuthValidatedRequest<ICreatePredicateRequestSchema>;
export type ITooglePredicateRequest = AuthValidatedRequest<ITooglePredicateRequestSchema>;
export type IUpdatePredicateRequest = AuthValidatedRequest<IUpdatePredicateRequestSchema>;
Expand All @@ -179,6 +185,7 @@ export type PredicateWithHidden = Omit<
isHidden: boolean;
};
export type IGetAllocationRequest = AuthValidatedRequest<IGetAllocationRequestSchema>;
export type ICheckPredicateBalancesRequest = AuthValidatedRequest<ICheckPredicateBalancesRequestSchema>;

export interface IPredicateService {
ordination(ordination?: IPredicateOrdination): this;
Expand Down Expand Up @@ -208,4 +215,9 @@ export interface IPredicateService {
authorization: string,
) => Promise<string[]>;
allocation: (params: IPredicateAllocationParams) => Promise<IPredicateAllocation>;
checkBalances: (params: {
predicateId: string;
userId: string;
network: Network;
}) => Promise<void>;
}
14 changes: 14 additions & 0 deletions packages/api/src/modules/user/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
IAllocationRequest,
ICheckHardwareRequest,
ICheckNicknameRequest,
ICheckUserBalancesRequest,
ICreateRequest,
IDeleteRequest,
IFindByNameRequest,
Expand Down Expand Up @@ -461,4 +462,17 @@ export class UserController {
return error(e.error ?? e, e.statusCode);
}
}

async checkUserBalances({ user, workspace, network }: ICheckUserBalancesRequest) {
try {
await this.userService.checkBalances({
userId: user.id,
workspaceId: workspace.id,
network,
});
return successful(null, Responses.Ok);
} catch (e) {
return error(e.error || e, e.statusCode);
}
}
}
5 changes: 5 additions & 0 deletions packages/api/src/modules/user/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ router.get(
AllocationQuerySchema,
handleResponse(userController.allocation),
);
router.get(
'/check-balances',
authMiddleware,
handleResponse(userController.checkUserBalances),
);
router.get('/', authMiddleware, handleResponse(userController.find));
router.get(
'/:id',
Expand Down
Loading
Loading