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
2 changes: 1 addition & 1 deletion apps/account-api/k6-test/generate/signups.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const signup = (api) => {

const addProviderData = api.registry.createType('PalletMsaAddProvider', {
authorizedMsaId: 1,
schemaIds: [1],
intentIds: [1],
expiration,
});
const createTx = api.tx.msa.createSponsoredAccountWithDelegation(
Expand Down
8 changes: 4 additions & 4 deletions apps/account-api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import {
AccountsControllerV1,
AccountsControllerV2,
DelegationControllerV1,
HandlesControllerV1,
IcsControllerV1,
KeysControllerV1,
DelegationsControllerV2,
HealthController,
} from './controllers';
import { AccountsService, HandlesService, DelegationService, KeysService, SiwfV2Service } from './services';
Expand All @@ -31,12 +29,15 @@ import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { AllExceptionsFilter } from '#utils/filters/exceptions.filter';
import { QueueModule } from '#queue/queue.module';
import { createPrometheusConfig, getPinoHttpOptions } from '#logger-lib';
import { DelegationsControllerV3 } from '#account-api/controllers/v3';
import { DecoratorsModule } from '#utils/decorators/decorators.module';

const configs = [apiConfig, allowReadOnly, cacheConfig, createRateLimitingConfig('account')];
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: configs }),
BlockchainModule.forRootAsync({ readOnly: true }),
DecoratorsModule,
EventEmitterModule.forRoot({
// Use this instance throughout the application
global: true,
Expand Down Expand Up @@ -101,8 +102,7 @@ const configs = [apiConfig, allowReadOnly, cacheConfig, createRateLimitingConfig
controllers: [
AccountsControllerV2,
AccountsControllerV1,
DelegationsControllerV2,
DelegationControllerV1,
DelegationsControllerV3,
HandlesControllerV1,
KeysControllerV1,
IcsControllerV1,
Expand Down
1 change: 0 additions & 1 deletion apps/account-api/src/controllers/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './accounts-v1.controller';
export * from './delegation-v1.controller';
export * from './handles-v1.controller';
export * from './keys-v1.controller';
export * from './ics.controller.v1';
7 changes: 4 additions & 3 deletions apps/account-api/src/controllers/v2/accounts-v2.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import apiConfig, { IAccountApiConfig } from '#account-api/api.config';
import { SiwfV2Service } from '#account-api/services/siwfV2.service';
import { BlockchainRpcQueryService } from '#blockchain/blockchain-rpc-query.service';
import blockchainConfig, { IBlockchainConfig } from '#blockchain/blockchain.config';
import { SCHEMA_NAME_TO_ID } from '#types/constants/schemas';
import { WalletV2LoginRequestDto } from '#types/dtos/account/wallet.v2.login.request.dto';
import { WalletV2LoginResponseDto } from '#types/dtos/account/wallet.v2.login.response.dto';
import { WalletV2RedirectRequestDto } from '#types/dtos/account/wallet.v2.redirect.request.dto';
Expand Down Expand Up @@ -32,7 +31,7 @@ import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
// - `signedRequest`: SIWF Signed Request Payload
// - Provider Signed
// - `callback`
// - `permissions` Schema Id Array
// - `permissions` Intent Id Array
// - Open
// - Credential Request(s)
// - `frequencyRpcUrl`: A public node used by SIWF dApps for sign-in
Expand Down Expand Up @@ -73,7 +72,9 @@ export class AccountsControllerV2 {
this.logger.debug('Received request for Sign In With Frequency v2 Redirect URL', JSON.stringify(query));

const { callbackUrl } = query;
const permissions = (query.permissions || []).map((p) => SCHEMA_NAME_TO_ID.get(p));
const permissions = (
await (query.permissions ? this.blockchainService.getIntentNamesToIds(query.permissions) : Promise.resolve([]))
).map((permission) => permission.intentId);
const credentials = query.credentials || [];

return this.siwfV2Service.getRedirectUrl(callbackUrl, permissions, credentials);
Expand Down
35 changes: 0 additions & 35 deletions apps/account-api/src/controllers/v2/delegation-v2.controller.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/account-api/src/controllers/v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './accounts-v2.controller';
export * from './delegation-v2.controller';
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import { ReadOnlyGuard } from '#account-api/guards/read-only.guard';
import { DelegationService } from '#account-api/services/delegation.service';
import {
DelegationRequestDto,
DelegationResponse,
ProviderDelegationRequestDto,
RevokeDelegationPayloadRequestDto,
RevokeDelegationPayloadResponseDto,
TransactionResponse,
RevokeDelegationPayloadRequestDto,
} from '#types/dtos/account';
import { DelegationResponse } from '#types/dtos/account/delegation.response.dto';
import { IDelegationResponse } from '#types/interfaces/account/delegations.interface';
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, UseGuards } from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AccountIdDto, MsaIdDto, ProviderMsaIdDto } from '#types/dtos/common';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { AccountIdDto, ProviderMsaIdDto } from '#types/dtos/common';

@Controller({ version: '1', path: 'delegation' })
@ApiTags('v1/delegation')
@Controller({ version: '3', path: 'delegations' })
@ApiTags('v3/delegations')
@UseGuards(ReadOnlyGuard) // Apply guard at the controller level
export class DelegationControllerV1 {
export class DelegationsControllerV3 {
constructor(
private delegationService: DelegationService,
@InjectPinoLogger(DelegationControllerV1.name) private readonly logger: PinoLogger,
@InjectPinoLogger(DelegationsControllerV3.name) private readonly logger: PinoLogger,
) {}

// eslint-disable-next-line class-methods-use-this
@Get(':msaId')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Get the delegation information associated with an MSA Id' })
@ApiOperation({ summary: 'Get all delegation information associated with an MSA Id' })
@ApiOkResponse({ description: 'Found delegation information', type: DelegationResponse })
/**
* Retrieves the delegation for a given MSA ID.
*
* @param msaId - The MSA ID for which to retrieve the delegation.
* @returns A Promise that resolves to a DelegationResponse object representing the delegation.
* @throws HttpException if the delegation cannot be found.
*/
async getDelegation(@Param() { msaId }: MsaIdDto): Promise<DelegationResponse> {
return this.delegationService.getDelegation(msaId);
async getDelegation(@Param() { msaId }: DelegationRequestDto): Promise<IDelegationResponse> {
return this.delegationService.getDelegationV3(msaId);
}

// eslint-disable-next-line class-methods-use-this
@Get(':msaId/:providerId')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get an MSA's delegation information for a specific provider" })
@ApiOkResponse({ description: 'Found delegation information', type: DelegationResponse })
async getProviderDelegation(@Param() { msaId, providerId }: ProviderDelegationRequestDto) {
return this.delegationService.getDelegationV3(msaId, providerId);
}

@Get('revokeDelegation/:accountId/:providerId')
Expand Down
1 change: 1 addition & 0 deletions apps/account-api/src/controllers/v3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './delegation-v3.controller';
7 changes: 6 additions & 1 deletion apps/account-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import apiConfig, { IAccountApiConfig } from './api.config';
import { generateSwaggerDoc, initializeSwaggerUI, writeOpenApiFile } from '#openapi/openapi';
import { getCurrentLogLevel, getPinoHttpOptions } from '#logger-lib';
import { useContainer } from 'class-validator';

import { Logger, PinoLogger } from 'nestjs-pino';
import { setupLoggingOverrides, validateEnvironmentVariables } from '#utils/common/common.utils';
Expand Down Expand Up @@ -50,7 +51,7 @@ async function bootstrap() {
'x-tagGroups',
[
{ name: 'accounts', tags: ['v1/accounts', 'v2/accounts'] },
{ name: 'delegations', tags: ['v1/delegation', 'v2/delegations'] },
{ name: 'delegations', tags: ['v3/delegations'] },
{ name: 'handles', tags: ['v1/handles'] },
{ name: 'ics', tags: ['v1/ics'] },
{ name: 'health', tags: ['health'] },
Expand Down Expand Up @@ -78,6 +79,10 @@ async function bootstrap() {

const config = app.get<IAccountApiConfig>(apiConfig.KEY);
try {
// 🔑 Enable Nest DI inside class-validator constraints
useContainer(app.select(ApiModule), {
fallbackOnErrors: true,
});
app.enableShutdownHooks();
app.useGlobalPipes(
new ValidationPipe({
Expand Down
47 changes: 14 additions & 33 deletions apps/account-api/src/services/delegation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { BlockchainRpcQueryService } from '#blockchain/blockchain-rpc-query.serv
import { EnqueueService } from '#account-lib/services/enqueue-request.service';
import { TransactionType } from '#types/account-webhook';
import {
DelegationResponse,
PublishRevokeDelegationRequestDto,
RevokeDelegationPayloadRequestDto,
RevokeDelegationPayloadResponseDto,
TransactionResponse,
} from '#types/dtos/account';
import { DelegationResponse, DelegationResponseV2 } from '#types/dtos/account/delegation.response.dto';
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import blockchainConfig, { IBlockchainConfig } from '#blockchain/blockchain.config';
import { PinoLogger } from 'nestjs-pino';
import { CommonPrimitivesMsaDelegationResponse } from '@polkadot/types/lookup';
import { chainDelegationToNative, IDelegation } from '#types/interfaces/account';

@Injectable()
export class DelegationService {
Expand All @@ -23,28 +25,6 @@ export class DelegationService {
this.logger.setContext(this.constructor.name);
}

async getDelegation(msaId: string): Promise<DelegationResponse> {
const isValidMsaId = await this.blockchainService.isValidMsaId(msaId);
if (isValidMsaId) {
const { providerId } = this.blockchainConf;

const commonPrimitivesMsaDelegation = await this.blockchainService.getCommonPrimitivesMsaDelegation(
msaId,
providerId,
);

if (commonPrimitivesMsaDelegation) {
return {
providerId: providerId.toString(),
schemaPermissions: commonPrimitivesMsaDelegation.schemaPermissions,
revokedAt: commonPrimitivesMsaDelegation.revokedAt,
};
}
throw new NotFoundException(`Failed to find the delegations for ${msaId} and ${providerId}`);
}
throw new NotFoundException(`Invalid msaId ${msaId}`);
}

async getRevokeDelegationPayload(
accountId: string,
providerMsaId: string,
Expand All @@ -67,11 +47,11 @@ export class DelegationService {
}

// Validate that delegations exist for this msaId
const delegations = await this.blockchainService.getProviderDelegationForMsa(msaId, providerMsaId);
if (!delegations) {
const delegation = await this.blockchainService.getProviderDelegationForMsa(msaId, providerMsaId);
if (!delegation) {
throw new NotFoundException(`No delegations found for ${msaId} and ${providerMsaId}`);
}
if (delegations.revokedAtBlock !== 0) {
if (delegation.revokedAtBlock !== 0) {
throw new BadRequestException('Delegation already revoked');
}
return this.blockchainService.createRevokedDelegationPayload(accountId, providerMsaId);
Expand All @@ -80,6 +60,7 @@ export class DelegationService {
async postRevokeDelegation(revokeDelegationRequest: RevokeDelegationPayloadRequestDto): Promise<TransactionResponse> {
try {
this.logger.trace(`Posting revoke delegation request for account ${revokeDelegationRequest.accountId}`);
// noinspection UnnecessaryLocalVariableJS
const referenceId = await this.enqueueService.enqueueRequest<PublishRevokeDelegationRequestDto>({
...revokeDelegationRequest,
type: TransactionType.REVOKE_DELEGATION,
Expand All @@ -91,16 +72,17 @@ export class DelegationService {
}
}

async getDelegationV2(msaId: string, providerId?: string): Promise<DelegationResponseV2> {
async getDelegationV3(msaId: string, providerId?: string): Promise<DelegationResponse> {
const isValidMsaId = await this.blockchainService.isValidMsaId(msaId);
if (!isValidMsaId) {
throw new NotFoundException(`Invalid MSA Id ${msaId}`);
}

let delegations: IDelegation[];
if (providerId) {
const isValidProviderId = await this.blockchainService.isValidMsaId(providerId);
if (!isValidProviderId) {
throw new NotFoundException(`Invalid MSA Id ${providerId}`);
throw new NotFoundException(`Invalid MSA Id for Provider ${providerId}`);
}

const providerInfo = await this.blockchainService.getProviderToRegistryEntry(providerId);
Expand All @@ -113,15 +95,14 @@ export class DelegationService {
throw new NotFoundException(`No delegations found for ${msaId} and ${providerId}`);
}

return {
msaId,
delegations: [delegation],
};
delegations = [delegation];
} else {
delegations = await this.blockchainService.getDelegationsForMsa(msaId);
}

return {
msaId,
delegations: await this.blockchainService.getDelegationsForMsa(msaId),
delegations,
};
}
}
36 changes: 34 additions & 2 deletions apps/account-api/src/services/keys.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import { createKeys } from '#testlib/keys.spec';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { LoggerModule } from 'nestjs-pino';
import { getPinoHttpOptions } from '#logger-lib';
import { mockRedisProvider } from '#testlib';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { useContainer } from 'class-validator';

jest.mock<typeof import('#blockchain/blockchain-rpc-query.service')>('#blockchain/blockchain-rpc-query.service');
jest.mock<typeof import('#account-lib/services/enqueue-request.service')>(
Expand All @@ -38,10 +41,39 @@ describe('KeysService', () => {
await cryptoWaitReady();
const mockBlockchainConfigProvider = buildBlockchainConfigProvider('Sr25519');
const moduleRef = await Test.createTestingModule({
imports: [LoggerModule.forRoot(getPinoHttpOptions())],
providers: [KeysService, BlockchainRpcQueryService, mockBlockchainConfigProvider, mockAccountApiConfigProvider],
imports: [
LoggerModule.forRoot(getPinoHttpOptions()),
EventEmitterModule.forRoot({
// Use this instance throughout the application
global: true,
// set this to `true` to use wildcards
wildcard: false,
// the delimiter used to segment namespaces
delimiter: '.',
// set this to `true` if you want to emit the newListener event
newListener: false,
// set this to `true` if you want to emit the removeListener event
removeListener: false,
// the maximum amount of listeners that can be assigned to an event
maxListeners: 10,
// show event name in memory leak message when more than maximum amount of listeners is assigned
verboseMemoryLeak: false,
// disable throwing uncaughtException if an error event is emitted and it has no listeners
ignoreErrors: false,
}),
],
providers: [
KeysService,
BlockchainRpcQueryService,
mockBlockchainConfigProvider,
mockAccountApiConfigProvider,
mockRedisProvider(),
],
}).compile();

// Important: point class-validator at Nest’s container for this test run
useContainer(moduleRef, { fallbackOnErrors: true });

keysService = moduleRef.get(KeysService);
});

Expand Down
Loading