diff --git a/package.json b/package.json index 21d0ffc3..923c7eee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ardrive-core-js", - "version": "1.0.3", + "version": "1.0.4", "description": "ArDrive Core contains the essential back end application features to support the ArDrive CLI and Desktop apps, such as file management, Permaweb upload/download, wallet management and other common functions.", "main": "./lib/exports.js", "types": "./lib/exports.d.ts", diff --git a/src/arfs/arfsdao.test.ts b/src/arfs/arfsdao.test.ts new file mode 100644 index 00000000..d50132e4 --- /dev/null +++ b/src/arfs/arfsdao.test.ts @@ -0,0 +1,91 @@ +import Arweave from 'arweave'; +import { stubEntityID } from '../../tests/stubs'; +import { ByteCount, FeeMultiple, stubTransactionID, UnixTime, W } from '../types'; +import { readJWKFile } from '../utils/common'; +import { ArFSDAO } from './arfsdao'; +import { ArFSPublicFileMetaDataPrototype } from './arfs_prototypes'; +import { ArFSPublicFileMetadataTransactionData } from './arfs_trx_data_types'; +import { expect } from 'chai'; +import { Tag } from 'arweave/node/lib/transaction'; + +describe('The ArFSDAO class', () => { + const wallet = readJWKFile('./test_wallet.json'); + const fakeArweave = Arweave.init({ + host: 'localhost', + port: 443, + protocol: 'https', + timeout: 600000 + }); + + const arfsDao = new ArFSDAO(wallet, fakeArweave, true, 'ArFSDAO-Test', '1.0'); + + const stubFileMetaDataTrx = new ArFSPublicFileMetaDataPrototype( + new ArFSPublicFileMetadataTransactionData( + 'Test Metadata', + new ByteCount(10), + new UnixTime(123456789), + stubTransactionID, + 'text/plain' + ), + stubEntityID, + stubEntityID, + stubEntityID + ); + + describe('prepareObjectTransaction function', () => { + // Helper function to grab the decoded gql tags off of a Transaction + const getDecodedTagName = (tag: Tag) => tag.get('name', { decode: true, string: true }); + + it('includes the base ArFS tags by default', async () => { + const transaction = await arfsDao.prepareArFSObjectTransaction({ + objectMetaData: stubFileMetaDataTrx, + rewardSettings: { reward: W(10) } + }); + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'ArFS')).to.exist; + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'App-Name')).to.exist; + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'App-Version')).to.exist; + expect(transaction.tags.length).to.equal(9); + }); + + it('includes the boost tag when boosted', async () => { + const transaction = await arfsDao.prepareArFSObjectTransaction({ + objectMetaData: stubFileMetaDataTrx, + rewardSettings: { reward: W(10), feeMultiple: new FeeMultiple(1.5) } + }); + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'Boost')).to.exist; + expect(transaction.tags.length).to.equal(10); + }); + + it('excludes the boost tag when boosted and boost tag is excluded', async () => { + const transaction = await arfsDao.prepareArFSObjectTransaction({ + objectMetaData: stubFileMetaDataTrx, + rewardSettings: { reward: W(10), feeMultiple: new FeeMultiple(1.5) }, + excludedTagNames: ['Boost'] + }); + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'Boost')).to.be.undefined; + expect(transaction.tags.length).to.equal(9); + }); + + it('excludes ArFS tag if its within the exclusion array', async () => { + const transaction = await arfsDao.prepareArFSObjectTransaction({ + objectMetaData: stubFileMetaDataTrx, + rewardSettings: { reward: W(10) }, + excludedTagNames: ['ArFS'] + }); + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'ArFS')).to.be.undefined; + expect(transaction.tags.length).to.equal(8); + }); + + it('can exclude multiple tags if provided within the exclusion array', async () => { + const transaction = await arfsDao.prepareArFSObjectTransaction({ + objectMetaData: stubFileMetaDataTrx, + rewardSettings: { reward: W(10) }, + excludedTagNames: ['ArFS', 'App-Version', 'App-Name'] + }); + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'ArFS')).to.be.undefined; + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'App-Name')).to.be.undefined; + expect(transaction.tags.find((tag) => getDecodedTagName(tag) === 'App-Version')).to.be.undefined; + expect(transaction.tags.length).to.equal(6); + }); + }); +}); diff --git a/src/arfs/arfsdao.ts b/src/arfs/arfsdao.ts index e3748a06..a844277b 100644 --- a/src/arfs/arfsdao.ts +++ b/src/arfs/arfsdao.ts @@ -108,6 +108,13 @@ export class PrivateDriveKeyData { } } +export interface PrepareObjectTransactionParams { + objectMetaData: ArFSObjectMetadataPrototype; + rewardSettings?: RewardSettings; + excludedTagNames?: string[]; + otherTags?: GQLTagInterface[]; +} + export interface ArFSMoveParams { originalMetaData: O; newParentFolderId: FolderID; @@ -208,7 +215,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { // Create a root folder metadata transaction const folderMetadata = folderPrototypeFactory(folderId, parentFolderId); - const folderTrx = await this.prepareArFSObjectTransaction(folderMetadata, rewardSettings); + const folderTrx = await this.prepareArFSObjectTransaction({ objectMetaData: folderMetadata, rewardSettings }); // Execute the upload if (!this.dryRun) { @@ -275,7 +282,10 @@ export class ArFSDAO extends ArFSDAOAnonymous { // Create a drive metadata transaction const driveMetaData = await createMetadataFn(driveId, rootFolderId); - const driveTrx = await this.prepareArFSObjectTransaction(driveMetaData, driveRewardSettings); + const driveTrx = await this.prepareArFSObjectTransaction({ + objectMetaData: driveMetaData, + rewardSettings: driveRewardSettings + }); // Execute the upload if (!this.dryRun) { @@ -369,7 +379,10 @@ export class ArFSDAO extends ArFSDAOAnonymous { const metadataPrototype = metaDataFactory(); // Prepare meta data transaction - const metaDataTrx = await this.prepareArFSObjectTransaction(metadataPrototype, metaDataBaseReward); + const metaDataTrx = await this.prepareArFSObjectTransaction({ + objectMetaData: metadataPrototype, + rewardSettings: metaDataBaseReward + }); // Upload meta data if (!this.dryRun) { @@ -493,7 +506,11 @@ export class ArFSDAO extends ArFSDAOAnonymous { // Build file data transaction const fileDataPrototype = await dataPrototypeFactoryFn(fileData, dataContentType, fileId); - const dataTrx = await this.prepareArFSObjectTransaction(fileDataPrototype, fileDataRewardSettings); + const dataTrx = await this.prepareArFSObjectTransaction({ + objectMetaData: fileDataPrototype, + rewardSettings: fileDataRewardSettings, + excludedTagNames: ['ArFS'] + }); // Upload file data if (!this.dryRun) { @@ -513,7 +530,10 @@ export class ArFSDAO extends ArFSDAOAnonymous { fileId ); const fileMetadata = metadataFactoryFn(metadataTrxData, fileId); - const metaDataTrx = await this.prepareArFSObjectTransaction(fileMetadata, metadataRewardSettings); + const metaDataTrx = await this.prepareArFSObjectTransaction({ + objectMetaData: fileMetadata, + rewardSettings: metadataRewardSettings + }); // Upload meta data if (!this.dryRun) { @@ -612,11 +632,12 @@ export class ArFSDAO extends ArFSDAOAnonymous { ); } - async prepareArFSObjectTransaction( - objectMetaData: ArFSObjectMetadataPrototype, - rewardSettings: RewardSettings = {}, - otherTags: GQLTagInterface[] = [] - ): Promise { + async prepareArFSObjectTransaction({ + objectMetaData, + rewardSettings = {}, + excludedTagNames = [], + otherTags = [] + }: PrepareObjectTransactionParams): Promise { const wallet = this.wallet as JWKWallet; // Create transaction @@ -641,23 +662,31 @@ export class ArFSDAO extends ArFSDAOAnonymous { transaction.reward = rewardSettings.feeMultiple.boostReward(transaction.reward); } - // Add baseline ArFS Tags - transaction.addTag('App-Name', this.appName); - transaction.addTag('App-Version', this.appVersion); - transaction.addTag('ArFS', CURRENT_ARFS_VERSION); + let tagsToAdd: GQLTagInterface[] = [ + // Add baseline App Name and App Version Tags + { name: 'App-Name', value: this.appName }, + { name: 'App-Version', value: this.appVersion }, + { name: 'ArFS', value: CURRENT_ARFS_VERSION } + ]; + if (rewardSettings.feeMultiple?.wouldBoostReward()) { - transaction.addTag('Boost', rewardSettings.feeMultiple.toString()); + tagsToAdd.push({ name: 'Boost', value: rewardSettings.feeMultiple.toString() }); } - // Add object-specific tags - objectMetaData.addTagsToTransaction(transaction); - // Enforce that other tags are not protected objectMetaData.assertProtectedTags(otherTags); - otherTags.forEach((tag) => { + tagsToAdd.push(...otherTags); + + // Remove any excluded tags + tagsToAdd = tagsToAdd.filter((tag) => !excludedTagNames.includes(tag.name)); + + tagsToAdd.forEach((tag) => { transaction.addTag(tag.name, tag.value); }); + // Add object-specific tags + objectMetaData.addTagsToTransaction(transaction); + // Sign the transaction await this.arweave.transactions.sign(transaction, wallet.getPrivateKey()); return transaction;