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

fix: Added HBar rate limiter to createFile #2634

Merged
merged 8 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
165 changes: 127 additions & 38 deletions packages/relay/src/lib/clients/sdkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export class SDKClient {
async submitEthereumTransaction(
transactionBuffer: Uint8Array,
callerName: string,
requestId?: string,
requestId: string,
): Promise<{ txResponse: TransactionResponse; fileId: FileId | null }> {
const ethereumTransactionData: EthereumTransactionData = EthereumTransactionData.fromBytes(transactionBuffer);
const ethereumTransaction = new EthereumTransaction();
Expand Down Expand Up @@ -476,6 +476,7 @@ export class SDKClient {

this.logger.info(`${requestIdPrefix} Execute ${transactionType} transaction`);
const resp = await transaction.execute(this.clientMain);

this.logger.info(
`${requestIdPrefix} ${resp.transactionId} ${callerName} ${transactionType} status: ${Status.Success} (${Status.Success._code})`,
);
Expand Down Expand Up @@ -643,60 +644,148 @@ export class SDKClient {
private createFile = async (
callData: Uint8Array,
client: Client,
requestId?: string,
callerName?: string,
interactingEntity?: string,
requestId: string,
callerName: string,
interactingEntity: string,
) => {
const requestIdPrefix = formatRequestIdMessage(requestId);
const hexedCallData = Buffer.from(callData).toString('hex');
const currentDateNow = Date.now();
let createFileId, fileCreateTx, fileAppendTx;

const fileCreateTx = await new FileCreateTransaction()
.setContents(hexedCallData.substring(0, this.fileAppendChunkSize))
.setKeys(client.operatorPublicKey ? [client.operatorPublicKey] : []);
const fileCreateTxResponse = await fileCreateTx.execute(client);
const { fileId } = await fileCreateTxResponse.getReceipt(client);

const createFileRecord = await fileCreateTxResponse.getRecord(this.clientMain);
this.captureMetrics(
SDKClient.transactionMode,
fileCreateTx.constructor.name,
Status.Success,
createFileRecord.transactionFee.toTinybars().toNumber(),
createFileRecord?.contractFunctionResult?.gasUsed,
callerName,
interactingEntity,
);
try {
const shouldLimit = this.hbarLimiter.shouldLimit(currentDateNow, SDKClient.transactionMode, callerName);
if (shouldLimit) {
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
}

if (fileId && callData.length > this.fileAppendChunkSize) {
const fileAppendTx = await new FileAppendTransaction()
.setFileId(fileId)
.setContents(hexedCallData.substring(this.fileAppendChunkSize, hexedCallData.length))
.setChunkSize(this.fileAppendChunkSize)
.setMaxChunks(this.maxChunks);
await fileAppendTx.execute(client);
const fileCreateTx = await new FileCreateTransaction()
.setContents(hexedCallData.substring(0, this.fileAppendChunkSize))
.setKeys(client.operatorPublicKey ? [client.operatorPublicKey] : []);

const fileCreateTxResponse = await fileCreateTx.execute(client);
const { fileId } = await fileCreateTxResponse.getReceipt(client);

// get transaction fee and add expense to limiter
const createFileRecord = await fileCreateTxResponse.getRecord(this.clientMain);
let transactionFee = createFileRecord.transactionFee;
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);

this.captureMetrics(
SDKClient.transactionMode,
fileAppendTx.constructor.name,
fileCreateTx.constructor.name,
Status.Success,
await this.calculateFileAppendTxTotalTinybarsCost(fileAppendTx),
0,
createFileRecord.transactionFee.toTinybars().toNumber(),
createFileRecord?.contractFunctionResult?.gasUsed,
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
callerName,
interactingEntity,
);
}

// Ensure that the calldata file is not empty
if (fileId) {
const fileSize = await (await new FileInfoQuery().setFileId(fileId).execute(client)).size;
if (fileId && callData.length > this.fileAppendChunkSize) {
const fileAppendTx = await new FileAppendTransaction()
.setFileId(fileId)
.setContents(hexedCallData.substring(this.fileAppendChunkSize, hexedCallData.length))
.setChunkSize(this.fileAppendChunkSize)
.setMaxChunks(this.maxChunks);
const fileAppendTxResponse = await fileAppendTx.execute(client);

// get transaction fee and add expense to limiter
const appendFileRecord = await fileAppendTxResponse.getRecord(this.clientMain);
transactionFee = appendFileRecord.transactionFee;
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);

this.captureMetrics(
SDKClient.transactionMode,
fileAppendTx.constructor.name,
Status.Success,
await this.calculateFileAppendTxTotalTinybarsCost(fileAppendTx),
0,
callerName,
interactingEntity,
);
}

// Ensure that the calldata file is not empty
if (fileId) {
const fileSize = await (await new FileInfoQuery().setFileId(fileId).execute(client)).size;

if (callData.length > 0 && fileSize.isZero()) {
throw new SDKClientError({}, `${requestIdPrefix} Created file is empty. `);
if (callData.length > 0 && fileSize.isZero()) {
throw new SDKClientError({}, `${requestIdPrefix} Created file is empty. `);
}
this.logger.trace(`${requestIdPrefix} Created file with fileId: ${fileId} and file size ${fileSize}`);
}
this.logger.trace(`${requestIdPrefix} Created file with fileId: ${fileId} and file size ${fileSize}`);

return fileId;
} catch (error: any) {
const sdkClientError = new SDKClientError(error, error.message);
let transactionFee: number | Hbar = 0;

// if valid network error utilize transaction id
if (sdkClientError.isValidNetworkError()) {
try {
const transactionCreateRecord = await new TransactionRecordQuery()
.setTransactionId(fileCreateTx.transactionId!)
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
.setNodeAccountIds(fileCreateTx.nodeAccountIds!)
.setValidateReceiptStatus(false)
.execute(this.clientMain);
transactionFee = transactionCreateRecord.transactionFee;
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);

this.captureMetrics(
SDKClient.transactionMode,
fileCreateTx.constructor.name,
sdkClientError.status,
transactionFee.toTinybars().toNumber(),
transactionCreateRecord?.contractFunctionResult?.gasUsed,
callerName,
interactingEntity,
);

if (fileAppendTx === null || fileAppendTx === undefined) {
ebadiere marked this conversation as resolved.
Show resolved Hide resolved
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
Nana-EC marked this conversation as resolved.
Show resolved Hide resolved
this.logger.info(
`${requestIdPrefix} ${fileCreateTx.transactionId} ${callerName} ${fileCreateTx.constructor.name} status: ${sdkClientError.status} (${sdkClientError.status._code}), cost: ${transactionFee}`,
);
} else {
const transactionAppendRecord = await new TransactionRecordQuery()
.setTransactionId(fileAppendTx.transactionId!)
.setNodeAccountIds(fileAppendTx.nodeAccountIds!)
.setValidateReceiptStatus(false)
.execute(this.clientMain);
transactionFee = transactionAppendRecord.transactionFee;
this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow);

this.captureMetrics(
SDKClient.transactionMode,
fileCreateTx.constructor.name,
sdkClientError.status,
transactionFee.toTinybars().toNumber(),
transactionCreateRecord?.contractFunctionResult?.gasUsed,
callerName,
interactingEntity,
);

this.logger.info(
`${requestIdPrefix} ${fileCreateTx.transactionId} ${callerName} ${fileCreateTx.constructor.name} status: ${sdkClientError.status} (${sdkClientError.status._code}), cost: ${transactionFee}`,
ebadiere marked this conversation as resolved.
Show resolved Hide resolved
);
}
} catch (err: any) {
const recordQueryError = new SDKClientError(err, err.message);
this.logger.error(
recordQueryError,
`${requestIdPrefix} Error raised during TransactionRecordQuery for ${fileCreateTx.transactionId}`,
);
}
}

this.logger.info(`${requestIdPrefix} HBAR_RATE_LIMIT_EXCEEDED cost: ${transactionFee}`);

if (error instanceof JsonRpcError) {
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
}
throw sdkClientError;
}

return fileId;
return createFileId;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,7 @@ export class EthImpl implements Eth {
*
* @param transaction
*/
async sendRawTransaction(transaction: string, requestIdPrefix?: string): Promise<string | JsonRpcError> {
async sendRawTransaction(transaction: string, requestIdPrefix: string): Promise<string | JsonRpcError> {
if (transaction?.length >= constants.FUNCTION_SELECTOR_CHAR_LENGTH)
this.ethExecutionsCounter
.labels(EthImpl.ethSendRawTransaction, transaction.substring(0, constants.FUNCTION_SELECTOR_CHAR_LENGTH))
Expand Down
Loading
Loading