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

feat: add ipAddressHbarSpendingPlan #2913

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ade525e
feat: Implement `HbarLimitService#addExpense`
victor-yanev Aug 29, 2024
f86bc7e
chore: reduce duplication in hbarLimitService.spec.ts
victor-yanev Aug 29, 2024
3027b35
chore: reduce duplication in hbarLimitService.spec.ts
victor-yanev Aug 29, 2024
146e003
chore: add TODO
victor-yanev Aug 29, 2024
f329cab
chore: small fix in hbarLimitService.spec.ts
victor-yanev Aug 29, 2024
740810a
chore: reset the limiter if needed before checking the daily budget
victor-yanev Aug 29, 2024
9bd9f0b
feat: add ipAddressHbarSpendingPlan
Ivo-Yankov Aug 30, 2024
03dd881
chore: reduce code duplication
Ivo-Yankov Aug 30, 2024
eba1204
nit: fix security alert
Ivo-Yankov Aug 30, 2024
ac46b36
nit: fix type
Ivo-Yankov Aug 30, 2024
99e53af
chore: fix eslint rules
victor-yanev Aug 30, 2024
36d3ca6
Merge branch 'main' into 2901-Implement-add-expense
victor-yanev Aug 30, 2024
abb49fa
chore: run npm install
victor-yanev Aug 30, 2024
97f6c7e
chore: fix .eslintrc.js + remove redundant TODO
victor-yanev Aug 30, 2024
39ced23
chore: revert changes to package-lock.json
victor-yanev Aug 30, 2024
af07187
chore: fix .eslintrc.js
victor-yanev Aug 30, 2024
32ccc27
chore: resolve conflicts
Ivo-Yankov Aug 30, 2024
91a9b1b
chore: add unit tests
Ivo-Yankov Aug 30, 2024
e597e28
chore: remove only
Ivo-Yankov Aug 30, 2024
876fd00
chore: removed duplicated test
Ivo-Yankov Aug 30, 2024
7815452
chore: resolve comments
Ivo-Yankov Aug 30, 2024
7a90b2a
chore: resolve comments
Ivo-Yankov Aug 30, 2024
7db0b37
chore: fix test assertion
Ivo-Yankov Sep 2, 2024
ac62c91
chore: resolve conflicts
Ivo-Yankov Sep 2, 2024
e48087b
chore: resolve conflicts
Ivo-Yankov Sep 2, 2024
5c509be
test: add ut for IPAddressHbarSpendingPlanRepository
Ivo-Yankov Sep 4, 2024
b2e447d
chore: attempt to fix failing ut
Ivo-Yankov Sep 5, 2024
9cac771
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
Ivo-Yankov Sep 5, 2024
fa125d0
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
Ivo-Yankov Sep 9, 2024
5b60a1e
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
Ivo-Yankov Sep 9, 2024
ad169a3
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
Ivo-Yankov Sep 9, 2024
c574cb2
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
quiet-node Sep 10, 2024
d674d71
chore: update ut redis port
Ivo-Yankov Sep 10, 2024
2445746
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
quiet-node Sep 10, 2024
826bc97
chore: update mock redis port
Ivo-Yankov Sep 11, 2024
17a2541
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
Ivo-Yankov Sep 11, 2024
2f9f033
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
ebadiere Sep 11, 2024
f4026df
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
victor-yanev Sep 16, 2024
7a7a29d
Merge branch 'main' into 2888-hbar-rate-limit-redesign-implement-ip-a…
victor-yanev Sep 17, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { IIPAddressHbarSpendingPlan } from '../../types/hbarLimiter/ipAddressHbarSpendingPlan';

export class IPAddressHbarSpendingPlan implements IIPAddressHbarSpendingPlan {
ipAddress: string;
planId: string;

constructor(data: IIPAddressHbarSpendingPlan) {
this.ipAddress = data.ipAddress;
this.planId = data.planId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { CacheService } from '../../../services/cacheService/cacheService';
import { Logger } from 'pino';
import { IIPAddressHbarSpendingPlan } from '../../types/hbarLimiter/ipAddressHbarSpendingPlan';
import { IPAddressHbarSpendingPlanNotFoundError } from '../../types/hbarLimiter/errors';
import { IPAddressHbarSpendingPlan } from '../../entities/hbarLimiter/ipAddressHbarSpendingPlan';

export class IPAddressHbarSpendingPlanRepository {
private readonly collectionKey = 'ipAddressHbarSpendingPlan';
private readonly threeMonthsInMillis = 90 * 24 * 60 * 60 * 1000;

/**
* The cache service used for storing data.
* @private
*/
private readonly cache: CacheService;

/**
* The logger used for logging all output from this class.
* @private
*/
private readonly logger: Logger;

constructor(cache: CacheService, logger: Logger) {
this.cache = cache;
this.logger = logger;
}

/**
* Finds an {@link IPAddressHbarSpendingPlan} for an IP address.
*
* @param {string} ipAddress - The IP address to search for.
* @returns {Promise<IPAddressHbarSpendingPlan>} - The associated plan for the IP address.
*/
async findByAddress(ipAddress: string): Promise<IPAddressHbarSpendingPlan> {
const key = this.getKey(ipAddress);
const addressPlan = await this.cache.getAsync<IIPAddressHbarSpendingPlan>(key, 'findByAddress');
if (!addressPlan) {
throw new IPAddressHbarSpendingPlanNotFoundError(ipAddress);
}
this.logger.trace(`Retrieved IPAddressHbarSpendingPlan with address ${ipAddress}`);
return new IPAddressHbarSpendingPlan(addressPlan);
}

/**
* Saves an {@link IPAddressHbarSpendingPlan} to the cache, linking the plan to the IP address.
*
* @param {IIPAddressHbarSpendingPlan} addressPlan - The plan to save.
* @returns {Promise<void>} - A promise that resolves when the IP address is linked to the plan.
*/
async save(addressPlan: IIPAddressHbarSpendingPlan): Promise<void> {
const key = this.getKey(addressPlan.ipAddress);
await this.cache.set(key, addressPlan, 'save', this.threeMonthsInMillis);
this.logger.trace(`Saved IPAddressHbarSpendingPlan with address ${addressPlan.ipAddress}`);
}

/**
* Deletes an {@link IPAddressHbarSpendingPlan} from the cache, unlinking the plan from the IP address.
*
* @param {string} ipAddress - The IP address to unlink the plan from.
* @returns {Promise<void>} - A promise that resolves when the IP address is unlinked from the plan.
*/
async delete(ipAddress: string): Promise<void> {
const key = this.getKey(ipAddress);
await this.cache.delete(key, 'delete');
this.logger.trace(`Deleted IPAddressHbarSpendingPlan with address ${ipAddress}`);
}

/**
* Gets the cache key for an {@link IPAddressHbarSpendingPlan}.
*
* @param {string} ipAddress - The IP address to get the key for.
* @private
*/
private getKey(ipAddress: string): string {
return `${this.collectionKey}:${ipAddress}`;
}
}
7 changes: 7 additions & 0 deletions packages/relay/src/lib/db/types/hbarLimiter/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ export class EthAddressHbarSpendingPlanNotFoundError extends Error {
this.name = 'EthAddressHbarSpendingPlanNotFoundError';
}
}

export class IPAddressHbarSpendingPlanNotFoundError extends Error {
constructor(ipAddress: string) {
super(`IPAddressHbarSpendingPlan with address ${ipAddress} not found`);
this.name = 'IPAddressHbarSpendingPlanNotFoundError';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

export interface IIPAddressHbarSpendingPlan {
ipAddress: string;
planId: string;
}
25 changes: 21 additions & 4 deletions packages/relay/src/lib/services/hbarLimitService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { SubscriptionType } from '../../db/types/hbarLimiter/subscriptionType';
import { IDetailedHbarSpendingPlan } from '../../db/types/hbarLimiter/hbarSpendingPlan';
import { HbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/hbarSpendingPlanRepository';
import { EthAddressHbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository';
import { IPAddressHbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository';

export class HbarLimitService implements IHbarLimitService {
static readonly ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -80,6 +81,7 @@ export class HbarLimitService implements IHbarLimitService {
constructor(
private readonly hbarSpendingPlanRepository: HbarSpendingPlanRepository,
private readonly ethAddressHbarSpendingPlanRepository: EthAddressHbarSpendingPlanRepository,
private readonly ipAddressHbarSpendingPlanRepository: IPAddressHbarSpendingPlanRepository,
private readonly logger: Logger,
private readonly register: Registry,
private readonly totalBudget: number,
Expand Down Expand Up @@ -305,7 +307,11 @@ export class HbarLimitService implements IHbarLimitService {
}
}
if (ipAddress) {
// TODO: Implement this with https://github.com/hashgraph/hedera-json-rpc-relay/issues/2888
try {
return await this.getSpendingPlanByIPAddress(ipAddress);
} catch (error) {
this.logger.warn(error, `Failed to get spending plan for IP address '${ipAddress}'`);
Ivo-Yankov marked this conversation as resolved.
Show resolved Hide resolved
}
}
return null;
}
Expand All @@ -321,6 +327,17 @@ export class HbarLimitService implements IHbarLimitService {
return this.hbarSpendingPlanRepository.findByIdWithDetails(ethAddressHbarSpendingPlan.planId);
}

/**
* Gets the spending plan for the given IP address.
* @param {string} ipAddress - The IP address to get the spending plan for.
* @returns {Promise<IDetailedHbarSpendingPlan>} - A promise that resolves with the spending plan.
* @private
*/
private async getSpendingPlanByIPAddress(ipAddress: string): Promise<IDetailedHbarSpendingPlan> {
const ipAddressHbarSpendingPlan = await this.ipAddressHbarSpendingPlanRepository.findByAddress(ipAddress);
return this.hbarSpendingPlanRepository.findByIdWithDetails(ipAddressHbarSpendingPlan.planId);
}

/**
* Creates a basic spending plan for the given eth address.
* @param {string} ethAddress - The eth address to create the spending plan for.
Expand All @@ -337,10 +354,10 @@ export class HbarLimitService implements IHbarLimitService {
if (ethAddress) {
this.logger.trace(`Linking spending plan with ID ${spendingPlan.id} to eth address ${ethAddress}`);
await this.ethAddressHbarSpendingPlanRepository.save({ ethAddress, planId: spendingPlan.id });
} else if (ipAddress) {
}
if (ipAddress) {
this.logger.trace(`Linking spending plan with ID ${spendingPlan.id} to ip address ${ipAddress}`);
// TODO: Implement this with https://github.com/hashgraph/hedera-json-rpc-relay/issues/2888
// await this.ipAddressHbarSpendingPlanRepository.save({ ipAddress, planId: spendingPlan.id });
await this.ipAddressHbarSpendingPlanRepository.save({ ipAddress, planId: spendingPlan.id });
}
return spendingPlan;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { IPAddressHbarSpendingPlanRepository } from '../../../../src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository';
import { CacheService } from '../../../../src/lib/services/cacheService/cacheService';
import pino from 'pino';
import { IIPAddressHbarSpendingPlan } from '../../../../src/lib/db/types/hbarLimiter/ipAddressHbarSpendingPlan';
import { IPAddressHbarSpendingPlanNotFoundError } from '../../../../src/lib/db/types/hbarLimiter/errors';
import { randomBytes, uuidV4 } from 'ethers';
import { Registry } from 'prom-client';
import { useInMemoryRedisServer } from '../../../helpers';

chai.use(chaiAsPromised);

describe('IPAddressHbarSpendingPlanRepository', function () {
const logger = pino();
const registry = new Registry();

const tests = (isSharedCacheEnabled: boolean) => {
let cacheService: CacheService;
let repository: IPAddressHbarSpendingPlanRepository;
const ipAddress = '555.555.555.555';
const nonExistingIpAddress = 'xxx.xxx.xxx.xxx';

if (isSharedCacheEnabled) {
useInMemoryRedisServer(logger, 6383);
}

before(() => {
cacheService = new CacheService(logger.child({ name: 'CacheService' }), registry);
repository = new IPAddressHbarSpendingPlanRepository(
cacheService,
logger.child({ name: 'IPAddressHbarSpendingPlanRepository' }),
);
});

after(async () => {
await cacheService.disconnectRedisClient();
});

describe('findByAddress', () => {
it('retrieves an address plan by ip', async () => {
const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) };
await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test');

const result = await repository.findByAddress(ipAddress);
expect(result).to.deep.equal(addressPlan);
});

it('throws an error if address plan is not found', async () => {
await expect(repository.findByAddress(nonExistingIpAddress)).to.be.eventually.rejectedWith(
IPAddressHbarSpendingPlanNotFoundError,
`IPAddressHbarSpendingPlan with address ${nonExistingIpAddress} not found`,
);
});
});

describe('save', () => {
it('saves an address plan successfully', async () => {
const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) };

await repository.save(addressPlan);
const result = await cacheService.getAsync<IIPAddressHbarSpendingPlan>(
`${repository['collectionKey']}:${ipAddress}`,
'test',
);
expect(result).to.deep.equal(addressPlan);
});

it('overwrites an existing address plan', async () => {
const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) };
await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test');

const newPlanId = uuidV4(randomBytes(16));
const newAddressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: newPlanId };
await repository.save(newAddressPlan);
const result = await cacheService.getAsync<IIPAddressHbarSpendingPlan>(
`${repository['collectionKey']}:${ipAddress}`,
'test',
);
expect(result).to.deep.equal(newAddressPlan);
});
});

describe('delete', () => {
it('deletes an address plan successfully', async () => {
const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) };
await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test');

await repository.delete(ipAddress);
const result = await cacheService.getAsync<IIPAddressHbarSpendingPlan>(
`${repository['collectionKey']}:${ipAddress}`,
'test',
);
expect(result).to.be.null;
});

it('does not throw an error if address plan to delete does not exist', async () => {
await expect(repository.delete(nonExistingIpAddress)).to.be.fulfilled;
});
});
};

describe('with shared cache', () => {
tests(true);
});

describe('without shared cache', () => {
tests(false);
});
});
Loading
Loading