Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement revokeDelegation(ByDelegator|ByProvider) #514

Merged
merged 14 commits into from
Sep 17, 2024
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
86 changes: 81 additions & 5 deletions apps/account-api/src/controllers/v1/delegation-v1.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { ReadOnlyGuard } from '#account-api/guards/read-only.guard';
import { DelegationService } from '#account-api/services/delegation.service';
import { DelegationService } from '#account-api/services';
import { TransactionResponse } from '#account-lib/types/dtos';
import { DelegationResponse } from '#account-lib/types/dtos/delegation.response.dto';
import { Controller, Get, HttpCode, HttpException, HttpStatus, Logger, Param, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import {
Body,
Controller,
Get,
HttpCode,
HttpException,
HttpStatus,
Logger,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import {
RevokeDelegationPayloadRequestDto,
RevokeDelegationPayloadResponseDto,
} from '#account-lib/types/dtos/revokeDelegation.request.dto';

@Controller('v1/delegation')
@ApiTags('v1/delegation')
Expand All @@ -14,11 +30,17 @@ export class DelegationControllerV1 {
this.logger = new Logger(this.constructor.name);
}

// Delegation endpoint
@Get(':msaId')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Get the delegation information associated with an MSA Id' })
@ApiOkResponse({ description: 'Found delegation information' })
@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') msaId: string): Promise<DelegationResponse> {
try {
const delegation = await this.delegationService.getDelegation(msaId);
Expand All @@ -28,4 +50,58 @@ export class DelegationControllerV1 {
throw new HttpException('Failed to find the delegation', HttpStatus.BAD_REQUEST);
}
}

@Get('revokeDelegation/:accountId/:providerId')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Get a properly encoded RevokeDelegationPayload that can be signed' })
@ApiOkResponse({
description: 'Returned an encoded RevokeDelegationPayload for signing',
type: RevokeDelegationPayloadResponseDto,
})
/**
* Retrieves the revoke delegation payload for a given provider ID.
* This encoded payload can be signed by the user to revoke the delegation to the given provider id.
* The payload can be used to verify the encoded payload is correct before signing.
* See the Frequency Rust Docs for more information:
* https://frequency-chain.github.io/frequency/pallet_msa/pallet/struct.Pallet.html#method.revoke_delegation_by_delegator
*
* @param providerId - The ID of the provider.
* @returns A promise that resolves to a RevokeDelegationPayloadRequest object containing the payload and encoded payload.
* @throws {HttpException} If there is an error generating the RevokeDelegationPayload.
*/
async getRevokeDelegationPayload(
@Param('accountId') accountId: string,
@Param('providerId') providerId: string,
): Promise<RevokeDelegationPayloadResponseDto> {
try {
this.logger.verbose(`Getting RevokeDelegationPayload for account ${accountId} and provider ${providerId}`);
return this.delegationService.getRevokeDelegationPayload(accountId, providerId);
} catch (error) {
this.logger.error(error);
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException('Failed to generate the RevokeDelegationPayload', HttpStatus.BAD_REQUEST);
}
}
}

// Pass through the revoke delegation request to the blockchain
@Post('revokeDelegation')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Request to revoke a delegation' })
@ApiCreatedResponse({ description: 'Created and queued request to revoke a delegation', type: TransactionResponse })
@ApiBody({ type: RevokeDelegationPayloadRequestDto })
async postRevokeDelegation(
@Body()
revokeDelegationRequest: RevokeDelegationPayloadRequestDto,
): Promise<TransactionResponse> {
try {
this.logger.verbose(revokeDelegationRequest, 'Posting RevokeDelegationPayloadRequest');
return this.delegationService.postRevokeDelegation(revokeDelegationRequest);
} catch (error) {
this.logger.error(error);
throw new HttpException('Failed to revoke the delegation', HttpStatus.BAD_REQUEST);
}
}
}
50 changes: 49 additions & 1 deletion apps/account-api/src/services/delegation.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { BlockchainService } from '#account-lib/blockchain/blockchain.service';
import { ConfigService } from '#account-lib/config/config.service';
import { TransactionResponse } from '#account-lib/types/dtos';
import { DelegationResponse } from '#account-lib/types/dtos/delegation.response.dto';
import { Injectable, Logger } from '@nestjs/common';
import {
PublishRevokeDelegationRequestDto,
RevokeDelegationPayloadRequestDto,
RevokeDelegationPayloadResponseDto,
} from '#account-lib/types/dtos/revokeDelegation.request.dto';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { TransactionType } from '#account-lib';
import { EnqueueService } from '#account-lib/services/enqueue-request.service';

@Injectable()
export class DelegationService {
Expand All @@ -10,6 +18,7 @@ export class DelegationService {
constructor(
private configService: ConfigService,
private blockchainService: BlockchainService,
private enqueueService: EnqueueService,
) {
this.logger = new Logger(this.constructor.name);
}
Expand All @@ -36,4 +45,43 @@ export class DelegationService {
}
throw new Error('Invalid msaId.');
}

async getRevokeDelegationPayload(accountId: string, providerId: string): Promise<RevokeDelegationPayloadResponseDto> {
try {
const msaId = await this.blockchainService.publicKeyToMsaId(accountId);
if (!msaId) {
throw new HttpException('MSA ID for account not found', HttpStatus.NOT_FOUND);
}

// Validate that the providerId is a registered provider, also checks for valid MSA ID
const providerRegistry = await this.blockchainService.getProviderToRegistryEntry(providerId);
if (!providerRegistry) {
throw new HttpException('Provider not found', HttpStatus.BAD_REQUEST);
}

// Validate that delegations exist for this msaId
const delegations = await this.getDelegation(msaId);
if (delegations.providerId !== providerId) {
throw new HttpException('Delegation not found', HttpStatus.NOT_FOUND);
}
} catch (e: any) {
this.logger.error(`Failed to get revoke delegation payload: ${e.toString()}`);
throw new Error('Failed to get revoke delegation payload');
}
return this.blockchainService.createRevokedDelegationPayload(accountId, providerId);
}

async postRevokeDelegation(revokeDelegationRequest: RevokeDelegationPayloadRequestDto): Promise<TransactionResponse> {
try {
this.logger.verbose(`Posting revoke delegation request for account ${revokeDelegationRequest.accountId}`);
const referenceId = await this.enqueueService.enqueueRequest<PublishRevokeDelegationRequestDto>({
...revokeDelegationRequest,
type: TransactionType.REVOKE_DELEGATION,
});
return referenceId;
} catch (e: any) {
this.logger.error(`Failed to enqueue revokeDelegation request: ${e.toString()}`);
throw new Error('Failed to enqueue revokeDelegation request');
}
}
}
35 changes: 31 additions & 4 deletions apps/account-api/test/delegations.controller.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-undef */
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { HttpStatus, INestApplication, ValidationPipe } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { EventEmitter2 } from '@nestjs/event-emitter';
import request from 'supertest';
import { ChainUser, ExtrinsicHelper, Schema, SchemaBuilder } from '@projectlibertylabs/frequency-scenario-template';
import { ApiModule } from '../src/api.module';
import { setupProviderAndUsers } from './e2e-setup.mock.spec';
import { CacheMonitorService } from '#account-lib/cache/cache-monitor.service';
import { RevokeDelegationPayloadRequestDto } from '#account-lib/types/dtos/revokeDelegation.request.dto';
import { u8aToHex } from '@polkadot/util';

let users: ChainUser[];
let provider: ChainUser;
Expand Down Expand Up @@ -68,23 +70,23 @@ describe('Delegation Controller', () => {
});
});

it('(GET) /delegation/:msaId with invalid msaId', async () => {
it('(GET) /v1/delegation/:msaId with invalid msaId', async () => {
const invalidMsaId = BigInt(maxMsaId) + 1000n;
await request(httpServer).get(`/v1/delegation/${invalidMsaId.toString()}`).expect(400).expect({
statusCode: 400,
message: 'Failed to find the delegation',
});
});

it('(GET) /delegation/:msaId with a valid MSA that has no delegations', async () => {
it('(GET) /v1/delegation/:msaId with a valid MSA that has no delegations', async () => {
const validMsaId = provider.msaId?.toString(); // use provider's MSA; will have no delegations
await request(httpServer).get(`/v1/delegation/${validMsaId}`).expect(400).expect({
statusCode: 400,
message: 'Failed to find the delegation',
});
});

it('(GET) /delegation/:msaId with valid msaId that has delegations', async () => {
it('(GET) /v1/delegation/:msaId with valid msaId that has delegations', async () => {
const validMsaId = users[0]?.msaId?.toString();
await request(httpServer)
.get(`/v1/delegation/${validMsaId}`)
Expand All @@ -101,4 +103,29 @@ describe('Delegation Controller', () => {
revokedAt: '0x00000000',
});
});

it('(POST) /v1/delegation/revokeDelegation/:accountId/:providerId', async () => {
const providerId = provider.msaId?.toString();
const { keypair } = users[1];
const accountId = keypair.address;
const getPath: string = `/v1/delegation/revokeDelegation/${accountId}/${providerId}`;
const response = await request(app.getHttpServer()).get(getPath).expect(200);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could either:

  • add a separate test for (GET) /v1/delegation/revokeDelegation/:accountId/:providerId specifically, -OR-
  • add some expect statements here to validate the response payload, and continue on to POST

console.log(`response.body = ${JSON.stringify(response.body)}`);
mattheworris marked this conversation as resolved.
Show resolved Hide resolved
const { payloadToSign, encodedExtrinsic } = response.body;

const signature: Uint8Array = keypair.sign(payloadToSign, { withType: true });
console.log(`signature = ${u8aToHex(signature)}`);

const revokeDelegationRequest: RevokeDelegationPayloadRequestDto = {
accountId,
providerId,
encodedExtrinsic,
payloadToSign,
signature: u8aToHex(signature),
};
console.log(`revokeDelegationRequest = ${JSON.stringify(revokeDelegationRequest)}`);

const postPath = '/v1/delegation/revokeDelegation';
await request(app.getHttpServer()).post(postPath).send(revokeDelegationRequest).expect(HttpStatus.CREATED);
});
});
1 change: 1 addition & 0 deletions apps/account-api/test/setup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.ts",
"scripts": {
"main": "tsx index.ts",
"test-revoke-e2e": "tsx test-revoke-e2e.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "commonjs",
Expand Down
57 changes: 57 additions & 0 deletions apps/account-api/test/setup/test-revoke-e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ChainUser, ExtrinsicHelper, initialize } from '@projectlibertylabs/frequency-scenario-template';
import log from 'loglevel';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { hexToU8a, u8aToHex } from '@polkadot/util';
// eslint-disable-next-line import/no-extraneous-dependencies
import axios from 'axios';
// eslint-disable-next-line import/no-relative-packages
import { setupProviderAndUsers } from '../e2e-setup.mock.spec';

const FREQUENCY_URL = process.env.FREQUENCY_URL || 'ws://127.0.0.1:9944';

async function main() {
await cryptoWaitReady();
log.setLevel('trace');
console.log('Connecting...');
await initialize(FREQUENCY_URL);

// eslint-disable-next-line no-use-before-define
await revokeDelegation();
}

async function revokeDelegation() {
await cryptoWaitReady();
const { provider, users } = await setupProviderAndUsers();
const providerId = provider.msaId?.toString();
const { keypair } = users[1];
const accountId = keypair.address;
const getPath: string = `http:/localhost:3013/v1/delegation/revokeDelegation/${accountId}/${providerId}`;
const response = await axios.get(getPath);
const revokeDelegationPayloadResponse = response.data;
console.log(`RevokeDelegationPayloadResponse = ${JSON.stringify(revokeDelegationPayloadResponse)}`);

// From github:https://github.com/polkadot-js/tools/issues/175
// Use the withType option to sign the payload to get the prefix 0x01
// which specifies the SR25519 type of the signature and avoids getting and error about an enum in the next signAsync step
const signature: Uint8Array = keypair.sign(revokeDelegationPayloadResponse.payloadToSign, { withType: true });
console.log('signature:', u8aToHex(signature));

const revokeDelegationRequest = {
accountId,
providerId,
encodedExtrinsic: revokeDelegationPayloadResponse.encodedExtrinsic,
payloadToSign: revokeDelegationPayloadResponse.payloadToSign,
signature: u8aToHex(signature),
};
console.log(`revokeDelegationRequest = ${JSON.stringify(revokeDelegationRequest)}`);

const postPath = 'http:/localhost:3013/v1/delegation/revokeDelegation';
await axios.post(postPath, revokeDelegationRequest);
}

main()
.catch((r) => {
console.error(r);
process.exit(1);
})
.finally(process.exit);
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ export class TransactionPublisherService extends BaseConsumer implements OnAppli
[tx, txHash] = await this.processProxyTxn(trx, job.data.accountId, job.data.signature);
break;
}
case TransactionType.REVOKE_DELEGATION: {
const trx = await this.blockchainService.decodeTransaction(job.data.encodedExtrinsic);
targetEvent = { section: 'msa', method: 'DelegationRevoked' };
[tx, txHash] = await this.processProxyTxn(trx, job.data.accountId, job.data.signature);
this.logger.debug(`tx: ${tx}`);
break;
}
default: {
throw new Error(`Invalid job type.`);
}
Expand Down
Loading