Skip to content

Commit d596921

Browse files
authored
feat: added deleteFile() method to delete files from the network (#2243)
* feat: added deleteFile() method to delete files from the network Signed-off-by: Logan Nguyen <[email protected]> * fix: inited a fileId variable and pass along Signed-off-by: Logan Nguyen <[email protected]> * fix: set a reasonable max transaction fee Signed-off-by: Logan Nguyen <[email protected]> * fix: replaced throwing exception with warnings Signed-off-by: Logan Nguyen <[email protected]> * fix: assert fileId is a valid entity Signed-off-by: Logan Nguyen <[email protected]> * test: added an acceptance test to test the deleteFile() Signed-off-by: Logan Nguyen <[email protected]> --------- Signed-off-by: Logan Nguyen <[email protected]>
1 parent 9bcd305 commit d596921

File tree

3 files changed

+165
-5
lines changed

3 files changed

+165
-5
lines changed

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
PrecheckStatusError,
4949
TransactionRecordQuery,
5050
Hbar,
51+
FileId,
52+
FileDeleteTransaction,
5153
} from '@hashgraph/sdk';
5254
import { BigNumber } from '@hashgraph/sdk/lib/Transfer';
5355
import { Logger } from 'pino';
@@ -192,7 +194,6 @@ export class SDKClient {
192194

193195
async getFeeSchedule(callerName: string, requestId?: string): Promise<FeeSchedules> {
194196
const feeSchedulesFileBytes = await this.getFileIdBytes(constants.FEE_SCHEDULE_FILE_ID, callerName, requestId);
195-
196197
return FeeSchedules.fromBytes(feeSchedulesFileBytes);
197198
}
198199

@@ -247,11 +248,12 @@ export class SDKClient {
247248
const ethereumTransactionData: EthereumTransactionData = EthereumTransactionData.fromBytes(transactionBuffer);
248249
const ethereumTransaction = new EthereumTransaction();
249250
const interactingEntity = ethereumTransactionData.toJSON()['to'].toString();
251+
let fileId: FileId | null = null;
250252

251253
if (ethereumTransactionData.toBytes().length <= 5120) {
252254
ethereumTransaction.setEthereumData(ethereumTransactionData.toBytes());
253255
} else {
254-
const fileId = await this.createFile(
256+
fileId = await this.createFile(
255257
ethereumTransactionData.callData,
256258
this.clientMain,
257259
requestId,
@@ -269,7 +271,7 @@ export class SDKClient {
269271
const tinybarsGasFee = await this.getTinyBarGasFee('eth_sendRawTransaction');
270272
ethereumTransaction.setMaxTransactionFee(Hbar.fromTinybars(Math.floor(tinybarsGasFee * constants.BLOCK_GAS_LIMIT)));
271273

272-
return this.executeTransaction(ethereumTransaction, callerName, interactingEntity, requestId);
274+
return this.executeTransaction(ethereumTransaction, fileId, callerName, interactingEntity, requestId);
273275
}
274276

275277
async submitContractCallQuery(
@@ -455,6 +457,7 @@ export class SDKClient {
455457

456458
private executeTransaction = async (
457459
transaction: Transaction,
460+
fileId,
458461
callerName: string,
459462
interactingEntity: string,
460463
requestId?: string,
@@ -515,6 +518,14 @@ export class SDKClient {
515518
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
516519
}
517520
throw sdkClientError;
521+
} finally {
522+
/**
523+
* For transactions of type CONTRACT_CREATE, if the contract's bytecode (calldata) exceeds 5120 bytes, HFS is employed to temporarily store the bytecode on the network.
524+
* After transaction execution, whether successful or not, any entity associated with the 'fileId' should be removed from the Hedera network.
525+
*/
526+
if (fileId) {
527+
await this.deleteFile(this.clientMain, fileId, requestId, callerName, interactingEntity);
528+
}
518529
}
519530
};
520531

@@ -692,4 +703,60 @@ export class SDKClient {
692703

693704
return fileId;
694705
};
706+
707+
/**
708+
* @dev Deletes `fileId` file from the Hedera Network utilizing Hashgraph SDK client
709+
* @param client
710+
* @param fileId
711+
* @param requestId
712+
* @param callerName
713+
* @param interactingEntity
714+
*/
715+
private deleteFile = async (
716+
client: Client,
717+
fileId: FileId,
718+
requestId?: string,
719+
callerName?: string,
720+
interactingEntity?: string,
721+
) => {
722+
// format request ID msg
723+
const requestIdPrefix = formatRequestIdMessage(requestId);
724+
725+
try {
726+
// Create fileDeleteTx
727+
const fileDeleteTx = await new FileDeleteTransaction()
728+
.setFileId(fileId)
729+
.setMaxTransactionFee(new Hbar(2))
730+
.freezeWith(client);
731+
732+
// execute fileDeleteTx
733+
const fileDeleteTxResponse = await fileDeleteTx.execute(client);
734+
735+
// get fileDeleteTx's record
736+
const deleteFileRecord = await fileDeleteTxResponse.getRecord(this.clientMain);
737+
738+
// capture metrics
739+
this.captureMetrics(
740+
SDKClient.transactionMode,
741+
fileDeleteTx.constructor.name,
742+
Status.Success,
743+
deleteFileRecord.transactionFee.toTinybars().toNumber(),
744+
deleteFileRecord?.contractFunctionResult?.gasUsed,
745+
callerName,
746+
interactingEntity,
747+
);
748+
749+
// ensure the file is deleted
750+
const receipt = deleteFileRecord.receipt;
751+
const fileInfo = await new FileInfoQuery().setFileId(fileId).execute(client);
752+
753+
if (receipt.status === Status.Success && fileInfo.isDeleted) {
754+
this.logger.trace(`${requestIdPrefix} Deleted file with fileId: ${fileId}`);
755+
} else {
756+
this.logger.warn(`${requestIdPrefix} Fail to delete file with fileId: ${fileId} `);
757+
}
758+
} catch (error: any) {
759+
this.logger.warn(`${requestIdPrefix} ${error['message']} `);
760+
}
761+
};
695762
}

packages/relay/tests/lib/sdkClient.spec.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,29 @@ import { Registry } from 'prom-client';
2626
dotenv.config({ path: path.resolve(__dirname, '../test.env') });
2727
const registry = new Registry();
2828
import pino from 'pino';
29-
import { AccountId, Client, ContractCallQuery, PrivateKey, TransactionId, Hbar, Status } from '@hashgraph/sdk';
29+
import {
30+
AccountId,
31+
Client,
32+
ContractCallQuery,
33+
PrivateKey,
34+
TransactionId,
35+
Hbar,
36+
Status,
37+
FileId,
38+
EthereumTransactionData,
39+
FileInfoQuery,
40+
FileInfo,
41+
FileDeleteTransaction,
42+
TransactionRecord,
43+
} from '@hashgraph/sdk';
3044
const logger = pino();
3145
import constants from '../../src/lib/constants';
3246
import HbarLimit from '../../src/lib/hbarlimiter';
3347
import { SDKClient } from '../../src/lib/clients';
3448
import { CacheService } from '../../src/lib/services/cacheService/cacheService';
49+
import { MAX_GAS_LIMIT_HEX } from './eth/eth-config';
50+
import { getRequestId, signTransaction } from '../helpers';
51+
import { TransactionReceipt } from 'ethers';
3552

3653
describe('SdkClient', async function () {
3754
this.timeout(20000);
@@ -157,4 +174,56 @@ describe('SdkClient', async function () {
157174
sinon.assert.calledOnce(convertGasPriceToTinyBarsStub);
158175
});
159176
});
177+
178+
describe('deleteFile', () => {
179+
// states
180+
const gasPrice = '0xad78ebc5ac620000';
181+
const callerName = 'eth_sendRawTransaction';
182+
const requestId = getRequestId();
183+
const transaction = {
184+
type: 1,
185+
value: 0,
186+
chainId: 0x12a,
187+
gasPrice,
188+
gasLimit: MAX_GAS_LIMIT_HEX,
189+
data: '0x' + '00'.repeat(5121), // large contract
190+
};
191+
192+
before(() => {
193+
// mock captureMetrics
194+
sinon.stub(sdkClient, 'captureMetrics').callsFake(() => {});
195+
});
196+
197+
it('should execute deleteFile', async () => {
198+
// prepare fileId
199+
const signedTx = await signTransaction(transaction);
200+
const transactionBuffer = Buffer.from(signedTx.substring(2), 'hex');
201+
const ethereumTransactionData: EthereumTransactionData = EthereumTransactionData.fromBytes(transactionBuffer);
202+
const fileId = await sdkClient.createFile(ethereumTransactionData.callData, client, requestId, callerName, '');
203+
204+
const fileInfoPreDelete = await new FileInfoQuery().setFileId(fileId).execute(client);
205+
expect(fileInfoPreDelete.fileId).to.deep.eq(fileId);
206+
expect(fileInfoPreDelete.isDeleted).to.be.false;
207+
expect(fileInfoPreDelete.size.toNumber()).to.not.eq(0);
208+
209+
// delete a file
210+
await sdkClient.deleteFile(client, fileId, requestId, callerName, '');
211+
const fileInfoPostDelete = await new FileInfoQuery().setFileId(fileId).execute(client);
212+
expect(fileInfoPostDelete.fileId).to.deep.eq(fileId);
213+
expect(fileInfoPostDelete.isDeleted).to.be.true;
214+
expect(fileInfoPostDelete.size.toNumber()).to.eq(0);
215+
});
216+
217+
it('should print a `warn` log when delete file with invalid fileId', async () => {
218+
// random fileId
219+
const fileId = new FileId(0, 0, 369);
220+
221+
// spy on warn logs
222+
const warnLoggerStub = sinon.stub(sdkClient.logger, 'warn');
223+
224+
// delete a file
225+
await sdkClient.deleteFile(client, fileId, requestId, callerName, '');
226+
expect(warnLoggerStub.calledWithMatch('ENTITY_NOT_ALLOWED_TO_DELETE')).to.be.true;
227+
});
228+
});
160229
});

packages/server/tests/acceptance/rpc_batch1.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ethers } from 'ethers';
2424
import { AliasAccount } from '../clients/servicesClient';
2525
import Assertions from '../helpers/assertions';
2626
import { Utils } from '../helpers/utils';
27-
import { ContractFunctionParameters, TransferTransaction } from '@hashgraph/sdk';
27+
import { ContractFunctionParameters, FileInfo, FileInfoQuery, TransferTransaction } from '@hashgraph/sdk';
2828

2929
// local resources
3030
import parentContractJson from '../contracts/Parent.json';
@@ -1006,6 +1006,30 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () {
10061006
expect(info.created_contract_ids.length).to.be.equal(1);
10071007
});
10081008

1009+
it('should delete the file created while execute "eth_sendRawTransaction" to deploy a large contract', async function () {
1010+
const gasPrice = await relay.gasPrice(requestId);
1011+
const transaction = {
1012+
type: 2,
1013+
chainId: Number(CHAIN_ID),
1014+
nonce: await relay.getAccountNonce(accounts[2].address, requestId),
1015+
maxPriorityFeePerGas: gasPrice,
1016+
maxFeePerGas: gasPrice,
1017+
gasLimit: defaultGasLimit,
1018+
data: '0x' + '00'.repeat(5121),
1019+
};
1020+
1021+
const signedTx = await accounts[2].wallet.signTransaction(transaction);
1022+
const transactionHash = await relay.sendRawTransaction(signedTx, requestId);
1023+
const txInfo = await mirrorNode.get(`/contracts/results/${transactionHash}`, requestId);
1024+
1025+
const contractResult = await mirrorNode.get(`/contracts/${txInfo.contract_id}`);
1026+
const fileInfo = await new FileInfoQuery().setFileId(contractResult.file_id).execute(servicesNode.client);
1027+
expect(fileInfo).to.exist;
1028+
expect(fileInfo instanceof FileInfo).to.be.true;
1029+
expect(fileInfo.isDeleted).to.be.true;
1030+
expect(fileInfo.size.toNumber()).to.eq(0);
1031+
});
1032+
10091033
it('should execute "eth_sendRawTransaction" and fail when deploying too large contract', async function () {
10101034
const gasPrice = await relay.gasPrice(requestId);
10111035
const transaction = {

0 commit comments

Comments
 (0)