From 64691101fc9467f140383002ffa08ba4acb19325 Mon Sep 17 00:00:00 2001 From: Philip Mataras Date: Wed, 3 Nov 2021 09:32:34 -0400 Subject: [PATCH 01/30] Creates manifest JSON. --- src/commands/create_manifest.ts | 137 ++++++++++++++++++++++++++++++++ src/commands/index.ts | 1 + 2 files changed, 138 insertions(+) create mode 100644 src/commands/create_manifest.ts diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts new file mode 100644 index 00000000..2e3e9eee --- /dev/null +++ b/src/commands/create_manifest.ts @@ -0,0 +1,137 @@ +import { arDriveFactory, cliArweave, cliWalletDao } from '..'; +import { ArDriveAnonymous } from '../ardrive'; +import { ArFSDAOAnonymous } from '../arfsdao_anonymous'; +import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; +import { CLICommand, ParametersHelper } from '../CLICommand'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { + BoostParameter, + DestinationFileNameParameter, + DriveIdParameter, + DrivePrivacyParameters, + DryRunParameter, + TreeDepthParams +} from '../parameter_declarations'; +import { FeeMultiple } from '../types'; +import { readJWKFile } from '../utils'; +import { alphabeticalOrder } from '../utils/sort_functions'; + +new CLICommand({ + name: 'create-manifest', + parameters: [ + DriveIdParameter, + DestinationFileNameParameter, + BoostParameter, + DryRunParameter, + ...TreeDepthParams, + ...DrivePrivacyParameters + ], + async action(options) { + if (!options.destFileName) { + options.destFileName = 'ArDrive Manifest.json'; + } + const parameters = new ParametersHelper(options, cliWalletDao); + + let rootFolderId: string; + + const wallet = readJWKFile(options.walletFile); + + const arDrive = arDriveFactory({ + wallet: wallet, + feeMultiple: options.boost as FeeMultiple, + dryRun: options.dryRun + }); + + const driveId = parameters.getRequiredParameterValue(DriveIdParameter); + let children: (ArFSPrivateFileOrFolderWithPaths | ArFSPublicFileOrFolderWithPaths)[]; + const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); + + if (await parameters.getIsPrivate()) { + const wallet = await parameters.getRequiredWallet(); + const arDrive = arDriveFactory({ wallet }); + const driveKey = await parameters.getDriveKey({ driveId }); + const drive = await arDrive.getPrivateDrive(driveId, driveKey); + rootFolderId = drive.rootFolderId; + + // We have the drive id from deriving a key, we can derive the owner + const driveOwner = await arDrive.getOwnerForDriveId(driveId); + + children = await arDrive.listPrivateFolder({ + folderId: rootFolderId, + driveKey, + maxDepth, + includeRoot: true, + owner: driveOwner + }); + } else { + const arDrive = new ArDriveAnonymous(new ArFSDAOAnonymous(cliArweave)); + const drive = await arDrive.getPublicDrive(driveId); + rootFolderId = drive.rootFolderId; + children = await arDrive.listPublicFolder({ folderId: rootFolderId, maxDepth, includeRoot: true }); + } + + const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)) as ( + | Partial + | Partial + )[]; + + // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 + sortedChildren.map((fileOrFolderMetaData) => { + if (fileOrFolderMetaData.entityType === 'folder') { + delete fileOrFolderMetaData.lastModifiedDate; + delete fileOrFolderMetaData.size; + delete fileOrFolderMetaData.dataTxId; + delete fileOrFolderMetaData.dataContentType; + } + delete fileOrFolderMetaData.syncStatus; + }); + + // TURN SORTED CHILDREN INTO MANIFEST + // These interfaces taken from arweave-deploy + interface ManifestPathMap { + [index: string]: { id: string }; + } + interface Manifest { + manifest: 'arweave/paths'; + version: '0.1.0'; + index?: { + path: string; + }; + paths: ManifestPathMap; + } + + //const indexPath = noIndex ? null : 'index.html'; + const indexPath = 'index.html'; + const pathMap: ManifestPathMap = {}; + sortedChildren.forEach((child) => { + if (child.dataTxId && child.path) { + pathMap[child.path] = { id: child.dataTxId }; + } + }); + + const arweaveManifest: Manifest = { + manifest: 'arweave/paths', + version: '0.1.0', + index: { + path: indexPath + }, + paths: pathMap + }; + + // Display data + console.log(JSON.stringify(arweaveManifest)); + console.log(JSON.stringify(sortedChildren, null, 4)); + + const result = await (async () => { + if (await parameters.getIsPrivate()) { + const driveKey = await parameters.getDriveKey({ driveId }); + return arDrive.uploadPrivateFile(rootFolderId, manifestEntity, driveKey, options.destFileName); + } else { + return arDrive.uploadPublicFile(rootFolderId, manifestEntity, options.destFileName); + } + })(); + console.log(JSON.stringify(result, null, 4)); + + return SUCCESS_EXIT_CODE; + } +}); diff --git a/src/commands/index.ts b/src/commands/index.ts index cb7cf7c0..3f470782 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -20,5 +20,6 @@ import './move_file'; import './move_folder'; import './get_drive_key'; import './get_file_key'; +import './create_manifest'; CLICommand.parse(); From f319a02973798db1921a36413651af344ad60925 Mon Sep 17 00:00:00 2001 From: Philip Mataras Date: Wed, 3 Nov 2021 11:22:03 -0400 Subject: [PATCH 02/30] Created uploadPublicManifest method --- src/ardrive.ts | 68 ++++++++++++++++++++++++++++++++- src/commands/create_manifest.ts | 15 +------- src/types.ts | 12 ++++++ 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index a18cfe72..522cc057 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -12,7 +12,8 @@ import { EntityID, FileID, ByteCount, - MakeOptional + MakeOptional, + Manifest } from './types'; import { WalletDAO, Wallet, JWKWallet } from './wallet'; import { ARDataPriceRegressionEstimator } from './utils/ar_data_price_regression_estimator'; @@ -545,6 +546,71 @@ export class ArDrive extends ArDriveAnonymous { }); } + async uploadPublicManifest( + parentFolderId: FolderID, + arweaveManifest: Manifest, + destinationFileName?: string + ): Promise { + const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); + + const owner = await this.getOwnerForDriveId(driveId); + await this.assertOwnerAddress(owner); + + // Derive destination name and names already within provided destination folder + const destFileName = destinationFileName ?? 'DriveManifest.json'; + const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(parentFolderId); + + // Files cannot overwrite folder names + if (filesAndFolderNames.folders.find((f) => f.folderName === destFileName)) { + // TODO: Add optional interactive prompt to resolve name conflicts in ticket PE-599 + throw new Error(errorMessage.entityNameExists); + } + + // File is a new revision if destination name conflicts + // with an existing file in the destination folder + const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destFileName)?.fileId; + + const size = new TextEncoder().encode(JSON.stringify(arweaveManifest)).length; + const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload( + size, + this.stubPublicFileMetadata(wrappedFile, destinationFileName), + 'public' + ); + const fileDataRewardSettings = { reward: uploadBaseCosts.fileDataBaseReward, feeMultiple: this.feeMultiple }; + const metadataRewardSettings = { reward: uploadBaseCosts.metaDataBaseReward, feeMultiple: this.feeMultiple }; + + const uploadFileResult = await this.arFsDao.uploadPublicFile({ + parentFolderId, + wrappedFile, + driveId, + fileDataRewardSettings, + metadataRewardSettings, + destFileName: destinationFileName, + existingFileId + }); + + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( + uploadBaseCosts.communityWinstonTip + ); + + return Promise.resolve({ + created: [ + { + type: 'file', + metadataTxId: uploadFileResult.metaDataTrxId, + dataTxId: uploadFileResult.dataTrxId, + entityId: uploadFileResult.fileId + } + ], + tips: [tipData], + fees: { + [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, + [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, + [tipData.txId]: +communityTipTrxReward + } + }); + } + public async createPublicFolderAndUploadChildren( parentFolderId: FolderID, wrappedFolder: ArFSFolderToUpload, diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index 2e3e9eee..a3f6824c 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -12,7 +12,7 @@ import { DryRunParameter, TreeDepthParams } from '../parameter_declarations'; -import { FeeMultiple } from '../types'; +import { FeeMultiple, Manifest, ManifestPathMap } from '../types'; import { readJWKFile } from '../utils'; import { alphabeticalOrder } from '../utils/sort_functions'; @@ -88,17 +88,6 @@ new CLICommand({ // TURN SORTED CHILDREN INTO MANIFEST // These interfaces taken from arweave-deploy - interface ManifestPathMap { - [index: string]: { id: string }; - } - interface Manifest { - manifest: 'arweave/paths'; - version: '0.1.0'; - index?: { - path: string; - }; - paths: ManifestPathMap; - } //const indexPath = noIndex ? null : 'index.html'; const indexPath = 'index.html'; @@ -127,7 +116,7 @@ new CLICommand({ const driveKey = await parameters.getDriveKey({ driveId }); return arDrive.uploadPrivateFile(rootFolderId, manifestEntity, driveKey, options.destFileName); } else { - return arDrive.uploadPublicFile(rootFolderId, manifestEntity, options.destFileName); + return arDrive.uploadPublicManifest(rootFolderId, arweaveManifest, options.destFileName); } })(); console.log(JSON.stringify(result, null, 4)); diff --git a/src/types.ts b/src/types.ts index 9a89f01e..20dc1476 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,3 +43,15 @@ export type RewardSettings = { type Omit = Pick>; export type MakeOptional = Omit & Partial; + +export interface ManifestPathMap { + [index: string]: { id: string }; +} +export interface Manifest { + manifest: 'arweave/paths'; + version: '0.1.0'; + index?: { + path: string; + }; + paths: ManifestPathMap; +} From 4f5efe3ecb492b8880b79d4997e756465afa35dc Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 3 Nov 2021 12:36:58 -0500 Subject: [PATCH 03/30] feat(manifest): Add manifest to upload class PE-477 --- src/ardrive.ts | 37 ++++++++++++++------------------- src/arfs_file_wrapper.ts | 33 +++++++++++++++++++++++++++-- src/arfsdao.ts | 6 +++--- src/commands/create_manifest.ts | 24 ++++++++++++--------- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index 522cc057..d0510416 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -12,12 +12,11 @@ import { EntityID, FileID, ByteCount, - MakeOptional, - Manifest + MakeOptional } from './types'; import { WalletDAO, Wallet, JWKWallet } from './wallet'; import { ARDataPriceRegressionEstimator } from './utils/ar_data_price_regression_estimator'; -import { ArFSFolderToUpload, ArFSFileToUpload } from './arfs_file_wrapper'; +import { ArFSFolderToUpload, ArFSFileToUpload, ArFSManifestToUpload, ArFSEntityToUpload } from './arfs_file_wrapper'; import { ARDataPriceEstimator } from './utils/ar_data_price_estimator'; import { ArFSDriveTransactionData, @@ -548,32 +547,28 @@ export class ArDrive extends ArDriveAnonymous { async uploadPublicManifest( parentFolderId: FolderID, - arweaveManifest: Manifest, - destinationFileName?: string + arweaveManifest: ArFSManifestToUpload, + destManifestName?: string ): Promise { const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); const owner = await this.getOwnerForDriveId(driveId); await this.assertOwnerAddress(owner); - // Derive destination name and names already within provided destination folder - const destFileName = destinationFileName ?? 'DriveManifest.json'; - const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(parentFolderId); + const destFileName = destManifestName ?? 'DriveManifest.json'; - // Files cannot overwrite folder names - if (filesAndFolderNames.folders.find((f) => f.folderName === destFileName)) { - // TODO: Add optional interactive prompt to resolve name conflicts in ticket PE-599 - throw new Error(errorMessage.entityNameExists); - } + // TODO: Handle collision with existing manifest. New manifest will always be a new file, with + // upsert by default this means it will only skip here on --skip conflict - // File is a new revision if destination name conflicts - // with an existing file in the destination folder + const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(parentFolderId); + + // Manifest becomes a new revision if the destination name + // conflicts with an existing file in the destination folder const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destFileName)?.fileId; - const size = new TextEncoder().encode(JSON.stringify(arweaveManifest)).length; const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload( - size, - this.stubPublicFileMetadata(wrappedFile, destinationFileName), + arweaveManifest.size, + this.stubPublicFileMetadata(arweaveManifest, destFileName), 'public' ); const fileDataRewardSettings = { reward: uploadBaseCosts.fileDataBaseReward, feeMultiple: this.feeMultiple }; @@ -581,11 +576,11 @@ export class ArDrive extends ArDriveAnonymous { const uploadFileResult = await this.arFsDao.uploadPublicFile({ parentFolderId, - wrappedFile, + wrappedFile: arweaveManifest, driveId, fileDataRewardSettings, metadataRewardSettings, - destFileName: destinationFileName, + destFileName, existingFileId }); @@ -1595,7 +1590,7 @@ export class ArDrive extends ArDriveAnonymous { // Provides for stubbing metadata during cost estimations since the data trx ID won't yet be known private stubPublicFileMetadata( - wrappedFile: ArFSFileToUpload, + wrappedFile: ArFSEntityToUpload, destinationFileName?: string ): ArFSPublicFileMetadataTransactionData { const { fileSize, dataContentType, lastModifiedDateMS } = wrappedFile.gatherFileInfo(); diff --git a/src/arfs_file_wrapper.ts b/src/arfs_file_wrapper.ts index 893a406d..327179c8 100644 --- a/src/arfs_file_wrapper.ts +++ b/src/arfs_file_wrapper.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { extToMime } from 'ardrive-core-js'; import { basename, join } from 'path'; -import { ByteCount, DataContentType, FileID, FolderID, UnixTime } from './types'; +import { ByteCount, DataContentType, FileID, FolderID, Manifest, UnixTime } from './types'; import { BulkFileBaseCosts, MetaDataBaseCosts } from './ardrive'; type BaseFileName = string; @@ -52,7 +52,36 @@ export function isFolder(fileOrFolder: ArFSFileToUpload | ArFSFolderToUpload): f return fileOrFolder instanceof ArFSFolderToUpload; } -export class ArFSFileToUpload { +export interface ArFSEntityToUpload { + gatherFileInfo: () => FileInfo; + getFileDataBuffer: () => Buffer; + getBaseFileName: () => BaseFileName; +} + +export class ArFSManifestToUpload implements ArFSEntityToUpload { + constructor(public readonly manifest: Manifest) {} + + public gatherFileInfo(): FileInfo { + const dataContentType = 'application/json'; + const lastModifiedDateMS = Math.round(Date.now() / 1000); // new unix time + + return { dataContentType, lastModifiedDateMS, fileSize: this.size }; + } + + public getBaseFileName(): BaseFileName { + return 'DriveManifest.json'; + } + + public getFileDataBuffer(): Buffer { + return Buffer.from(JSON.stringify(this.manifest)); + } + + public get size(): ByteCount { + return Buffer.byteLength(JSON.stringify(this.manifest)); + } +} + +export class ArFSFileToUpload implements ArFSEntityToUpload { constructor(public readonly filePath: FilePath, public readonly fileStats: fs.Stats) { if (this.fileStats.size >= maxFileSize) { throw new Error(`Files greater than "${maxFileSize}" bytes are not yet supported!`); diff --git a/src/arfsdao.ts b/src/arfsdao.ts index c02f8935..8741c992 100644 --- a/src/arfsdao.ts +++ b/src/arfsdao.ts @@ -28,7 +28,7 @@ import { ArFSPublicFolderTransactionData } from './arfs_trx_data_types'; import { ASCENDING_ORDER, buildQuery } from './query'; -import { ArFSFileToUpload } from './arfs_file_wrapper'; +import { ArFSEntityToUpload } from './arfs_file_wrapper'; import { DriveID, FolderID, @@ -117,7 +117,7 @@ export type ArFSListPrivateFolderParams = Required; export interface UploadPublicFileParams { parentFolderId: FolderID; - wrappedFile: ArFSFileToUpload; + wrappedFile: ArFSEntityToUpload; driveId: DriveID; fileDataRewardSettings: RewardSettings; metadataRewardSettings: RewardSettings; @@ -463,7 +463,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { } async uploadFile( - wrappedFile: ArFSFileToUpload, + wrappedFile: ArFSEntityToUpload, fileDataRewardSettings: RewardSettings, metadataRewardSettings: RewardSettings, dataPrototypeFactoryFn: FileDataPrototypeFactory, diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index a3f6824c..a337bcfc 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -2,6 +2,7 @@ import { arDriveFactory, cliArweave, cliWalletDao } from '..'; import { ArDriveAnonymous } from '../ardrive'; import { ArFSDAOAnonymous } from '../arfsdao_anonymous'; import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; +import { ArFSManifestToUpload } from '../arfs_file_wrapper'; import { CLICommand, ParametersHelper } from '../CLICommand'; import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; import { @@ -13,7 +14,6 @@ import { TreeDepthParams } from '../parameter_declarations'; import { FeeMultiple, Manifest, ManifestPathMap } from '../types'; -import { readJWKFile } from '../utils'; import { alphabeticalOrder } from '../utils/sort_functions'; new CLICommand({ @@ -34,7 +34,7 @@ new CLICommand({ let rootFolderId: string; - const wallet = readJWKFile(options.walletFile); + const wallet = await parameters.getRequiredWallet(); const arDrive = arDriveFactory({ wallet: wallet, @@ -89,7 +89,6 @@ new CLICommand({ // TURN SORTED CHILDREN INTO MANIFEST // These interfaces taken from arweave-deploy - //const indexPath = noIndex ? null : 'index.html'; const indexPath = 'index.html'; const pathMap: ManifestPathMap = {}; sortedChildren.forEach((child) => { @@ -107,19 +106,24 @@ new CLICommand({ paths: pathMap }; - // Display data - console.log(JSON.stringify(arweaveManifest)); - console.log(JSON.stringify(sortedChildren, null, 4)); + // Display manifest + // console.log(JSON.stringify(arweaveManifest, null, 4)); + // console.log(JSON.stringify(sortedChildren, null, 4)); const result = await (async () => { if (await parameters.getIsPrivate()) { - const driveKey = await parameters.getDriveKey({ driveId }); - return arDrive.uploadPrivateFile(rootFolderId, manifestEntity, driveKey, options.destFileName); + // const driveKey = await parameters.getDriveKey({ driveId }); + // return arDrive.uploadPrivateFile(rootFolderId, manifestEntity, driveKey, options.destFileName); + throw new Error('implement me'); } else { - return arDrive.uploadPublicManifest(rootFolderId, arweaveManifest, options.destFileName); + return arDrive.uploadPublicManifest( + rootFolderId, + new ArFSManifestToUpload(arweaveManifest), + options.destFileName + ); } })(); - console.log(JSON.stringify(result, null, 4)); + console.log(JSON.stringify({ manifest: arweaveManifest, ...result }, null, 4)); return SUCCESS_EXIT_CODE; } From 657a1f23c8cf336d5a1e64bd6a30e5eceb8c194f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 3 Nov 2021 17:03:31 -0500 Subject: [PATCH 04/30] refactor(manifest): Point to index.html in root folder if it exists PE-477 --- src/arfs_file_wrapper.ts | 4 +- src/commands/create_manifest.ts | 88 ++++++++++++--------------------- src/types.ts | 2 + 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/src/arfs_file_wrapper.ts b/src/arfs_file_wrapper.ts index 327179c8..9b28d248 100644 --- a/src/arfs_file_wrapper.ts +++ b/src/arfs_file_wrapper.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { extToMime } from 'ardrive-core-js'; import { basename, join } from 'path'; -import { ByteCount, DataContentType, FileID, FolderID, Manifest, UnixTime } from './types'; +import { ByteCount, DataContentType, FileID, FolderID, Manifest, MANIFEST_CONTENT_TYPE, UnixTime } from './types'; import { BulkFileBaseCosts, MetaDataBaseCosts } from './ardrive'; type BaseFileName = string; @@ -62,7 +62,7 @@ export class ArFSManifestToUpload implements ArFSEntityToUpload { constructor(public readonly manifest: Manifest) {} public gatherFileInfo(): FileInfo { - const dataContentType = 'application/json'; + const dataContentType = MANIFEST_CONTENT_TYPE; const lastModifiedDateMS = Math.round(Date.now() / 1000); // new unix time return { dataContentType, lastModifiedDateMS, fileSize: this.size }; diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index a337bcfc..13ba409d 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -1,6 +1,4 @@ -import { arDriveFactory, cliArweave, cliWalletDao } from '..'; -import { ArDriveAnonymous } from '../ardrive'; -import { ArFSDAOAnonymous } from '../arfsdao_anonymous'; +import { arDriveFactory, cliWalletDao } from '..'; import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; import { ArFSManifestToUpload } from '../arfs_file_wrapper'; import { CLICommand, ParametersHelper } from '../CLICommand'; @@ -9,11 +7,12 @@ import { BoostParameter, DestinationFileNameParameter, DriveIdParameter, - DrivePrivacyParameters, DryRunParameter, - TreeDepthParams + SeedPhraseParameter, + TreeDepthParams, + WalletFileParameter } from '../parameter_declarations'; -import { FeeMultiple, Manifest, ManifestPathMap } from '../types'; +import { FeeMultiple, Manifest, ManifestPathMap, MANIFEST_CONTENT_TYPE } from '../types'; import { alphabeticalOrder } from '../utils/sort_functions'; new CLICommand({ @@ -23,8 +22,9 @@ new CLICommand({ DestinationFileNameParameter, BoostParameter, DryRunParameter, - ...TreeDepthParams, - ...DrivePrivacyParameters + WalletFileParameter, + SeedPhraseParameter, + ...TreeDepthParams ], async action(options) { if (!options.destFileName) { @@ -32,8 +32,6 @@ new CLICommand({ } const parameters = new ParametersHelper(options, cliWalletDao); - let rootFolderId: string; - const wallet = await parameters.getRequiredWallet(); const arDrive = arDriveFactory({ @@ -43,32 +41,13 @@ new CLICommand({ }); const driveId = parameters.getRequiredParameterValue(DriveIdParameter); - let children: (ArFSPrivateFileOrFolderWithPaths | ArFSPublicFileOrFolderWithPaths)[]; const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); - if (await parameters.getIsPrivate()) { - const wallet = await parameters.getRequiredWallet(); - const arDrive = arDriveFactory({ wallet }); - const driveKey = await parameters.getDriveKey({ driveId }); - const drive = await arDrive.getPrivateDrive(driveId, driveKey); - rootFolderId = drive.rootFolderId; - - // We have the drive id from deriving a key, we can derive the owner - const driveOwner = await arDrive.getOwnerForDriveId(driveId); - - children = await arDrive.listPrivateFolder({ - folderId: rootFolderId, - driveKey, - maxDepth, - includeRoot: true, - owner: driveOwner - }); - } else { - const arDrive = new ArDriveAnonymous(new ArFSDAOAnonymous(cliArweave)); - const drive = await arDrive.getPublicDrive(driveId); - rootFolderId = drive.rootFolderId; - children = await arDrive.listPublicFolder({ folderId: rootFolderId, maxDepth, includeRoot: true }); - } + const drive = await arDrive.getPublicDrive(driveId); + const rootFolderId = drive.rootFolderId; + const driveName = drive.name; + + const children = await arDrive.listPublicFolder({ folderId: rootFolderId, maxDepth, includeRoot: true }); const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)) as ( | Partial @@ -87,16 +66,19 @@ new CLICommand({ }); // TURN SORTED CHILDREN INTO MANIFEST - // These interfaces taken from arweave-deploy - const indexPath = 'index.html'; const pathMap: ManifestPathMap = {}; sortedChildren.forEach((child) => { - if (child.dataTxId && child.path) { - pathMap[child.path] = { id: child.dataTxId }; + if (child.dataTxId && child.path && child.dataContentType !== MANIFEST_CONTENT_TYPE) { + pathMap[child.path.slice(1)] = { id: child.dataTxId }; } }); + // Use index.html in root folder if it exists, otherwise show first file found + const indexPath = Object.keys(pathMap).includes(`${driveName}/index.html`) + ? `${driveName}/index.html` + : Object.keys(pathMap)[0]; + const arweaveManifest: Manifest = { manifest: 'arweave/paths', version: '0.1.0', @@ -106,24 +88,18 @@ new CLICommand({ paths: pathMap }; - // Display manifest - // console.log(JSON.stringify(arweaveManifest, null, 4)); - // console.log(JSON.stringify(sortedChildren, null, 4)); - - const result = await (async () => { - if (await parameters.getIsPrivate()) { - // const driveKey = await parameters.getDriveKey({ driveId }); - // return arDrive.uploadPrivateFile(rootFolderId, manifestEntity, driveKey, options.destFileName); - throw new Error('implement me'); - } else { - return arDrive.uploadPublicManifest( - rootFolderId, - new ArFSManifestToUpload(arweaveManifest), - options.destFileName - ); - } - })(); - console.log(JSON.stringify({ manifest: arweaveManifest, ...result }, null, 4)); + // TODO: Private manifests 🤔 + const result = await arDrive.uploadPublicManifest( + rootFolderId, + new ArFSManifestToUpload(arweaveManifest), + options.destFileName + ); + + const allLinks = Object.keys(arweaveManifest.paths).map( + (path) => `arweave.net/${result.created[0].dataTxId}/${path}` + ); + + console.log(JSON.stringify({ manifest: arweaveManifest, ...result, links: allLinks }, null, 4)); return SUCCESS_EXIT_CODE; } diff --git a/src/types.ts b/src/types.ts index df242008..cf8e06ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export const DEFAULT_APP_VERSION = '1.0.0'; export const JSON_CONTENT_TYPE = 'application/json'; export const PRIVATE_CONTENT_TYPE = 'application/octet-stream'; +export const MANIFEST_CONTENT_TYPE = 'application/x.arweave-manifest+json'; export type PublicKey = string; export type SeedPhrase = string; @@ -47,6 +48,7 @@ export type RewardSettings = { type Omit = Pick>; export type MakeOptional = Omit & Partial; +// These interfaces taken from arweave-deploy export interface ManifestPathMap { [index: string]: { id: string }; } From 187bbd021c4c27a160110d9b780d81ddb6e22961 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 4 Nov 2021 11:20:44 -0500 Subject: [PATCH 05/30] refactor(manifest): Move manifest logic out of CLI layer PE-477 --- src/ardrive.ts | 101 ++++++++++++++++++++++++++++---- src/commands/create_manifest.ts | 88 ++++++++-------------------- src/commands/folder_info.ts | 2 +- src/commands/move_folder.ts | 4 +- src/parameter_declarations.ts | 13 ++-- 5 files changed, 125 insertions(+), 83 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index d0510416..124bddd2 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -12,7 +12,9 @@ import { EntityID, FileID, ByteCount, - MakeOptional + MakeOptional, + ManifestPathMap, + MANIFEST_CONTENT_TYPE } from './types'; import { WalletDAO, Wallet, JWKWallet } from './wallet'; import { ARDataPriceRegressionEstimator } from './utils/ar_data_price_regression_estimator'; @@ -48,6 +50,7 @@ import { PrivateKeyData } from './private_key_data'; import { EntityNamesAndIds } from './utils/mapper_functions'; import { ArweaveAddress } from './arweave_address'; import { WithDriveKey } from './arfs_entity_result_factory'; +import { alphabeticalOrder } from './utils/sort_functions'; export type ArFSEntityDataType = 'drive' | 'folder' | 'file'; @@ -81,6 +84,10 @@ export interface ArFSResult { fees: ArFSFees; } +export interface ArFSManifestResult extends ArFSResult { + links: string[]; +} + export interface MetaDataBaseCosts { metaDataBaseReward: Winston; } @@ -97,6 +104,13 @@ export interface DriveUploadBaseCosts { rootFolderMetaDataBaseReward: Winston; } +export interface UploadPublicManifestParams { + driveId?: DriveID; + parentFolderId?: FolderID; + maxDepth?: number; + destManifestName?: string; +} + interface RecursivePublicBulkUploadParams { parentFolderId: FolderID; wrappedFolder: ArFSFolderToUpload; @@ -545,30 +559,86 @@ export class ArDrive extends ArDriveAnonymous { }); } - async uploadPublicManifest( - parentFolderId: FolderID, - arweaveManifest: ArFSManifestToUpload, - destManifestName?: string - ): Promise { - const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); + async uploadPublicManifest({ + parentFolderId, + driveId, + destManifestName, + maxDepth = Number.MAX_SAFE_INTEGER + }: UploadPublicManifestParams): Promise { + if (!driveId) { + if (!parentFolderId) { + throw new Error('Must provide either a drive ID or a folder ID to!'); + } + + driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); + } const owner = await this.getOwnerForDriveId(driveId); await this.assertOwnerAddress(owner); - const destFileName = destManifestName ?? 'DriveManifest.json'; + const drive = await this.arFsDao.getPublicDrive(driveId, owner); + + const driveName = drive.name; + + parentFolderId ??= drive.rootFolderId; + destManifestName ??= 'DriveManifest.json'; // TODO: Handle collision with existing manifest. New manifest will always be a new file, with // upsert by default this means it will only skip here on --skip conflict - const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(parentFolderId); // Manifest becomes a new revision if the destination name // conflicts with an existing file in the destination folder - const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destFileName)?.fileId; + const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destManifestName)?.fileId; + + const children = await this.arFsDao.listPublicFolder({ + folderId: parentFolderId, + maxDepth, + includeRoot: true, + owner + }); + + const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)) as ( + | Partial + | Partial + )[]; + + // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 + sortedChildren.map((fileOrFolderMetaData) => { + if (fileOrFolderMetaData.entityType === 'folder') { + delete fileOrFolderMetaData.lastModifiedDate; + delete fileOrFolderMetaData.size; + delete fileOrFolderMetaData.dataTxId; + delete fileOrFolderMetaData.dataContentType; + } + delete fileOrFolderMetaData.syncStatus; + }); + + // TURN SORTED CHILDREN INTO MANIFEST + const pathMap: ManifestPathMap = {}; + sortedChildren.forEach((child) => { + if (child.dataTxId && child.path && child.dataContentType !== MANIFEST_CONTENT_TYPE) { + pathMap[child.path.slice(1)] = { id: child.dataTxId }; + } + }); + + // Use index.html in root folder if it exists, otherwise show first file found + const indexPath = Object.keys(pathMap).includes(`${driveName}/index.html`) + ? `${driveName}/index.html` + : Object.keys(pathMap)[0]; + + const arweaveManifest = new ArFSManifestToUpload({ + manifest: 'arweave/paths', + version: '0.1.0', + index: { + path: indexPath + }, + paths: pathMap + }); const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload( arweaveManifest.size, - this.stubPublicFileMetadata(arweaveManifest, destFileName), + this.stubPublicFileMetadata(arweaveManifest, destManifestName), 'public' ); const fileDataRewardSettings = { reward: uploadBaseCosts.fileDataBaseReward, feeMultiple: this.feeMultiple }; @@ -580,7 +650,7 @@ export class ArDrive extends ArDriveAnonymous { driveId, fileDataRewardSettings, metadataRewardSettings, - destFileName, + destFileName: destManifestName, existingFileId }); @@ -588,6 +658,10 @@ export class ArDrive extends ArDriveAnonymous { uploadBaseCosts.communityWinstonTip ); + const allLinks = Object.keys(arweaveManifest.manifest.paths).map( + (path) => `arweave.net/${uploadFileResult.dataTrxId}/${path}` + ); + return Promise.resolve({ created: [ { @@ -602,7 +676,8 @@ export class ArDrive extends ArDriveAnonymous { [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, [tipData.txId]: +communityTipTrxReward - } + }, + links: [`arweave.net/${uploadFileResult.dataTrxId}`, ...allLinks] }); } diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index 13ba409d..7db32560 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -1,25 +1,24 @@ import { arDriveFactory, cliWalletDao } from '..'; -import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; -import { ArFSManifestToUpload } from '../arfs_file_wrapper'; import { CLICommand, ParametersHelper } from '../CLICommand'; import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; import { BoostParameter, - DestinationFileNameParameter, + FolderIdParameter, DriveIdParameter, DryRunParameter, SeedPhraseParameter, TreeDepthParams, - WalletFileParameter + WalletFileParameter, + DestinationManifestNameParameter } from '../parameter_declarations'; -import { FeeMultiple, Manifest, ManifestPathMap, MANIFEST_CONTENT_TYPE } from '../types'; -import { alphabeticalOrder } from '../utils/sort_functions'; +import { FeeMultiple } from '../types'; new CLICommand({ name: 'create-manifest', parameters: [ DriveIdParameter, - DestinationFileNameParameter, + FolderIdParameter, + DestinationManifestNameParameter, BoostParameter, DryRunParameter, WalletFileParameter, @@ -27,9 +26,6 @@ new CLICommand({ ...TreeDepthParams ], async action(options) { - if (!options.destFileName) { - options.destFileName = 'ArDrive Manifest.json'; - } const parameters = new ParametersHelper(options, cliWalletDao); const wallet = await parameters.getRequiredWallet(); @@ -40,66 +36,30 @@ new CLICommand({ dryRun: options.dryRun }); - const driveId = parameters.getRequiredParameterValue(DriveIdParameter); - const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); - - const drive = await arDrive.getPublicDrive(driveId); - const rootFolderId = drive.rootFolderId; - const driveName = drive.name; - - const children = await arDrive.listPublicFolder({ folderId: rootFolderId, maxDepth, includeRoot: true }); - - const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)) as ( - | Partial - | Partial - )[]; - - // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 - sortedChildren.map((fileOrFolderMetaData) => { - if (fileOrFolderMetaData.entityType === 'folder') { - delete fileOrFolderMetaData.lastModifiedDate; - delete fileOrFolderMetaData.size; - delete fileOrFolderMetaData.dataTxId; - delete fileOrFolderMetaData.dataContentType; - } - delete fileOrFolderMetaData.syncStatus; - }); - - // TURN SORTED CHILDREN INTO MANIFEST + // User can specify either a drive ID or a folder ID + const driveId = parameters.getParameterValue(DriveIdParameter); + const folderId = parameters.getParameterValue(FolderIdParameter); - const pathMap: ManifestPathMap = {}; - sortedChildren.forEach((child) => { - if (child.dataTxId && child.path && child.dataContentType !== MANIFEST_CONTENT_TYPE) { - pathMap[child.path.slice(1)] = { id: child.dataTxId }; - } - }); + if (driveId && folderId) { + throw new Error('Drive ID cannot be used in conjunction with folder ID!'); + } - // Use index.html in root folder if it exists, otherwise show first file found - const indexPath = Object.keys(pathMap).includes(`${driveName}/index.html`) - ? `${driveName}/index.html` - : Object.keys(pathMap)[0]; + if (!driveId && !folderId) { + throw new Error('Must provide either a drive ID or a folder ID to!'); + } - const arweaveManifest: Manifest = { - manifest: 'arweave/paths', - version: '0.1.0', - index: { - path: indexPath - }, - paths: pathMap - }; + const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); + const destManifestName = parameters.getParameterValue(DestinationManifestNameParameter); // TODO: Private manifests 🤔 - const result = await arDrive.uploadPublicManifest( - rootFolderId, - new ArFSManifestToUpload(arweaveManifest), - options.destFileName - ); - - const allLinks = Object.keys(arweaveManifest.paths).map( - (path) => `arweave.net/${result.created[0].dataTxId}/${path}` - ); + const result = await arDrive.uploadPublicManifest({ + driveId, + parentFolderId: folderId, + maxDepth, + destManifestName + }); - console.log(JSON.stringify({ manifest: arweaveManifest, ...result, links: allLinks }, null, 4)); + console.log(JSON.stringify(result, null, 4)); return SUCCESS_EXIT_CODE; } diff --git a/src/commands/folder_info.ts b/src/commands/folder_info.ts index 759646da..fd98bb3f 100644 --- a/src/commands/folder_info.ts +++ b/src/commands/folder_info.ts @@ -13,7 +13,7 @@ new CLICommand({ // const shouldGetAllRevisions: boolean = options.getAllRevisions; const result: Partial = await (async function () { - const folderId: FolderID = options.folderId; + const folderId: FolderID = parameters.getRequiredParameterValue(FolderIdParameter); if (await parameters.getIsPrivate()) { const wallet = await parameters.getRequiredWallet(); diff --git a/src/commands/move_folder.ts b/src/commands/move_folder.ts index e7190834..d01e4999 100644 --- a/src/commands/move_folder.ts +++ b/src/commands/move_folder.ts @@ -23,7 +23,9 @@ new CLICommand({ async action(options) { const parameters = new ParametersHelper(options); - const { folderId, parentFolderId: newParentFolderId, boost, dryRun } = options; + const { parentFolderId: newParentFolderId, boost, dryRun } = options; + + const folderId = parameters.getRequiredParameterValue(FolderIdParameter); const wallet: Wallet = await parameters.getRequiredWallet(); const ardrive = arDriveFactory({ diff --git a/src/parameter_declarations.ts b/src/parameter_declarations.ts index 971ca69b..e4e230ce 100644 --- a/src/parameter_declarations.ts +++ b/src/parameter_declarations.ts @@ -18,6 +18,7 @@ export const FileIdParameter = 'fileId'; export const ParentFolderIdParameter = 'parentFolderId'; export const LocalFilePathParameter = 'localFilePath'; export const DestinationFileNameParameter = 'destFileName'; +export const DestinationManifestNameParameter = 'destManifestName'; export const LocalFilesParameter = 'localFiles'; export const GetAllRevisionsParameter = 'getAllRevisions'; export const AllParameter = 'all'; @@ -113,8 +114,7 @@ Parameter.declare({ Parameter.declare({ name: DriveIdParameter, aliases: ['-d', '--drive-id'], - description: 'the ArFS entity ID associated with the target drive', - required: true + description: 'the ArFS entity ID associated with the target drive' }); Parameter.declare({ @@ -156,8 +156,7 @@ Parameter.declare({ Parameter.declare({ name: FolderIdParameter, aliases: ['-f', '--folder-id'], - description: `the ArFS folder ID for the folder to query`, - required: true + description: `the ArFS folder ID for the folder to query` }); Parameter.declare({ @@ -179,6 +178,12 @@ Parameter.declare({ description: `(OPTIONAL) a destination file name to use when uploaded to ArDrive` }); +Parameter.declare({ + name: DestinationManifestNameParameter, + aliases: ['-n', '--dest-manifest-name'], + description: `(OPTIONAL) a destination file name for the manifest to use when uploaded to ArDrive` +}); + Parameter.declare({ name: LocalFilesParameter, aliases: ['--local-files'], From 7b1db923e12aa94d5f1c622da70aa4e4aa339d39 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 4 Nov 2021 12:38:54 -0500 Subject: [PATCH 06/30] refactor(manifest): Replace spaces with underscores for link sharing PE-477 --- src/ardrive.ts | 31 ++++++++++++++++++------------- src/commands/create_manifest.ts | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index 124bddd2..fa6fe7a3 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -106,7 +106,7 @@ export interface DriveUploadBaseCosts { export interface UploadPublicManifestParams { driveId?: DriveID; - parentFolderId?: FolderID; + folderId?: FolderID; maxDepth?: number; destManifestName?: string; } @@ -560,17 +560,17 @@ export class ArDrive extends ArDriveAnonymous { } async uploadPublicManifest({ - parentFolderId, + folderId, driveId, - destManifestName, + destManifestName = 'DriveManifest.json', maxDepth = Number.MAX_SAFE_INTEGER }: UploadPublicManifestParams): Promise { if (!driveId) { - if (!parentFolderId) { + if (!folderId) { throw new Error('Must provide either a drive ID or a folder ID to!'); } - driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); + driveId = await this.arFsDao.getDriveIdForFolderId(folderId); } const owner = await this.getOwnerForDriveId(driveId); @@ -580,19 +580,18 @@ export class ArDrive extends ArDriveAnonymous { const driveName = drive.name; - parentFolderId ??= drive.rootFolderId; - destManifestName ??= 'DriveManifest.json'; + folderId ??= drive.rootFolderId; // TODO: Handle collision with existing manifest. New manifest will always be a new file, with // upsert by default this means it will only skip here on --skip conflict - const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(parentFolderId); + const filesAndFolderNames = await this.arFsDao.getPublicEntityNamesAndIdsInFolder(folderId); // Manifest becomes a new revision if the destination name // conflicts with an existing file in the destination folder const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destManifestName)?.fileId; const children = await this.arFsDao.listPublicFolder({ - folderId: parentFolderId, + folderId, maxDepth, includeRoot: true, owner @@ -618,7 +617,13 @@ export class ArDrive extends ArDriveAnonymous { const pathMap: ManifestPathMap = {}; sortedChildren.forEach((child) => { if (child.dataTxId && child.path && child.dataContentType !== MANIFEST_CONTENT_TYPE) { - pathMap[child.path.slice(1)] = { id: child.dataTxId }; + const path = child.path + // Slice off the leading "/" so manifest URLs path correctly + .slice(1) + // Replace spaces with underscores for sharing links + .replace(/ /g, '_'); + + pathMap[path] = { id: child.dataTxId }; } }); @@ -645,7 +650,7 @@ export class ArDrive extends ArDriveAnonymous { const metadataRewardSettings = { reward: uploadBaseCosts.metaDataBaseReward, feeMultiple: this.feeMultiple }; const uploadFileResult = await this.arFsDao.uploadPublicFile({ - parentFolderId, + parentFolderId: folderId, wrappedFile: arweaveManifest, driveId, fileDataRewardSettings, @@ -658,7 +663,7 @@ export class ArDrive extends ArDriveAnonymous { uploadBaseCosts.communityWinstonTip ); - const allLinks = Object.keys(arweaveManifest.manifest.paths).map( + const fileLinks = Object.keys(arweaveManifest.manifest.paths).map( (path) => `arweave.net/${uploadFileResult.dataTrxId}/${path}` ); @@ -677,7 +682,7 @@ export class ArDrive extends ArDriveAnonymous { [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, [tipData.txId]: +communityTipTrxReward }, - links: [`arweave.net/${uploadFileResult.dataTrxId}`, ...allLinks] + links: [`arweave.net/${uploadFileResult.dataTrxId}`, ...fileLinks] }); } diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index 7db32560..f220271c 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -54,7 +54,7 @@ new CLICommand({ // TODO: Private manifests 🤔 const result = await arDrive.uploadPublicManifest({ driveId, - parentFolderId: folderId, + folderId, maxDepth, destManifestName }); From 4925ba86ab49dd839975f18fa1841639a3c171b0 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 4 Nov 2021 13:32:19 -0500 Subject: [PATCH 07/30] refactor(manifest): Use index.html in specified manifest folder PE-477 --- src/ardrive.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index fa6fe7a3..595420b9 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -578,8 +578,6 @@ export class ArDrive extends ArDriveAnonymous { const drive = await this.arFsDao.getPublicDrive(driveId, owner); - const driveName = drive.name; - folderId ??= drive.rootFolderId; // TODO: Handle collision with existing manifest. New manifest will always be a new file, with @@ -627,9 +625,12 @@ export class ArDrive extends ArDriveAnonymous { } }); - // Use index.html in root folder if it exists, otherwise show first file found - const indexPath = Object.keys(pathMap).includes(`${driveName}/index.html`) - ? `${driveName}/index.html` + // Slice and replace path to compare above pattern + const baseFolderPath = sortedChildren[0]?.path?.slice(1).replace(/ /g, '_'); + + // Use index.html in the specified folder if it exists, otherwise show first file found + const indexPath = Object.keys(pathMap).includes(`${baseFolderPath}/index.html`) + ? `${baseFolderPath}/index.html` : Object.keys(pathMap)[0]; const arweaveManifest = new ArFSManifestToUpload({ From 5ad2399a93bdad07288d4090f1001f592569212b Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 10 Nov 2021 13:31:02 -0600 Subject: [PATCH 08/30] refactor(manifest): Use local pathing instead of full drive path PE-477 --- src/ardrive.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ardrive.ts b/src/ardrive.ts index ff9bd0d8..75272e47 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -650,13 +650,18 @@ export class ArDrive extends ArDriveAnonymous { owner }); - const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)) as ( + const sortedChildren = children.sort((a, b) => alphabeticalOrder(a.path, b.path)); + + // Slice and replace path to compare above pattern + const baseFolderPath = sortedChildren[0].path; + + const castedChildren = sortedChildren as ( | Partial | Partial )[]; // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 - sortedChildren.map((fileOrFolderMetaData) => { + castedChildren.map((fileOrFolderMetaData) => { if (fileOrFolderMetaData.entityType === 'folder') { delete fileOrFolderMetaData.lastModifiedDate; delete fileOrFolderMetaData.size; @@ -668,11 +673,11 @@ export class ArDrive extends ArDriveAnonymous { // TURN SORTED CHILDREN INTO MANIFEST const pathMap: ManifestPathMap = {}; - sortedChildren.forEach((child) => { + castedChildren.forEach((child) => { if (child.dataTxId && child.path && child.dataContentType !== MANIFEST_CONTENT_TYPE) { const path = child.path - // Slice off the leading "/" so manifest URLs path correctly - .slice(1) + // Slice off base folder path and the leading "/" so manifest URLs path correctly + .slice(baseFolderPath.length + 1) // Replace spaces with underscores for sharing links .replace(/ /g, '_'); @@ -680,13 +685,8 @@ export class ArDrive extends ArDriveAnonymous { } }); - // Slice and replace path to compare above pattern - const baseFolderPath = sortedChildren[0]?.path?.slice(1).replace(/ /g, '_'); - // Use index.html in the specified folder if it exists, otherwise show first file found - const indexPath = Object.keys(pathMap).includes(`${baseFolderPath}/index.html`) - ? `${baseFolderPath}/index.html` - : Object.keys(pathMap)[0]; + const indexPath = Object.keys(pathMap).includes(`index.html`) ? `index.html` : Object.keys(pathMap)[0]; const arweaveManifest = new ArFSManifestToUpload({ manifest: 'arweave/paths', From db0f2072b480d45afa307a087e177f6b4a819665 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 15 Nov 2021 13:19:15 -0600 Subject: [PATCH 09/30] Merge branch 'PE-635_integration' into PE-477_create_manifest_test --- .pnp.js | 12 +- ...mander-npm-8.2.0-c925691796-e41e680f2a.zip | Bin 44212 -> 0 bytes ...mander-npm-8.3.0-c0d18c66d5-0b818d97ca.zip | Bin 0 -> 44364 bytes package.json | 1 + src/CLICommand/action.test.ts | 68 +++ src/CLICommand/action.ts | 121 ++++ src/CLICommand/cli.ts | 23 +- src/CLICommand/cli_command.test.ts | 22 +- src/CLICommand/cli_command.ts | 65 +-- src/CLICommand/constants.ts | 2 - src/CLICommand/error_codes.ts | 3 + src/CLICommand/parameters_helper.test.ts | 308 +++++++---- src/CLICommand/parameters_helper.ts | 40 +- src/ardrive.ts | 522 +++++++----------- src/ardrive.types.ts | 161 ++++++ src/ardrive_anonymous.ts | 80 +++ src/arfs_entities.ts | 92 ++- src/arfs_entity_result_factory.ts | 2 +- src/arfs_file_wrapper.ts | 32 +- src/arfs_meta_data_factory.ts | 2 +- src/arfs_prototypes.ts | 16 +- src/arfs_trx_data_types.ts | 24 +- src/arfsdao.ts | 84 ++- src/arfsdao_anonymous.ts | 37 +- src/arweave_address.test.ts | 42 -- src/arweave_address.ts | 21 - src/commands/create_drive.ts | 20 +- src/commands/create_folder.ts | 22 +- src/commands/create_manifest.ts | 17 +- src/commands/drive_info.ts | 19 +- src/commands/file_info.ts | 18 +- src/commands/folder_info.ts | 18 +- src/commands/generate_seedphrase.ts | 7 +- src/commands/generate_wallet.ts | 12 +- src/commands/get_address.ts | 7 +- src/commands/get_balance.ts | 11 +- src/commands/get_drive_key.ts | 10 +- src/commands/get_file_key.ts | 13 +- src/commands/get_mempool.ts | 7 +- src/commands/index.ts | 3 - src/commands/list_all_drives.ts | 16 +- src/commands/list_drive.ts | 17 +- src/commands/list_folder.ts | 11 +- src/commands/move_file.ts | 23 +- src/commands/move_folder.ts | 19 +- src/commands/send_ar.ts | 23 +- src/commands/tx_status.ts | 20 +- src/commands/upload_file.ts | 38 +- src/community/ardrive_community_oracle.ts | 11 +- src/community/ardrive_contract_oracle.ts | 4 +- src/community/community_oracle.ts | 3 +- src/community/contract_types.ts | 2 +- src/community/smartweave_contract_oracle.ts | 2 +- src/folderHierarchy.ts | 51 +- src/index.ts | 12 +- src/parameter_declarations.ts | 30 +- src/private_key_data.ts | 12 +- src/query.ts | 2 +- src/types/ar.test.ts | 64 +++ src/types/ar.ts | 35 ++ src/types/arweave_address.test.ts | 82 +++ src/types/arweave_address.ts | 39 ++ src/types/byte_count.test.ts | 65 +++ src/types/byte_count.ts | 33 ++ src/types/entity_id.test.ts | 85 +++ src/types/entity_id.ts | 40 ++ src/types/equatable.ts | 3 + src/types/fee_multiple.test.ts | 84 +++ src/types/fee_multiple.ts | 38 ++ src/types/index.ts | 10 + src/types/seed_phrase.test.ts | 72 +++ src/types/seed_phrase.ts | 35 ++ src/types/transaction_id.test.ts | 86 +++ src/types/transaction_id.ts | 42 ++ src/{ => types}/types.ts | 26 +- src/types/unix_time.test.ts | 51 ++ src/types/unix_time.ts | 27 + src/types/winston.test.ts | 159 ++++++ src/types/winston.ts | 57 ++ src/utils/ar_data_price.test.ts | 50 -- src/utils/ar_data_price.ts | 22 +- src/utils/ar_data_price_estimator.ts | 40 +- src/utils/ar_data_price_oracle_estimator.ts | 4 +- ...ar_data_price_regression_estimator.test.ts | 66 +-- .../ar_data_price_regression_estimator.ts | 38 +- src/utils/ar_unit.test.ts | 42 -- src/utils/ar_unit.ts | 21 - src/utils/ardrive.test.ts | 85 ++- src/utils/arfs_builders/arfs_builders.ts | 29 +- .../arfs_builders/arfs_drive_builders.ts | 51 +- src/utils/arfs_builders/arfs_file_builders.ts | 41 +- .../arfs_builders/arfs_folder_builders.ts | 52 +- src/utils/arweave_oracle.ts | 4 +- src/utils/data_price_regression.test.ts | 51 +- src/utils/data_price_regression.ts | 18 +- src/utils/filter_methods.test.ts | 81 +++ src/utils/filter_methods.ts | 11 +- src/utils/gateway_oracle.ts | 7 +- src/utils/stubs.ts | 89 +-- src/wallet.ts | 54 +- tests/integration/ardrive.int.test.ts | 235 +++++--- yarn.lock | 9 +- 102 files changed, 3042 insertions(+), 1451 deletions(-) delete mode 100644 .yarn/cache/commander-npm-8.2.0-c925691796-e41e680f2a.zip create mode 100644 .yarn/cache/commander-npm-8.3.0-c0d18c66d5-0b818d97ca.zip create mode 100644 src/CLICommand/action.test.ts create mode 100644 src/CLICommand/action.ts delete mode 100644 src/CLICommand/constants.ts create mode 100644 src/CLICommand/error_codes.ts create mode 100644 src/ardrive.types.ts create mode 100644 src/ardrive_anonymous.ts delete mode 100644 src/arweave_address.test.ts delete mode 100644 src/arweave_address.ts create mode 100644 src/types/ar.test.ts create mode 100644 src/types/ar.ts create mode 100644 src/types/arweave_address.test.ts create mode 100644 src/types/arweave_address.ts create mode 100644 src/types/byte_count.test.ts create mode 100644 src/types/byte_count.ts create mode 100644 src/types/entity_id.test.ts create mode 100644 src/types/entity_id.ts create mode 100644 src/types/equatable.ts create mode 100644 src/types/fee_multiple.test.ts create mode 100644 src/types/fee_multiple.ts create mode 100644 src/types/index.ts create mode 100644 src/types/seed_phrase.test.ts create mode 100644 src/types/seed_phrase.ts create mode 100644 src/types/transaction_id.test.ts create mode 100644 src/types/transaction_id.ts rename src/{ => types}/types.ts (65%) create mode 100644 src/types/unix_time.test.ts create mode 100644 src/types/unix_time.ts create mode 100644 src/types/winston.test.ts create mode 100644 src/types/winston.ts delete mode 100644 src/utils/ar_data_price.test.ts delete mode 100644 src/utils/ar_unit.test.ts delete mode 100644 src/utils/ar_unit.ts create mode 100644 src/utils/filter_methods.test.ts diff --git a/.pnp.js b/.pnp.js index 46095fa4..ad4e7f5c 100755 --- a/.pnp.js +++ b/.pnp.js @@ -56,8 +56,9 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], ["base64-js", "npm:1.5.1"], + ["bignumber.js", "npm:9.0.1"], ["chai", "npm:4.3.4"], - ["commander", "npm:8.2.0"], + ["commander", "npm:8.3.0"], ["eslint", "npm:7.23.0"], ["eslint-config-prettier", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:8.1.0"], ["eslint-plugin-prettier", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:3.3.1"], @@ -1649,8 +1650,9 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], ["base64-js", "npm:1.5.1"], + ["bignumber.js", "npm:9.0.1"], ["chai", "npm:4.3.4"], - ["commander", "npm:8.2.0"], + ["commander", "npm:8.3.0"], ["eslint", "npm:7.23.0"], ["eslint-config-prettier", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:8.1.0"], ["eslint-plugin-prettier", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:3.3.1"], @@ -2460,10 +2462,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ], "linkType": "HARD", }], - ["npm:8.2.0", { - "packageLocation": "./.yarn/cache/commander-npm-8.2.0-c925691796-e41e680f2a.zip/node_modules/commander/", + ["npm:8.3.0", { + "packageLocation": "./.yarn/cache/commander-npm-8.3.0-c0d18c66d5-0b818d97ca.zip/node_modules/commander/", "packageDependencies": [ - ["commander", "npm:8.2.0"] + ["commander", "npm:8.3.0"] ], "linkType": "HARD", }] diff --git a/.yarn/cache/commander-npm-8.2.0-c925691796-e41e680f2a.zip b/.yarn/cache/commander-npm-8.2.0-c925691796-e41e680f2a.zip deleted file mode 100644 index 82823c8e915d362337efe091c49ac6f44b3fa8be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44212 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 8d81e8b0..f14e6847 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "arweave-bundles": "^1.0.3", "arweave-mnemonic-keys": "^0.0.9", "base64-js": "^1.5.1", + "bignumber.js": "^9.0.0", "commander": "^8.2.0", "jwk-to-pem": "^2.0.4", "lodash": "^4.17.21", diff --git a/src/CLICommand/action.test.ts b/src/CLICommand/action.test.ts new file mode 100644 index 00000000..5fad43ba --- /dev/null +++ b/src/CLICommand/action.test.ts @@ -0,0 +1,68 @@ +import { expect } from 'chai'; +import { CLIAction } from './action'; +import { ParsedParameters } from './cli'; +import { ALL_VALID_EXIT_CODES, SUCCESS_EXIT_CODE } from './error_codes'; + +const dummyActionHandler = () => Promise.resolve(SUCCESS_EXIT_CODE); + +describe('The CLIAction class', () => { + let action: CLIAction; + const params: ParsedParameters = { + myCustomOption: 'the parameter value' + }; + + it('throws an error when runningAction is called before the `trigger` method', () => { + expect(() => CLIAction.runningAction).to.throw(); + }); + + describe('On trigger resolve', () => { + let beforeTriggerAwaiter: Promise, afterTriggerAwaiter: Promise; + + before(() => { + action = new CLIAction(dummyActionHandler); + beforeTriggerAwaiter = action.actionAwaiter(); + }); + + it('Trigger resolves with a valid exit code', () => { + return action.trigger(params).then((exitCode) => expect(ALL_VALID_EXIT_CODES).to.include(exitCode)); + }); + + it('The before awaiter resolves with the parsedParameters', () => { + return beforeTriggerAwaiter.then((parsedParameters) => expect(parsedParameters).to.eql(params)); + }); + + it('The after awaiter resolves with the parsedParameters', () => { + afterTriggerAwaiter = action.actionAwaiter(); + return afterTriggerAwaiter.then((parsedParameters) => { + expect(parsedParameters).to.eql(params); + expect(() => CLIAction.runningAction).to.not.throw(); + }); + }); + }); + + describe('On trigger reject', () => { + it('The awaiter rejects on trigger reject', () => { + action = new CLIAction(async () => { + throw new Error('Error from inside the action'); + }); + action + .trigger(params) + .catch((err) => err) + .then((err) => expect(err).to.be.instanceOf(Error)); + }); + + it('The awaiter rejects if a parsing error is thrown', () => { + action = new CLIAction(dummyActionHandler); + const awaiter = action.actionAwaiter(); + action.setParsingError(new Error('Error while parsing argv')); + return awaiter.catch((err) => err).then((err) => expect(err).to.be.instanceOf(Error)); + }); + + it('The awaiter rejects when the action was not triggered', () => { + action = new CLIAction(dummyActionHandler); + const awaiter = action.actionAwaiter(); + action.wasNotTriggered(); + return awaiter.catch((err) => err).then((err) => expect(err).to.be.instanceOf(Error)); + }); + }); +}); diff --git a/src/CLICommand/action.ts b/src/CLICommand/action.ts new file mode 100644 index 00000000..9ba141c6 --- /dev/null +++ b/src/CLICommand/action.ts @@ -0,0 +1,121 @@ +import { ActionReturnType, AsyncActionCallback, ParsedParameters } from './cli'; +import { ERROR_EXIT_CODE } from './error_codes'; + +/** + * A wrapper for the command action callback + */ +export class CLIAction { + private _promiseInstance?: Promise; + private _parsedParameters: ParsedParameters = {}; + private awaiterDone?: (value?: ParsedParameters, error?: Error) => void; + private static _runningAction?: CLIAction; + + /** + * Create an instance of CLIAction + * @param {AsyncActionCallback} actionCallback the handler method of the action + */ + constructor(private readonly actionCallback: AsyncActionCallback) {} + + private get promiseInstance(): Promise { + if (!this._promiseInstance) { + throw new Error(`There's no instance of a promise before calling it`); + } + return this._promiseInstance; + } + + private get parsedParameters(): ParsedParameters { + if (!this._parsedParameters) { + throw new Error(`There's no instance of a promise before calling it`); + } + return this._parsedParameters; + } + + /** + * A static member refering to the executed action + * @name runningAction + * @type {CLIAction} + * @throws - When read before any action has run + */ + public static get runningAction(): CLIAction { + if (!this._runningAction) { + throw new Error(`No action has been called yet`); + } + return this._runningAction; + } + + /** + * Triggers the callback of the action and returns its promise + * @name trigger + * @param {ParsedParameters} params - A key/value dict representing the parsed argv + * @returns {Promise} - The promise of the callback + */ + async trigger(params: ParsedParameters): Promise { + this._promiseInstance = this.actionCallback(params); + CLIAction._runningAction = this; + this._parsedParameters = params; + return this.promiseInstance + .then((exitCode) => { + this.resolveAwaiter(); + return exitCode; + }) + .catch((err: Error) => { + console.log(`${err.name}: ${err.message}`); + this.rejectAwaiter(err); + return ERROR_EXIT_CODE; + }); + } + + /** + * A hook promise to the action callback + * @returns {Promise { + if (this._promiseInstance) { + // the promise was already called, resolve when the action is finished + return this.promiseInstance.then(() => this.parsedParameters); + } else { + // queue the awaiter for when the promise is called + const awaiter = new Promise((resolve, reject) => { + this.awaiterDone = function (value?: ParsedParameters, error?: Error): void { + if (value) { + resolve(value); + } else { + reject(error); + } + }; + }); + return awaiter; + } + } + + private resolveAwaiter(): void { + if (this.awaiterDone) { + const awaiter = this.awaiterDone; + awaiter(this.parsedParameters); + } + } + + private rejectAwaiter(err: Error): void { + if (this.awaiterDone) { + const awaiter = this.awaiterDone; + awaiter(undefined, err); + } + } + + /** + * Rejects all the awaiters of this specific action with the given err + * @param {Error} err - The error thrown while parsing the argv + * @returns {void} + */ + public setParsingError(err: Error): void { + this.rejectAwaiter(err); + } + + /** + * Rejects all the awaiters of this specific action because another action has run + * @returns {void} + */ + public wasNotTriggered(): void { + this.rejectAwaiter(new Error(`Action didn't run`)); + } +} diff --git a/src/CLICommand/cli.ts b/src/CLICommand/cli.ts index d4092004..1ab4740e 100644 --- a/src/CLICommand/cli.ts +++ b/src/CLICommand/cli.ts @@ -1,8 +1,8 @@ -export interface ParsedArguments { - // TODO: make parameterName to have type ParameterName - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [parameterName: string /** ParameterName */]: any; -} +import { CommanderError } from 'commander'; +import { ParameterName } from './parameter'; + +export type ParsedParameters = Record; + /** * @name CliApiObject * abstract class intended to help encapsulate the cli api object @@ -10,14 +10,23 @@ export interface ParsedArguments { */ export abstract class CliApiObject { abstract arguments(names: string): CliApiObject; - abstract action(action: (options: ParsedArguments) => Promise): CliApiObject; + abstract action(action: ActionCallback): CliApiObject; abstract option(aliases: string, description: string, defaultValue?: string | boolean): CliApiObject; abstract requiredOption(aliases: string, description: string, defaultValue?: string | boolean): CliApiObject; abstract command(commandName: string): CliApiObject; abstract parse(...args: [program: CliApiObject] | [argv: string[]]): void; abstract addHelpCommand(addHelp: boolean): void; - abstract opts(): ParsedArguments; + abstract opts(): ParsedParameters; abstract name(name: string): CliApiObject; abstract usage(usage: string): CliApiObject; abstract outputHelp(): void; + abstract exitOverride(callback?: (commanderError: CommanderError) => void): CliApiObject; } + +export type ActionCallback = (options: ParsedParameters) => ActionReturnType; // commander action callback + +export type AsyncActionCallback = (options: ParsedParameters) => Promise; + +export type ActionReturnType = ExitCode | void; + +export type ExitCode = number; diff --git a/src/CLICommand/cli_command.test.ts b/src/CLICommand/cli_command.test.ts index 448bc2c5..69caabe4 100644 --- a/src/CLICommand/cli_command.test.ts +++ b/src/CLICommand/cli_command.test.ts @@ -12,22 +12,20 @@ import { import { CliApiObject } from './cli'; import { baseArgv } from './test_constants'; import { Parameter } from './parameter'; +import { CLIAction } from './action'; +import { SUCCESS_EXIT_CODE } from './error_codes'; const MY_DRIVE_NAME = 'My awesome drive!'; const testingCommandName = 'drive-name-test'; +async function dummyAction() { + return SUCCESS_EXIT_CODE; +} const driveNameCommandDescription: CommandDescriptor = { name: testingCommandName, parameters: [DriveNameParameter], - async action(option) { - /** This code here will run after argv is parsed */ - expect(option.driveNameTest).to.equal(MY_DRIVE_NAME); - } + action: new CLIAction(dummyAction) }; const driveNameArgv: string[] = [...baseArgv, testingCommandName, '--drive-name', MY_DRIVE_NAME]; -async function action() { - // eslint-disable-next-line no-console - console.log('DUMMY ACTION'); -} const nonEmptyValue = 'non-empty value'; const commandDescriptorRequiredWallet: CommandDescriptor = { name: testingCommandName, @@ -35,7 +33,7 @@ const commandDescriptorRequiredWallet: CommandDescriptor = { WalletFileParameter, { name: UnsafeDrivePasswordParameter, requiredConjunctionParameters: [WalletFileParameter] } ], - action + action: new CLIAction(dummyAction) }; const parsedOptionsMissingWallet = { [WalletFileParameter]: undefined, @@ -44,7 +42,7 @@ const parsedOptionsMissingWallet = { const commandDescriptorForbiddenWalletFileAndSeedPhrase: CommandDescriptor = { name: testingCommandName, parameters: [WalletFileParameter, SeedPhraseParameter], - action + action: new CLIAction(dummyAction) }; const parsedCommandOptionsBothSpecified = { [WalletFileParameter]: nonEmptyValue, @@ -61,7 +59,7 @@ class TestCliApiObject { parse = stub(this.program, 'parse'); addHelpCommand = stub(this.program, 'addHelpCommand').returnsThis(); opts = stub(this.program, 'opts').returnsThis(); - + exitOverride = stub(this.program, 'exitOverride').returnsThis(); name = stub(this.program, 'name').returnsThis(); usage = stub(this.program, 'usage').returnsThis(); outputHelp = stub(this.program, 'outputHelp'); @@ -102,7 +100,7 @@ describe('CLICommand class', () => { }); it('No colliding parameters', () => { - const allCommandDescriptors = CLICommand._getAllCommandDescriptors(); + const allCommandDescriptors = CLICommand.getAllCommandDescriptors(); allCommandDescriptors.forEach((command) => { const parameters = command.parameters.map((param) => new Parameter(param)); parameters.forEach((parameter_1, index) => { diff --git a/src/CLICommand/cli_command.ts b/src/CLICommand/cli_command.ts index a1f9eefa..5a86fec8 100644 --- a/src/CLICommand/cli_command.ts +++ b/src/CLICommand/cli_command.ts @@ -1,20 +1,18 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Command } from 'commander'; -import { CliApiObject, ParsedArguments } from './cli'; -import { ERROR_EXIT_CODE } from './constants'; +import { program } from 'commander'; +import { CLIAction } from './action'; +import { CliApiObject, ExitCode, ParsedParameters } from './cli'; import { Parameter, ParameterName, ParameterOverridenConfig } from './parameter'; export type CommandName = string; export interface CommandDescriptor { name: CommandName; parameters: (ParameterName | ParameterOverridenConfig)[]; - action(options: ParsedArguments): Promise; + action: CLIAction; } -const program: CliApiObject = new Command() as CliApiObject; -program.name('ardrive'); -program.addHelpCommand(true); -program.usage('[command] [command-specific options]'); +const programAsUnknown: unknown = program; +const programApi: CliApiObject = programAsUnknown as CliApiObject; /** * @name setCommanderCommand @@ -46,14 +44,10 @@ function setCommanderCommand(commandDescriptor: CommandDescriptor, program: CliA command.option(...optionArguments); } }); - command = command.action(async (options) => { - await (async function () { - assertConjunctionParameters(commandDescriptor, options); - const exitCode = await commandDescriptor.action(options); + command = command.action((options) => { + assertConjunctionParameters(commandDescriptor, options); + commandDescriptor.action.trigger(options).then((exitCode) => { exitProgram(exitCode || 0); - })().catch((err) => { - console.log(err.message); - exitProgram(ERROR_EXIT_CODE); }); }); } @@ -97,44 +91,45 @@ export function assertForbidden(parameter: Parameter, options: any): void { } export class CLICommand { - private static allCommandDescriptors: CommandDescriptor[] = []; + private static allCommandInstances: CLICommand[] = []; /** * @param {CommandDescriptor} commandDescription an immutable representation of a command * @param {string[]} argv a custom argv for testing purposes */ - constructor(private readonly commandDescription: CommandDescriptor, private readonly _program?: CliApiObject) { - CLICommand.allCommandDescriptors.push(commandDescription); - this.setCommand(); + constructor(readonly commandDescription: CommandDescriptor, program: CliApiObject = programApi) { + program.name('ardrive'); + program.addHelpCommand(true); + program.usage('[command] [command-specific options]'); + // Override the commander's default exit (process.exit()) to avoid abruptly interrupting the script execution + program.exitOverride(); + setCommanderCommand(this.commandDescription, program); + CLICommand.allCommandInstances.push(this); } - // A singleton instance of the commander's program object - public static get program(): CliApiObject { - // TODO: make me private when index.ts is fully de-coupled from commander library - return program; + public get action(): Promise { + return this.commandDescription.action.actionAwaiter(); } - private get program(): CliApiObject { - return this._program || CLICommand.program; - } - - private setCommand(): void { - setCommanderCommand(this.commandDescription, this.program); + public static parse(program: CliApiObject = programApi, argv: string[] = process.argv): void { + program.parse(argv); + this.rejectNonTriggeredAwaiters(); } - public static parse(program: CliApiObject = this.program, argv: string[] = process.argv): void { - program.parse(argv); + private static rejectNonTriggeredAwaiters(): void { + // reject all action awaiters that haven't run + const theOtherCommandActions = CLICommand.getAllCommandDescriptors().map((descriptor) => descriptor.action); + theOtherCommandActions.forEach((action) => action.wasNotTriggered()); } /** - * For test purposes only * @returns {CommandDescriptor[]} all declared command descriptors */ - public static _getAllCommandDescriptors(): CommandDescriptor[] { - return this.allCommandDescriptors; + public static getAllCommandDescriptors(): CommandDescriptor[] { + return this.allCommandInstances.map((cmd) => cmd.commandDescription); } } -function exitProgram(exitCode: number): void { +function exitProgram(exitCode: ExitCode): void { process.exitCode = exitCode; } diff --git a/src/CLICommand/constants.ts b/src/CLICommand/constants.ts deleted file mode 100644 index f4dee84f..00000000 --- a/src/CLICommand/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const SUCCESS_EXIT_CODE = 0; -export const ERROR_EXIT_CODE = 1; diff --git a/src/CLICommand/error_codes.ts b/src/CLICommand/error_codes.ts new file mode 100644 index 00000000..0624f8f4 --- /dev/null +++ b/src/CLICommand/error_codes.ts @@ -0,0 +1,3 @@ +export const SUCCESS_EXIT_CODE = 0; +export const ERROR_EXIT_CODE = 1; +export const ALL_VALID_EXIT_CODES = [SUCCESS_EXIT_CODE, ERROR_EXIT_CODE]; diff --git a/src/CLICommand/parameters_helper.test.ts b/src/CLICommand/parameters_helper.test.ts index c0727637..9a07bbb4 100644 --- a/src/CLICommand/parameters_helper.test.ts +++ b/src/CLICommand/parameters_helper.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { Command } from 'commander'; -import { CliApiObject, ParsedArguments } from './cli'; +import { ActionReturnType, CliApiObject, ParsedParameters } from './cli'; import { CLICommand, CommandDescriptor } from './cli_command'; import { Parameter, ParameterName } from './parameter'; import { @@ -21,25 +21,39 @@ import { DriveKeyParameter, UnsafeDrivePasswordParameter, SeedPhraseParameter, - WalletFileParameter + WalletFileParameter, + MaxDepthParameter, + AllParameter } from '../parameter_declarations'; import '../parameter_declarations'; import { urlEncodeHashKey } from '../utils'; import { stubArweaveAddress } from '../utils/stubs'; +import { EID } from '../types'; +import { CLIAction } from './action'; +import { SUCCESS_EXIT_CODE } from './error_codes'; const expectedArweaveAddress = stubArweaveAddress('P8aFJizMVBl7HeoRAz2i1dNYkG_KoN7oB9tZpIw6lo4'); +const dummyActionHandler = () => Promise.resolve(SUCCESS_EXIT_CODE); + +/** + * @name declareCommandWithParams + * @param program + * @param parameters + * @param action - default is set for testing propuses + * @returns {void} + */ function declareCommandWithParams( program: CliApiObject, parameters: ParameterName[], - action: (options: ParsedArguments) => Promise -): void { + action?: (options: ParsedParameters) => Promise +): CLICommand { const command: CommandDescriptor = { name: testCommandName, parameters, - action + action: new CLIAction(action || dummyActionHandler) }; - new CLICommand(command, program); + return new CLICommand(command, program); } describe('ParametersHelper class', () => { @@ -51,116 +65,131 @@ describe('ParametersHelper class', () => { it('Actually reads the value from argv', () => { Parameter.declare(singleValueParameter); - declareCommandWithParams(program, [singleValueParameterName], async (options) => { + const cmd = declareCommandWithParams(program, [singleValueParameterName]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--single-value-parameter', '1234567890']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(parameters.getParameterValue(singleValueParameterName)).to.not.be.undefined; + return expect(parameters.getParameterValue(singleValueParameterName)).to.not.be.undefined; }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--single-value-parameter', '1234567890']); }); it('Boolean parameter false', () => { Parameter.declare(booleanParameter); - declareCommandWithParams(program, [booleanParameterName], async (options) => { + const cmd = declareCommandWithParams(program, [booleanParameterName]); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(!!parameters.getParameterValue(booleanParameterName)).to.be.false; + return expect(!!parameters.getParameterValue(booleanParameterName)).to.be.false; }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); it('Boolean parameter true', () => { Parameter.declare(booleanParameter); - declareCommandWithParams(program, [booleanParameterName], async (options) => { + const cmd = declareCommandWithParams(program, [booleanParameterName]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--boolean-parameter']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(parameters.getParameterValue(booleanParameterName)).to.be.true; + return expect(parameters.getParameterValue(booleanParameterName)).to.be.true; }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--boolean-parameter']); }); it('Array parameter', () => { const colorsArray = ['red', 'green', 'blue']; Parameter.declare(arrayParameter); - declareCommandWithParams(program, [arrayParameterName], async (options) => { + const cmd = declareCommandWithParams(program, [arrayParameterName]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--array-parameter', ...colorsArray]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(parameters.getParameterValue(arrayParameterName)).to.deep.equal(colorsArray); + return expect(parameters.getParameterValue(arrayParameterName)).to.deep.equal(colorsArray); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--array-parameter', ...colorsArray]); }); it('Required parameter throws if missing', () => { - CLICommand.parse(program, [...baseArgv, requiredParameterName]); Parameter.declare(requiredParameter); + declareCommandWithParams(program, [requiredParameterName]); + expect(() => CLICommand.parse(program, [...baseArgv, testCommandName])).to.throw(); }); describe('getIsPrivate method', () => { it('returns false when none of --unsafe-drive-password, --drive-key, -p, or -k are provided', () => { - declareCommandWithParams(program, [], async (options) => { + const cmd = declareCommandWithParams(program, []); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getIsPrivate()).to.be.false; + return parameters.getIsPrivate().then((isPrivate) => expect(isPrivate).to.be.false); }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); it('returns true when --unsafe-drive-password is provided', () => { - declareCommandWithParams(program, [UnsafeDrivePasswordParameter], async (options) => { + const cmd = declareCommandWithParams(program, [UnsafeDrivePasswordParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--unsafe-drive-password', 'pw']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getIsPrivate()).to.be.true; + return parameters.getIsPrivate().then((isPrivate) => expect(isPrivate).to.be.true); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--unsafe-drive-password', 'pw']); }); it('returns true when -p is provided', () => { - declareCommandWithParams(program, [UnsafeDrivePasswordParameter], async (options) => { + const cmd = declareCommandWithParams(program, [UnsafeDrivePasswordParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '-p', 'pw']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getIsPrivate()).to.be.true; + return parameters.getIsPrivate().then((isPrivate) => expect(isPrivate).to.be.true); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '-p', 'pw']); }); it('returns true when --drive-key is provided', () => { - declareCommandWithParams(program, [DriveKeyParameter], async (options) => { + const cmd = declareCommandWithParams(program, [DriveKeyParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--drive-key', 'key']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getIsPrivate()).to.be.true; + return parameters.getIsPrivate().then((isPrivate) => expect(isPrivate).to.be.true); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--drive-key', 'key']); }); it('returns true when -k is provided', () => { - declareCommandWithParams(program, [DriveKeyParameter], async (options) => { + const cmd = declareCommandWithParams(program, [DriveKeyParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '-k', 'key']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getIsPrivate()).to.be.true; + return parameters.getIsPrivate().then((isPrivate) => expect(isPrivate).to.be.true); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '-k', 'key']); }); }); describe('getRequiredWallet method', () => { it('returns a wallet when a valid --wallet-file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getRequiredWallet()).to.not.be.null; + return parameters.getRequiredWallet(); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); }); it('returns a wallet when a valid --w file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getRequiredWallet()).to.not.be.null; + return parameters.getRequiredWallet(); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); }); - it('returns a wallet when a valid --seed-phrase option is provided', () => { - declareCommandWithParams(program, [SeedPhraseParameter], async (options) => { - const parameters = new ParametersHelper(options); - expect(await parameters.getRequiredWallet()).to.not.be.null; - }); + it('returns a wallet when a valid --seed-phrase option is provided', function () { + // FIXME: it takes too long + this.timeout(60_000); + const cmd = declareCommandWithParams(program, [SeedPhraseParameter]); CLICommand.parse(program, [ ...baseArgv, testCommandName, '--seed-phrase', 'alcohol wisdom allow used april recycle exhibit parent music field cabbage treat' ]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + return parameters.getRequiredWallet(); + }); }); // Note: Redundant prolonged seed-phrase tests are commented out to save testing time @@ -178,37 +207,36 @@ describe('ParametersHelper class', () => { // ]); // }); - it('throws when none of --wallet-file, -w, --seed-phrase, or -s option are provided', (done) => { - declareCommandWithParams(program, [], async (options) => { + it('throws when none of --wallet-file, -w, --seed-phrase, or -s option are provided', () => { + const cmd = declareCommandWithParams(program, []); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - await parameters + return parameters .getRequiredWallet() - .then((wallet) => { - done(`It shouldn't have returned a wallet: ${wallet}`); - }) - .catch(() => { - done(); - }); + .catch(() => null) + .then((wallet) => expect(wallet).to.be.null); }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); }); describe('getOptionalWallet method', () => { it('returns a wallet when a valid --wallet-file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getOptionalWallet()).to.not.be.null; + return parameters.getOptionalWallet(); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); }); it('returns a wallet when a valid --w file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(await parameters.getOptionalWallet()).to.not.be.null; + return parameters.getOptionalWallet(); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); }); // Note: Redundant prolonged seed-phrase tests are commented out to save testing time @@ -240,30 +268,39 @@ describe('ParametersHelper class', () => { // }); it('returns null when none of --wallet-file, -w, --seed-phrase, or -s option are provided', () => { - declareCommandWithParams(program, [], async (options) => { + const cmd = declareCommandWithParams(program, []); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - const wallet = await parameters.getOptionalWallet().catch(() => null); - expect(wallet).to.be.null; + const walletPromise = parameters.getOptionalWallet().catch(() => null); + return walletPromise.then((wallet) => expect(wallet).to.be.null); }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); }); describe('getWalletAddress method', () => { it('returns the address of the wallet when a valid --wallet-file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(`${await parameters.getWalletAddress()}`).to.equal(`${expectedArweaveAddress}`); + const walletAddressPromise = parameters.getWalletAddress(); + return walletAddressPromise.then((walletAddress) => + expect(`${walletAddress}`).to.equal(`${expectedArweaveAddress}`) + ); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '--wallet-file', './test_wallet.json']); }); it('returns the address of the wallet when a valid --w file is provided', () => { - declareCommandWithParams(program, [WalletFileParameter], async (options) => { + const cmd = declareCommandWithParams(program, [WalletFileParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - expect(`${await parameters.getWalletAddress()}`).to.equal(`${expectedArweaveAddress}`); + const walletAddressPromise = parameters.getWalletAddress(); + return walletAddressPromise.then((walletAddress) => + expect(`${walletAddress}`).to.equal(`${expectedArweaveAddress}`) + ); }); - CLICommand.parse(program, [...baseArgv, testCommandName, '-w', './test_wallet.json']); }); // Note: Redundant prolonged seed-phrase tests are commented out to save testing time @@ -295,49 +332,53 @@ describe('ParametersHelper class', () => { // }); it('returns the address provided by the --address option value', () => { - declareCommandWithParams(program, [AddressParameter], async (options) => { - const parameters = new ParametersHelper(options); - expect(`${await parameters.getWalletAddress()}`).to.equal(`${expectedArweaveAddress}`); - }); + const cmd = declareCommandWithParams(program, [AddressParameter]); CLICommand.parse(program, [ ...baseArgv, testCommandName, '--address', 'P8aFJizMVBl7HeoRAz2i1dNYkG_KoN7oB9tZpIw6lo4' ]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const walletAddressPromise = parameters.getWalletAddress(); + return walletAddressPromise.then((walletAddress) => + expect(`${walletAddress}`).to.equal(`${expectedArweaveAddress}`) + ); + }); }); it('returns the address provided by the -a option value', () => { - declareCommandWithParams(program, [AddressParameter], async (options) => { - const parameters = new ParametersHelper(options); - expect(`${await parameters.getWalletAddress()}`).to.equal(`${expectedArweaveAddress}`); - }); + const cmd = declareCommandWithParams(program, [AddressParameter]); CLICommand.parse(program, [ ...baseArgv, testCommandName, '-a', 'P8aFJizMVBl7HeoRAz2i1dNYkG_KoN7oB9tZpIw6lo4' ]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const walletPromise = parameters.getWalletAddress(); + return walletPromise.then((walletAddress) => + expect(`${walletAddress}`).to.equal(`${expectedArweaveAddress}`) + ); + }); }); it('throws when none of --wallet-file, -w, --seed-phrase, -s, --address, or -a option are provided', () => { - declareCommandWithParams(program, [], async (options) => { + const cmd = declareCommandWithParams(program, []); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - const wallet = await parameters.getWalletAddress().catch(() => null); - expect(wallet).to.be.null; + const walletPromise = parameters.getWalletAddress().catch(() => null); + return walletPromise.then((wallet) => expect(wallet).to.be.null); }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); }); describe('getDriveKey method', () => { it('returns the correct drive key given a valid --wallet-file and --unsafe-drive-password', () => { - declareCommandWithParams(program, [WalletFileParameter, UnsafeDrivePasswordParameter], async (options) => { - const parameters = new ParametersHelper(options); - expect( - urlEncodeHashKey(await parameters.getDriveKey({ driveId: '00000000-0000-0000-0000-000000000000' })) - ).to.equal('Fqjb/eoHUHkoPwyTe52VUJkUkOtLg0eoWdV1u03DDzg'); - }); + const cmd = declareCommandWithParams(program, [WalletFileParameter, UnsafeDrivePasswordParameter]); CLICommand.parse(program, [ ...baseArgv, testCommandName, @@ -346,40 +387,97 @@ describe('ParametersHelper class', () => { '--unsafe-drive-password', 'password' ]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const driveKeyPromise = parameters.getDriveKey({ + driveId: EID('00000000-0000-0000-0000-000000000000') + }); + return driveKeyPromise.then((driveKey) => + expect(urlEncodeHashKey(driveKey)).to.equal('Fqjb/eoHUHkoPwyTe52VUJkUkOtLg0eoWdV1u03DDzg') + ); + }); }); it('returns the drive key provided by the --drive-key option', () => { - declareCommandWithParams(program, [DriveKeyParameter], async (options) => { - const parameters = new ParametersHelper(options); - expect( - urlEncodeHashKey(await parameters.getDriveKey({ driveId: '00000000-0000-0000-0000-000000000000' })) - ).to.equal('Fqjb/eoHUHkoPwyTe52VUJkUkOtLg0eoWdV1u03DDzg'); - }); + const cmd = declareCommandWithParams(program, [DriveKeyParameter]); CLICommand.parse(program, [ ...baseArgv, testCommandName, '--drive-key', 'Fqjb/eoHUHkoPwyTe52VUJkUkOtLg0eoWdV1u03DDzg' ]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + return parameters + .getDriveKey({ driveId: EID('00000000-0000-0000-0000-000000000000') }) + .then((driveKey) => + expect(urlEncodeHashKey(driveKey)).to.equal('Fqjb/eoHUHkoPwyTe52VUJkUkOtLg0eoWdV1u03DDzg') + ); + }); }); it('throws when none of --wallet-file, -w, --seed-phrase, -s, --drive-key, or -k option are provided', () => { - declareCommandWithParams(program, [], async (options) => { + const cmd = declareCommandWithParams(program, []); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { const parameters = new ParametersHelper(options); - const driveKey = await parameters - .getDriveKey({ driveId: '00000000-0000-0000-0000-000000000000' }) + const driveKeyPromise = parameters + .getDriveKey({ driveId: EID('00000000-0000-0000-0000-000000000000') }) .catch(() => null); - expect(driveKey).to.be.null; + return driveKeyPromise.then((driveKey) => expect(driveKey).to.be.null); }); - CLICommand.parse(program, [...baseArgv, testCommandName]); }); }); describe('getMaxDepth method', () => { - it(`Defaults to zero`); - it(`Does not accept a decimal`); - it(`Does not accept a negative integer`); - it(`Max depth is infinity when --all is specified`); - it(`Custom positive value is providen`); + it('Defaults to zero', () => { + const cmd = declareCommandWithParams(program, [MaxDepthParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName]); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const maxDepthPromise = parameters.getMaxDepth(); + return maxDepthPromise.then((maxDepth) => expect(maxDepth).to.equal(0)); + }); + }); + + it('Does not accept a decimal', () => { + const cmd = declareCommandWithParams(program, [MaxDepthParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--max-depth=.33']); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const maxDepthPromise = parameters.getMaxDepth().catch(() => null); + return maxDepthPromise.then((maxDepth) => expect(maxDepth).to.be.null); + }); + }); + + it('Does not accept a negative integer', () => { + const cmd = declareCommandWithParams(program, [MaxDepthParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--max-depth=-100']); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const maxDepthPromise = parameters.getMaxDepth().catch(() => null); + return maxDepthPromise.then((maxDepth) => expect(maxDepth).to.be.null); + }); + }); + + it('Max depth is the MAX_SAFE_INTEGER when --all is specified', () => { + const cmd = declareCommandWithParams(program, [MaxDepthParameter, AllParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--all']); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const maxDepthPromise = parameters.getMaxDepth().catch(() => null); + return maxDepthPromise.then((maxDepth) => expect(maxDepth).to.equal(Number.MAX_SAFE_INTEGER)); + }); + }); + + it('Custom positive value is provided', () => { + const cmd = declareCommandWithParams(program, [MaxDepthParameter]); + CLICommand.parse(program, [...baseArgv, testCommandName, '--max-depth=8']); + return cmd.action.then((options) => { + const parameters = new ParametersHelper(options); + const maxDepthPromise = parameters.getMaxDepth().catch(() => null); + return maxDepthPromise.then((maxDepth) => expect(typeof maxDepth).to.equal('number')); + }); + }); }); }); diff --git a/src/CLICommand/parameters_helper.ts b/src/CLICommand/parameters_helper.ts index 58dcdc9a..5eff9faa 100644 --- a/src/CLICommand/parameters_helper.ts +++ b/src/CLICommand/parameters_helper.ts @@ -12,14 +12,14 @@ import { WalletFileParameter, PrivateParameter, ReplaceParameter, - SkipParameter + SkipParameter, + BoostParameter } from '../parameter_declarations'; import { cliWalletDao } from '..'; -import { DriveID, DriveKey } from '../types'; +import { DriveID, DriveKey, ArweaveAddress, SeedPhrase, FeeMultiple, ADDR } from '../types'; import passwordPrompt from 'prompts'; import { PrivateKeyData } from '../private_key_data'; -import { ArweaveAddress } from '../arweave_address'; -import { FileNameConflictResolution, replaceOnConflicts, skipOnConflicts, upsertOnConflicts } from '../ardrive'; +import { FileNameConflictResolution, replaceOnConflicts, skipOnConflicts, upsertOnConflicts } from '../ardrive.types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ParameterOptions = any; @@ -70,7 +70,7 @@ export class ParametersHelper { const walletJWK: JWKInterface = walletJSON as JWKInterface; return new JWKWallet(walletJWK); } else if (seedPhrase) { - return await this.walletDao.generateJWKWallet(seedPhrase); + return await this.walletDao.generateJWKWallet(new SeedPhrase(seedPhrase)); } throw new Error('Neither a wallet file nor seed phrase was provided!'); } @@ -82,12 +82,17 @@ export class ParametersHelper { public async getWalletAddress(): Promise { const address = this.getParameterValue(AddressParameter); if (address) { - return new ArweaveAddress(address); + return ADDR(address); } return this.getRequiredWallet().then((wallet) => wallet.getAddress()); } + public getOptionalBoostSetting(): FeeMultiple | undefined { + const boost = this.getParameterValue(BoostParameter); + return boost ? new FeeMultiple(+boost) : undefined; + } + public async getPrivateKeyData(): Promise { // Gather optional private parameters const driveKey = this.getParameterValue(DriveKeyParameter); @@ -116,7 +121,7 @@ export class ParametersHelper { // • (--wallet-file or --seed-phrase) + (--unsafe-drive-password or --private password) if (useCache) { - const cachedDriveKey = ParametersHelper.driveKeyCache[driveId]; + const cachedDriveKey = ParametersHelper.driveKeyCache[`${driveId}`]; if (cachedDriveKey) { return cachedDriveKey; } @@ -125,7 +130,7 @@ export class ParametersHelper { const driveKey = this.getParameterValue(DriveKeyParameter); if (driveKey) { const paramDriveKey = Buffer.from(driveKey, 'base64'); - ParametersHelper.driveKeyCache[driveId] = paramDriveKey; + ParametersHelper.driveKeyCache[`${driveId}`] = paramDriveKey; return paramDriveKey; } @@ -134,10 +139,10 @@ export class ParametersHelper { const wallet: JWKWallet = (await this.getRequiredWallet()) as JWKWallet; const derivedDriveKey: DriveKey = await deriveDriveKey( drivePassword, - driveId, + `${driveId}`, JSON.stringify(wallet.getPrivateKey()) ); - ParametersHelper.driveKeyCache[driveId] = derivedDriveKey; + ParametersHelper.driveKeyCache[`${driveId}`] = derivedDriveKey; return derivedDriveKey; } throw new Error(`No drive key or password provided for drive ID ${driveId}!`); @@ -193,7 +198,7 @@ export class ParametersHelper { return unsafePassword; } - public async getMaxDepth(defaultDepth: number): Promise { + public async getMaxDepth(defaultDepth = 0): Promise { if (this.getParameterValue(AllParameter)) { return Number.MAX_SAFE_INTEGER; } @@ -235,14 +240,21 @@ export class ParametersHelper { /** * @param {ParameterName} parameterName + * @param {(input: any) => T} mapFunc A function that maps the parameter value into a T instance * @returns {string | undefined} - * Returns the string value for the specific parameter; throws an error if not set + * @throws - When the required parameter value has a falsy value + * Returns the string value for the specific parameter */ - public getRequiredParameterValue(parameterName: ParameterName): string { + public getRequiredParameterValue( + parameterName: ParameterName, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mapFunc: (input: any) => T = (input: any) => input as T + ): T { + // FIXME: it could also return an array or a boolean! const value = this.options[parameterName]; if (!value) { throw new Error(`Required parameter ${parameterName} wasn't provided!`); } - return value; + return mapFunc(value); } } diff --git a/src/ardrive.ts b/src/ardrive.ts index 1bef5351..04394e97 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -1,20 +1,27 @@ -import { ArFSDAO, PrivateDriveKeyData } from './arfsdao'; +import { ArFSDAO } from './arfsdao'; +import { + ArFSManifestResult, + UploadPrivateFileParams, + UploadPublicFileParams, + UploadPublicManifestParams +} from './ardrive.types'; import { CommunityOracle } from './community/community_oracle'; -import { ArFSDriveEntity, deriveDriveKey, DrivePrivacy, GQLTagInterface, winstonToAr } from 'ardrive-core-js'; +import { deriveDriveKey, DrivePrivacy, GQLTagInterface } from 'ardrive-core-js'; import { - TransactionID, - Winston, DriveID, FolderID, TipType, - FeeMultiple, DriveKey, - EntityID, - FileID, + ArweaveAddress, ByteCount, - MakeOptional, ManifestPathMap, - MANIFEST_CONTENT_TYPE + MANIFEST_CONTENT_TYPE, + W, + Winston, + AR, + FeeMultiple, + FileID, + stubTransactionID } from './types'; import { WalletDAO, Wallet, JWKWallet } from './wallet'; import { ARDataPriceRegressionEstimator } from './utils/ar_data_price_regression_estimator'; @@ -33,195 +40,47 @@ import { ArFSPublicFolderTransactionData } from './arfs_trx_data_types'; import { urlEncodeHashKey } from './utils'; -import { ArFSDAOAnonymous, ArFSDAOType, ArFSListPublicFolderParams } from './arfsdao_anonymous'; import { ArFSPrivateDrive, ArFSPrivateFile, ArFSPrivateFileOrFolderWithPaths, ArFSPrivateFolder, - ArFSPublicDrive, - ArFSPublicFile, - ArFSPublicFileOrFolderWithPaths, - ArFSPublicFolder + ArFSPublicFileOrFolderWithPaths } from './arfs_entities'; -import { stubEntityID, stubTransactionID } from './utils/stubs'; +import { stubEntityID } from './utils/stubs'; import { errorMessage } from './error_message'; -import { PrivateKeyData } from './private_key_data'; -import { ArweaveAddress } from './arweave_address'; -import { WithDriveKey } from './arfs_entity_result_factory'; import { alphabeticalOrder } from './utils/sort_functions'; - -export type ArFSEntityDataType = 'drive' | 'folder' | 'file'; - -export interface ArFSEntityData { - type: ArFSEntityDataType; - metadataTxId: TransactionID; - dataTxId?: TransactionID; - entityId: EntityID; - key?: string; -} - -export type ListPublicFolderParams = MakeOptional; -export type ListPrivateFolderParams = ListPublicFolderParams & WithDriveKey; - -export interface TipData { - txId: TransactionID; - recipient: ArweaveAddress; - winston: Winston; -} - -export interface TipResult { - tipData: TipData; - reward: Winston; -} - -export type ArFSFees = { [key: string]: number }; - -export interface ArFSResult { - created: ArFSEntityData[]; - tips: TipData[]; - fees: ArFSFees; -} - -export interface ArFSManifestResult extends ArFSResult { - links: string[]; -} -const emptyArFSResult: ArFSResult = { - created: [], - tips: [], - fees: {} -}; - -export interface MetaDataBaseCosts { - metaDataBaseReward: Winston; -} - -export interface BulkFileBaseCosts extends MetaDataBaseCosts { - fileDataBaseReward: Winston; -} -export interface FileUploadBaseCosts extends BulkFileBaseCosts { - communityWinstonTip: Winston; -} - -export interface DriveUploadBaseCosts { - driveMetaDataBaseReward: Winston; - rootFolderMetaDataBaseReward: Winston; -} - -export interface UploadPublicManifestParams { - driveId?: DriveID; - folderId?: FolderID; - maxDepth?: number; - destManifestName?: string; -} - -interface RecursivePublicBulkUploadParams { - parentFolderId: FolderID; - wrappedFolder: ArFSFolderToUpload; - driveId: DriveID; - owner: ArweaveAddress; - conflictResolution: FileNameConflictResolution; -} -type RecursivePrivateBulkUploadParams = RecursivePublicBulkUploadParams & WithDriveKey; - -interface CreatePublicFolderParams { - folderName: string; - driveId: DriveID; - parentFolderId: FolderID; -} -type CreatePrivateFolderParams = CreatePublicFolderParams & WithDriveKey; - -interface MovePublicFolderParams { - folderId: FolderID; - newParentFolderId: FolderID; -} -type MovePrivateFolderParams = MovePublicFolderParams & WithDriveKey; - -export const skipOnConflicts = 'skip'; -export const replaceOnConflicts = 'replace'; -export const upsertOnConflicts = 'upsert'; -// export const askOnConflicts = 'ask'; - -export type FileNameConflictResolution = typeof skipOnConflicts | typeof replaceOnConflicts | typeof upsertOnConflicts; -// | typeof askOnConflicts; - -export interface UploadParams { - parentFolderId: FolderID; - conflictResolution?: FileNameConflictResolution; -} - -export interface BulkPublicUploadParams extends UploadParams { - wrappedFolder: ArFSFolderToUpload; - destParentFolderName?: string; -} -export type BulkPrivateUploadParams = BulkPublicUploadParams & WithDriveKey; - -export interface UploadPublicFileParams extends UploadParams { - wrappedFile: ArFSFileToUpload; - destinationFileName?: string; -} -export type UploadPrivateFileParams = UploadPublicFileParams & WithDriveKey; - -export abstract class ArDriveType { - protected abstract readonly arFsDao: ArFSDAOType; -} - -export class ArDriveAnonymous extends ArDriveType { - constructor(protected readonly arFsDao: ArFSDAOAnonymous) { - super(); - } - - async getOwnerForDriveId(driveId: DriveID): Promise { - return this.arFsDao.getOwnerForDriveId(driveId); - } - - async getPublicDrive(driveId: DriveID, owner?: ArweaveAddress): Promise { - if (!owner) { - owner = await this.getOwnerForDriveId(driveId); - } - - return this.arFsDao.getPublicDrive(driveId, owner); - } - - async getPublicFolder(folderId: FolderID, owner?: ArweaveAddress): Promise { - if (!owner) { - owner = await this.arFsDao.getDriveOwnerForFolderId(folderId); - } - - return this.arFsDao.getPublicFolder(folderId, owner); - } - - async getPublicFile(fileId: FileID, owner?: ArweaveAddress): Promise { - if (!owner) { - owner = await this.arFsDao.getDriveOwnerForFileId(fileId); - } - - return this.arFsDao.getPublicFile(fileId, owner); - } - - async getAllDrivesForAddress(address: ArweaveAddress, privateKeyData: PrivateKeyData): Promise { - return this.arFsDao.getAllDrivesForAddress(address, privateKeyData); - } - - /** - * Lists the children of certain public folder - * @param {FolderID} folderId the folder ID to list children of - * @returns {ArFSPublicFileOrFolderWithPaths[]} an array representation of the children and parent folder - */ - async listPublicFolder({ - folderId, - maxDepth = 0, - includeRoot = false, - owner - }: ListPublicFolderParams): Promise { - if (!owner) { - owner = await this.arFsDao.getDriveOwnerForFolderId(folderId); - } - - const children = await this.arFsDao.listPublicFolder({ folderId, maxDepth, includeRoot, owner }); - return children; - } -} +import { ArDriveAnonymous } from './ardrive_anonymous'; +import { + CommunityTipParams, + TipResult, + MovePublicFileParams, + ArFSResult, + MovePrivateFileParams, + MovePublicFolderParams, + MovePrivateFolderParams, + upsertOnConflicts, + skipOnConflicts, + emptyArFSResult, + BulkPublicUploadParams, + RecursivePublicBulkUploadParams, + ArFSEntityData, + ArFSFees, + BulkPrivateUploadParams, + RecursivePrivateBulkUploadParams, + CreatePublicFolderParams, + CreatePrivateFolderParams, + CreatePublicDriveParams, + FileNameConflictResolution, + GetPrivateDriveParams, + GetPrivateFolderParams, + GetPrivateFileParams, + ListPrivateFolderParams, + MetaDataBaseCosts, + FileUploadBaseCosts, + DriveUploadBaseCosts, + CreatePrivateDriveParams +} from './ardrive.types'; export class ArDrive extends ArDriveAnonymous { constructor( @@ -232,22 +91,22 @@ export class ArDrive extends ArDriveAnonymous { private readonly appName: string, private readonly appVersion: string, private readonly priceEstimator: ARDataPriceEstimator = new ARDataPriceRegressionEstimator(true), - private readonly feeMultiple: FeeMultiple = 1.0, + private readonly feeMultiple: FeeMultiple = new FeeMultiple(1.0), private readonly dryRun: boolean = false ) { super(arFsDao); } // NOTE: Presumes that there's a sufficient wallet balance - async sendCommunityTip(communityWinstonTip: Winston, assertBalance = false): Promise { + async sendCommunityTip({ communityWinstonTip, assertBalance = false }: CommunityTipParams): Promise { const tokenHolder: ArweaveAddress = await this.communityOracle.selectTokenHolder(); - const arTransferBaseFee = await this.priceEstimator.getBaseWinstonPriceForByteCount(0); + const arTransferBaseFee = await this.priceEstimator.getBaseWinstonPriceForByteCount(new ByteCount(0)); const transferResult = await this.walletDao.sendARToAddress( - winstonToAr(+communityWinstonTip), + new AR(communityWinstonTip), this.wallet, tokenHolder, - { reward: arTransferBaseFee.toString(), feeMultiple: this.feeMultiple }, + { reward: arTransferBaseFee, feeMultiple: this.feeMultiple }, this.dryRun, this.getTipTags(), assertBalance @@ -268,19 +127,19 @@ export class ArDrive extends ArDriveAnonymous { ]; } - async movePublicFile(fileId: FileID, newParentFolderId: FolderID): Promise { + public async movePublicFile({ fileId, newParentFolderId }: MovePublicFileParams): Promise { const destFolderDriveId = await this.arFsDao.getDriveIdForFolderId(newParentFolderId); const owner = await this.getOwnerForDriveId(destFolderDriveId); await this.assertOwnerAddress(owner); - const originalFileMetaData = await this.getPublicFile(fileId); + const originalFileMetaData = await this.getPublicFile({ fileId }); - if (destFolderDriveId !== originalFileMetaData.driveId) { + if (!destFolderDriveId.equals(originalFileMetaData.driveId)) { throw new Error(errorMessage.cannotMoveToDifferentDrive); } - if (originalFileMetaData.parentFolderId === newParentFolderId) { + if (originalFileMetaData.parentFolderId.equals(newParentFolderId)) { throw new Error(errorMessage.cannotMoveIntoSamePlace('File', newParentFolderId)); } @@ -321,24 +180,24 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [moveFileResult.metaDataTrxId]: +moveFileResult.metaDataTrxReward + [`${moveFileResult.metaDataTrxId}`]: moveFileResult.metaDataTrxReward } }); } - async movePrivateFile(fileId: FileID, newParentFolderId: FolderID, driveKey: DriveKey): Promise { + public async movePrivateFile({ fileId, newParentFolderId, driveKey }: MovePrivateFileParams): Promise { const destFolderDriveId = await this.arFsDao.getDriveIdForFolderId(newParentFolderId); const owner = await this.getOwnerForDriveId(destFolderDriveId); await this.assertOwnerAddress(owner); - const originalFileMetaData = await this.getPrivateFile(fileId, driveKey); + const originalFileMetaData = await this.getPrivateFile({ fileId, driveKey }); - if (destFolderDriveId !== originalFileMetaData.driveId) { + if (!destFolderDriveId.equals(originalFileMetaData.driveId)) { throw new Error(errorMessage.cannotMoveToDifferentDrive); } - if (originalFileMetaData.parentFolderId === newParentFolderId) { + if (originalFileMetaData.parentFolderId.equals(newParentFolderId)) { throw new Error(errorMessage.cannotMoveIntoSamePlace('File', newParentFolderId)); } @@ -382,13 +241,13 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [moveFileResult.metaDataTrxId]: +moveFileResult.metaDataTrxReward + [`${moveFileResult.metaDataTrxId}`]: moveFileResult.metaDataTrxReward } }); } - async movePublicFolder({ folderId, newParentFolderId }: MovePublicFolderParams): Promise { - if (folderId === newParentFolderId) { + public async movePublicFolder({ folderId, newParentFolderId }: MovePublicFolderParams): Promise { + if (folderId.equals(newParentFolderId)) { throw new Error(errorMessage.folderCannotMoveIntoItself); } @@ -397,13 +256,13 @@ export class ArDrive extends ArDriveAnonymous { const owner = await this.getOwnerForDriveId(destFolderDriveId); await this.assertOwnerAddress(owner); - const originalFolderMetaData = await this.getPublicFolder(folderId); + const originalFolderMetaData = await this.getPublicFolder({ folderId }); - if (destFolderDriveId !== originalFolderMetaData.driveId) { + if (!destFolderDriveId.equals(originalFolderMetaData.driveId)) { throw new Error(errorMessage.cannotMoveToDifferentDrive); } - if (originalFolderMetaData.parentFolderId === newParentFolderId) { + if (originalFolderMetaData.parentFolderId.equals(newParentFolderId)) { throw new Error(errorMessage.cannotMoveIntoSamePlace('Folder', newParentFolderId)); } @@ -420,7 +279,7 @@ export class ArDrive extends ArDriveAnonymous { owner }); - if (childrenFolderIds.includes(newParentFolderId)) { + if (childrenFolderIds.some((fid) => fid.equals(newParentFolderId))) { throw new Error(errorMessage.cannotMoveParentIntoChildFolder); } @@ -449,13 +308,17 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [moveFolderResult.metaDataTrxId]: +moveFolderResult.metaDataTrxReward + [`${moveFolderResult.metaDataTrxId}`]: moveFolderResult.metaDataTrxReward } }); } - async movePrivateFolder({ folderId, newParentFolderId, driveKey }: MovePrivateFolderParams): Promise { - if (folderId === newParentFolderId) { + public async movePrivateFolder({ + folderId, + newParentFolderId, + driveKey + }: MovePrivateFolderParams): Promise { + if (folderId.equals(newParentFolderId)) { throw new Error(errorMessage.folderCannotMoveIntoItself); } @@ -464,13 +327,13 @@ export class ArDrive extends ArDriveAnonymous { const owner = await this.getOwnerForDriveId(destFolderDriveId); await this.assertOwnerAddress(owner); - const originalFolderMetaData = await this.getPrivateFolder(folderId, driveKey); + const originalFolderMetaData = await this.getPrivateFolder({ folderId, driveKey }); - if (destFolderDriveId !== originalFolderMetaData.driveId) { + if (!destFolderDriveId.equals(originalFolderMetaData.driveId)) { throw new Error(errorMessage.cannotMoveToDifferentDrive); } - if (originalFolderMetaData.parentFolderId === newParentFolderId) { + if (originalFolderMetaData.parentFolderId.equals(newParentFolderId)) { throw new Error(errorMessage.cannotMoveIntoSamePlace('Folder', newParentFolderId)); } @@ -488,7 +351,7 @@ export class ArDrive extends ArDriveAnonymous { owner }); - if (childrenFolderIds.includes(newParentFolderId)) { + if (childrenFolderIds.some((fid) => fid.equals(newParentFolderId))) { throw new Error(errorMessage.cannotMoveParentIntoChildFolder); } @@ -521,12 +384,12 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [moveFolderResult.metaDataTrxId]: +moveFolderResult.metaDataTrxReward + [`${moveFolderResult.metaDataTrxId}`]: moveFolderResult.metaDataTrxReward } }); } - async uploadPublicFile({ + public async uploadPublicFile({ parentFolderId, wrappedFile, destinationFileName, @@ -562,7 +425,7 @@ export class ArDrive extends ArDriveAnonymous { if ( conflictResolution === upsertOnConflicts && - conflictingFileName.lastModifiedDate === wrappedFile.lastModifiedDate + conflictingFileName.lastModifiedDate.valueOf() === wrappedFile.lastModifiedDate.valueOf() ) { // These files have the same name and last modified date, skip the upload return emptyArFSResult; @@ -576,7 +439,7 @@ export class ArDrive extends ArDriveAnonymous { const existingFileId = conflictingFileName?.fileId; const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload( - wrappedFile.fileStats.size, + new ByteCount(wrappedFile.fileStats.size), this.stubPublicFileMetadata(wrappedFile, destinationFileName), 'public' ); @@ -593,9 +456,9 @@ export class ArDrive extends ArDriveAnonymous { existingFileId }); - const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( - uploadBaseCosts.communityWinstonTip - ); + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({ + communityWinstonTip: uploadBaseCosts.communityWinstonTip + }); return Promise.resolve({ created: [ @@ -608,9 +471,9 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [tipData], fees: { - [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, - [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, - [tipData.txId]: +communityTipTrxReward + [`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward, + [`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward, + [`${tipData.txId}`]: communityTipTrxReward } }); } @@ -669,7 +532,6 @@ export class ArDrive extends ArDriveAnonymous { delete fileOrFolderMetaData.dataTxId; delete fileOrFolderMetaData.dataContentType; } - delete fileOrFolderMetaData.syncStatus; }); // TURN SORTED CHILDREN INTO MANIFEST @@ -682,7 +544,7 @@ export class ArDrive extends ArDriveAnonymous { // Replace spaces with underscores for sharing links .replace(/ /g, '_'); - pathMap[path] = { id: child.dataTxId }; + pathMap[path] = { id: `${child.dataTxId}` }; } }); @@ -716,9 +578,9 @@ export class ArDrive extends ArDriveAnonymous { existingFileId }); - const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( - uploadBaseCosts.communityWinstonTip - ); + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({ + communityWinstonTip: uploadBaseCosts.communityWinstonTip + }); const fileLinks = Object.keys(arweaveManifest.manifest.paths).map( (path) => `arweave.net/${uploadFileResult.dataTrxId}/${path}` @@ -735,9 +597,9 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [tipData], fees: { - [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, - [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, - [tipData.txId]: +communityTipTrxReward + [`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward, + [`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward, + [`${tipData.txId}`]: communityTipTrxReward }, links: [`arweave.net/${uploadFileResult.dataTrxId}`, ...fileLinks] }); @@ -786,18 +648,18 @@ export class ArDrive extends ArDriveAnonymous { conflictResolution }); - if (+bulkEstimation.communityWinstonTip > 0) { + if (bulkEstimation.communityWinstonTip.isGreaterThan(W(0))) { // Send community tip only if communityWinstonTip has a value // This can be zero when a user uses this method to upload empty folders - const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( - bulkEstimation.communityWinstonTip - ); + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({ + communityWinstonTip: bulkEstimation.communityWinstonTip + }); return Promise.resolve({ created: results.entityResults, tips: [tipData], - fees: { ...results.feeResults, [tipData.txId]: +communityTipTrxReward } + fees: { ...results.feeResults, [`${tipData.txId}`]: communityTipTrxReward } }); } @@ -850,7 +712,7 @@ export class ArDrive extends ArDriveAnonymous { const { metaDataTrxId, folderId: newFolderId, metaDataTrxReward } = createFolderResult; // Capture parent folder results - uploadEntityFees = { [metaDataTrxId]: +metaDataTrxReward }; + uploadEntityFees = { [`${metaDataTrxId}`]: metaDataTrxReward }; uploadEntityResults = [ { type: 'folder', @@ -898,8 +760,8 @@ export class ArDrive extends ArDriveAnonymous { // Capture all file results uploadEntityFees = { ...uploadEntityFees, - [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, - [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward + [`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward, + [`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward }; uploadEntityResults = [ ...uploadEntityResults, @@ -939,17 +801,14 @@ export class ArDrive extends ArDriveAnonymous { /** Computes the size of a private file encrypted with AES256-GCM */ encryptedDataSize(dataSize: ByteCount): ByteCount { - if (dataSize < 0 || !Number.isInteger(dataSize)) { - throw new Error(`dataSize must be non-negative, integer value! ${dataSize} is invalid!`); - } - if (dataSize > Number.MAX_SAFE_INTEGER - 16) { + // TODO: Refactor to utils? + if (+dataSize > Number.MAX_SAFE_INTEGER - 16) { throw new Error(`Max un-encrypted dataSize allowed is ${Number.MAX_SAFE_INTEGER - 16}!`); } - - return (dataSize / 16 + 1) * 16; + return new ByteCount((+dataSize / 16 + 1) * 16); } - async uploadPrivateFile({ + public async uploadPrivateFile({ parentFolderId, wrappedFile, driveKey, @@ -986,7 +845,7 @@ export class ArDrive extends ArDriveAnonymous { if ( conflictResolution === upsertOnConflicts && - conflictingFileName.lastModifiedDate === wrappedFile.lastModifiedDate + conflictingFileName.lastModifiedDate.valueOf() === wrappedFile.lastModifiedDate.valueOf() ) { // These files have the same name and last modified date, skip the upload return emptyArFSResult; @@ -1000,7 +859,7 @@ export class ArDrive extends ArDriveAnonymous { const existingFileId = conflictingFileName?.fileId; const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload( - wrappedFile.fileStats.size, + new ByteCount(wrappedFile.fileStats.size), await this.stubPrivateFileMetadata(wrappedFile, destinationFileName), 'private' ); @@ -1027,9 +886,9 @@ export class ArDrive extends ArDriveAnonymous { existingFileId }); - const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( - uploadBaseCosts.communityWinstonTip - ); + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({ + communityWinstonTip: uploadBaseCosts.communityWinstonTip + }); return Promise.resolve({ created: [ @@ -1043,9 +902,9 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [tipData], fees: { - [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, - [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward, - [tipData.txId]: +communityTipTrxReward + [`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward, + [`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward, + [`${tipData.txId}`]: communityTipTrxReward } }); } @@ -1103,18 +962,18 @@ export class ArDrive extends ArDriveAnonymous { conflictResolution }); - if (+bulkEstimation.communityWinstonTip > 0) { + if (bulkEstimation.communityWinstonTip.isGreaterThan(W(0))) { // Send community tip only if communityWinstonTip has a value // This can be zero when a user uses this method to upload empty folders - const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip( - bulkEstimation.communityWinstonTip - ); + const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({ + communityWinstonTip: bulkEstimation.communityWinstonTip + }); return Promise.resolve({ created: results.entityResults, tips: [tipData], - fees: { ...results.feeResults, [tipData.txId]: +communityTipTrxReward } + fees: { ...results.feeResults, [`${tipData.txId}`]: communityTipTrxReward } }); } @@ -1184,7 +1043,7 @@ export class ArDrive extends ArDriveAnonymous { const { metaDataTrxId, folderId: newFolderId, metaDataTrxReward } = createFolderResult; // Capture parent folder results - uploadEntityFees = { [metaDataTrxId]: +metaDataTrxReward }; + uploadEntityFees = { [`${metaDataTrxId}`]: metaDataTrxReward }; uploadEntityResults = [ { type: 'folder', @@ -1233,8 +1092,8 @@ export class ArDrive extends ArDriveAnonymous { // Capture all file results uploadEntityFees = { ...uploadEntityFees, - [uploadFileResult.dataTrxId]: +uploadFileResult.dataTrxReward, - [uploadFileResult.metaDataTrxId]: +uploadFileResult.metaDataTrxReward + [`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward, + [`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward }; uploadEntityResults = [ ...uploadEntityResults, @@ -1274,7 +1133,11 @@ export class ArDrive extends ArDriveAnonymous { }; } - async createPublicFolder({ folderName, driveId, parentFolderId }: CreatePublicFolderParams): Promise { + public async createPublicFolder({ + folderName, + driveId, + parentFolderId + }: CreatePublicFolderParams): Promise { const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId); await this.assertOwnerAddress(owner); @@ -1309,12 +1172,12 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [metaDataTrxId]: +metaDataTrxReward + [`${metaDataTrxId}`]: metaDataTrxReward } }); } - async createPrivateFolder({ + public async createPrivateFolder({ folderName, driveId, driveKey, @@ -1356,12 +1219,12 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [metaDataTrxId]: +metaDataTrxReward + [`${metaDataTrxId}`]: metaDataTrxReward } }); } - async createPublicDrive(driveName: string): Promise { + public async createPublicDrive({ driveName }: CreatePublicDriveParams): Promise { // Assert that there's enough AR available in the wallet // Use stub data to estimate costs since actual data requires entity IDs generated by ArFSDao const stubRootFolderData = new ArFSPublicFolderTransactionData(driveName); @@ -1397,13 +1260,13 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [createDriveResult.metaDataTrxId]: +createDriveResult.metaDataTrxReward, - [createDriveResult.rootFolderTrxId]: +createDriveResult.rootFolderTrxReward + [`${createDriveResult.metaDataTrxId}`]: createDriveResult.metaDataTrxReward, + [`${createDriveResult.rootFolderTrxId}`]: createDriveResult.rootFolderTrxReward } }); } - async createPrivateDrive(driveName: string, newPrivateDriveData: PrivateDriveKeyData): Promise { + public async createPrivateDrive({ driveName, newPrivateDriveData }: CreatePrivateDriveParams): Promise { // Assert that there's enough AR available in the wallet const stubRootFolderData = await ArFSPrivateFolderTransactionData.from(driveName, newPrivateDriveData.driveKey); const stubDriveData = await ArFSPrivateDriveTransactionData.from( @@ -1447,8 +1310,8 @@ export class ArDrive extends ArDriveAnonymous { ], tips: [], fees: { - [createDriveResult.metaDataTrxId]: +createDriveResult.metaDataTrxReward, - [createDriveResult.rootFolderTrxId]: +createDriveResult.rootFolderTrxReward + [`${createDriveResult.metaDataTrxId}`]: createDriveResult.metaDataTrxReward, + [`${createDriveResult.rootFolderTrxId}`]: createDriveResult.rootFolderTrxReward } }); } @@ -1472,12 +1335,12 @@ export class ArDrive extends ArDriveAnonymous { driveKey?: DriveKey, isParentFolder = true ): Promise<{ totalPrice: Winston; totalFilePrice: Winston; communityWinstonTip: Winston }> { - let totalPrice = 0; - let totalFilePrice = 0; + let totalPrice = W(0); + let totalFilePrice = W(0); if (folderToUpload.existingFileAtDestConflict) { // Return an empty estimation, folders CANNOT overwrite files - return { totalPrice: '0', totalFilePrice: '0', communityWinstonTip: '0' }; + return { totalPrice: W('0'), totalFilePrice: W('0'), communityWinstonTip: W('0') }; } // Don't estimate cost of folder metadata if using existing folder @@ -1493,12 +1356,12 @@ export class ArDrive extends ArDriveAnonymous { const metaDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount( folderMetadataTrxData.sizeOf() ); - const parentFolderWinstonPrice = metaDataBaseReward.toString(); + const parentFolderWinstonPrice = metaDataBaseReward; // Assign base costs to folder folderToUpload.baseCosts = { metaDataBaseReward: parentFolderWinstonPrice }; - totalPrice += +parentFolderWinstonPrice; + totalPrice = totalPrice.plus(parentFolderWinstonPrice); } for await (const file of folderToUpload.files) { @@ -1511,7 +1374,7 @@ export class ArDrive extends ArDriveAnonymous { continue; } - const fileSize = driveKey ? file.encryptedDataSize() : file.fileStats.size; + const fileSize = driveKey ? file.encryptedDataSize() : new ByteCount(file.fileStats.size); const fileDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(fileSize); @@ -1522,15 +1385,15 @@ export class ArDrive extends ArDriveAnonymous { stubFileMetaData.sizeOf() ); - totalPrice += fileDataBaseReward; - totalPrice += metaDataBaseReward; + totalPrice = totalPrice.plus(fileDataBaseReward); + totalPrice = totalPrice.plus(metaDataBaseReward); - totalFilePrice += fileDataBaseReward; + totalFilePrice = totalFilePrice.plus(fileDataBaseReward); // Assign base costs to the file file.baseCosts = { - fileDataBaseReward: fileDataBaseReward.toString(), - metaDataBaseReward: metaDataBaseReward.toString() + fileDataBaseReward: fileDataBaseReward, + metaDataBaseReward: metaDataBaseReward }; } @@ -1542,22 +1405,22 @@ export class ArDrive extends ArDriveAnonymous { false ); - totalPrice += +childFolderResults.totalPrice; - totalFilePrice += +childFolderResults.totalFilePrice; + totalPrice = totalPrice.plus(childFolderResults.totalPrice); + totalFilePrice = totalFilePrice.plus(childFolderResults.totalFilePrice); } - const totalWinstonPrice = totalPrice.toString(); - let communityWinstonTip = '0'; + const totalWinstonPrice = totalPrice; + let communityWinstonTip = W(0); if (isParentFolder) { - if (totalFilePrice > 0) { - communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(String(totalFilePrice)); + if (totalFilePrice.isGreaterThan(W(0))) { + communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(totalFilePrice); } // Check and assert balance of the total bulk upload if this folder is the parent folder const walletHasBalance = await this.walletDao.walletHasBalance( this.wallet, - String(+communityWinstonTip + +totalWinstonPrice) + communityWinstonTip.plus(totalWinstonPrice) ); if (!walletHasBalance) { @@ -1571,16 +1434,20 @@ export class ArDrive extends ArDriveAnonymous { } } - return { totalPrice: String(totalPrice), totalFilePrice: String(totalFilePrice), communityWinstonTip }; + return { + totalPrice, + totalFilePrice, + communityWinstonTip + }; } async assertOwnerAddress(owner: ArweaveAddress): Promise { - if (!owner.equalsAddress(await this.wallet.getAddress())) { + if (!owner.equals(await this.wallet.getAddress())) { throw new Error('Supplied wallet is not the owner of this drive!'); } } - async getPrivateDrive(driveId: DriveID, driveKey: DriveKey, owner?: ArweaveAddress): Promise { + public async getPrivateDrive({ driveId, driveKey, owner }: GetPrivateDriveParams): Promise { if (!owner) { owner = await this.getOwnerForDriveId(driveId); } @@ -1589,7 +1456,7 @@ export class ArDrive extends ArDriveAnonymous { return this.arFsDao.getPrivateDrive(driveId, driveKey, owner); } - async getPrivateFolder(folderId: FolderID, driveKey: DriveKey, owner?: ArweaveAddress): Promise { + public async getPrivateFolder({ folderId, driveKey, owner }: GetPrivateFolderParams): Promise { if (!owner) { owner = await this.arFsDao.getDriveOwnerForFolderId(folderId); } @@ -1598,7 +1465,7 @@ export class ArDrive extends ArDriveAnonymous { return this.arFsDao.getPrivateFolder(folderId, driveKey, owner); } - async getPrivateFile(fileId: FileID, driveKey: DriveKey, owner?: ArweaveAddress): Promise { + public async getPrivateFile({ fileId, driveKey, owner }: GetPrivateFileParams): Promise { if (!owner) { owner = await this.arFsDao.getDriveOwnerForFileId(fileId); } @@ -1612,7 +1479,7 @@ export class ArDrive extends ArDriveAnonymous { * @param {FolderID} folderId the folder ID to list children of * @returns {ArFSPrivateFileOrFolderWithPaths[]} an array representation of the children and parent folder */ - async listPrivateFolder({ + public async listPrivateFolder({ folderId, driveKey, maxDepth = 0, @@ -1631,10 +1498,9 @@ export class ArDrive extends ArDriveAnonymous { async estimateAndAssertCostOfMoveFile( fileTransactionData: ArFSFileMetadataTransactionData ): Promise { - const fileMetaTransactionDataReward = String( - await this.priceEstimator.getBaseWinstonPriceForByteCount(fileTransactionData.sizeOf()) + const fileMetaTransactionDataReward = await this.priceEstimator.getBaseWinstonPriceForByteCount( + fileTransactionData.sizeOf() ); - const walletHasBalance = await this.walletDao.walletHasBalance(this.wallet, fileMetaTransactionDataReward); if (!walletHasBalance) { @@ -1653,30 +1519,26 @@ export class ArDrive extends ArDriveAnonymous { metaData: ArFSObjectTransactionData, drivePrivacy: DrivePrivacy ): Promise { - if (decryptedFileSize < 0 || !Number.isInteger(decryptedFileSize)) { - throw new Error('File size should be non-negative integer number!'); - } - let fileSize = decryptedFileSize; if (drivePrivacy === 'private') { fileSize = this.encryptedDataSize(fileSize); } - let totalPrice = 0; - let fileDataBaseReward = 0; - let communityWinstonTip = '0'; + let totalPrice = W(0); + let fileDataBaseReward = W(0); + let communityWinstonTip = W(0); if (fileSize) { fileDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(fileSize); - communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(fileDataBaseReward.toString()); - const tipReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(0); - totalPrice += fileDataBaseReward; - totalPrice += +communityWinstonTip; - totalPrice += tipReward; + communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(fileDataBaseReward); + const tipReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(new ByteCount(0)); + totalPrice = totalPrice.plus(fileDataBaseReward); + totalPrice = totalPrice.plus(communityWinstonTip); + totalPrice = totalPrice.plus(tipReward); } const metaDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(metaData.sizeOf()); - totalPrice += metaDataBaseReward; + totalPrice = totalPrice.plus(metaDataBaseReward); - const totalWinstonPrice = totalPrice.toString(); + const totalWinstonPrice = totalPrice; const walletHasBalance = await this.walletDao.walletHasBalance(this.wallet, totalWinstonPrice); @@ -1689,15 +1551,15 @@ export class ArDrive extends ArDriveAnonymous { } return { - fileDataBaseReward: fileDataBaseReward.toString(), - metaDataBaseReward: metaDataBaseReward.toString(), + fileDataBaseReward: fileDataBaseReward, + metaDataBaseReward: metaDataBaseReward, communityWinstonTip }; } async estimateAndAssertCostOfFolderUpload(metaData: ArFSObjectTransactionData): Promise { const metaDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount(metaData.sizeOf()); - const totalWinstonPrice = metaDataBaseReward.toString(); + const totalWinstonPrice = metaDataBaseReward; const walletHasBalance = await this.walletDao.walletHasBalance(this.wallet, totalWinstonPrice); @@ -1718,17 +1580,17 @@ export class ArDrive extends ArDriveAnonymous { driveMetaData: ArFSDriveTransactionData, rootFolderMetaData: ArFSFolderTransactionData ): Promise { - let totalPrice = 0; + let totalPrice = W(0); const driveMetaDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount( driveMetaData.sizeOf() ); - totalPrice += driveMetaDataBaseReward; + totalPrice = totalPrice.plus(driveMetaDataBaseReward); const rootFolderMetaDataBaseReward = await this.priceEstimator.getBaseWinstonPriceForByteCount( rootFolderMetaData.sizeOf() ); - totalPrice += rootFolderMetaDataBaseReward; + totalPrice = totalPrice.plus(rootFolderMetaDataBaseReward); - const totalWinstonPrice = totalPrice.toString(); + const totalWinstonPrice = totalPrice; const walletHasBalance = await this.walletDao.walletHasBalance(this.wallet, totalWinstonPrice); @@ -1741,16 +1603,16 @@ export class ArDrive extends ArDriveAnonymous { } return { - driveMetaDataBaseReward: driveMetaDataBaseReward.toString(), - rootFolderMetaDataBaseReward: rootFolderMetaDataBaseReward.toString() + driveMetaDataBaseReward, + rootFolderMetaDataBaseReward }; } - async getDriveIdForFileId(fileId: FileID): Promise { + public async getDriveIdForFileId(fileId: FileID): Promise { return this.arFsDao.getDriveIdForFileId(fileId); } - async getDriveIdForFolderId(folderId: FolderID): Promise { + public async getDriveIdForFolderId(folderId: FolderID): Promise { return this.arFsDao.getDriveIdForFolderId(folderId); } @@ -1786,7 +1648,7 @@ export class ArDrive extends ArDriveAnonymous { stubEntityID, await deriveDriveKey( 'stubPassword', - stubEntityID, + `${stubEntityID}`, JSON.stringify((this.wallet as JWKWallet).getPrivateKey()) ) ); diff --git a/src/ardrive.types.ts b/src/ardrive.types.ts new file mode 100644 index 00000000..15b8d2c0 --- /dev/null +++ b/src/ardrive.types.ts @@ -0,0 +1,161 @@ +import { PrivateDriveKeyData } from './arfsdao'; +import { ArFSListPublicFolderParams } from './arfsdao_anonymous'; +import { WithDriveKey } from './arfs_entity_result_factory'; +import { ArFSFolderToUpload, ArFSFileToUpload } from './arfs_file_wrapper'; +import { PrivateKeyData } from './private_key_data'; +import { TransactionID, AnyEntityID, MakeOptional, ArweaveAddress, Winston, FolderID, DriveID, FileID } from './types'; + +export type ArFSEntityDataType = 'drive' | 'folder' | 'file'; + +export interface ArFSEntityData { + type: ArFSEntityDataType; + metadataTxId: TransactionID; + dataTxId?: TransactionID; + entityId: AnyEntityID; + key?: string; +} + +export type ListPublicFolderParams = MakeOptional; +export type ListPrivateFolderParams = ListPublicFolderParams & WithDriveKey; + +export interface TipData { + txId: TransactionID; + recipient: ArweaveAddress; + winston: Winston; +} + +export interface TipResult { + tipData: TipData; + reward: Winston; +} + +export type ArFSFees = { [key: string]: Winston }; + +export interface ArFSResult { + created: ArFSEntityData[]; + tips: TipData[]; + fees: ArFSFees; +} + +export interface ArFSManifestResult extends ArFSResult { + links: string[]; +} + +export const emptyArFSResult: ArFSResult = { + created: [], + tips: [], + fees: {} +}; + +export interface UploadPublicManifestParams { + driveId?: DriveID; + folderId?: FolderID; + maxDepth?: number; + destManifestName?: string; +} + +export interface MetaDataBaseCosts { + metaDataBaseReward: Winston; +} + +export interface BulkFileBaseCosts extends MetaDataBaseCosts { + fileDataBaseReward: Winston; +} +export interface FileUploadBaseCosts extends BulkFileBaseCosts { + communityWinstonTip: Winston; +} + +export interface DriveUploadBaseCosts { + driveMetaDataBaseReward: Winston; + rootFolderMetaDataBaseReward: Winston; +} + +export interface RecursivePublicBulkUploadParams { + parentFolderId: FolderID; + wrappedFolder: ArFSFolderToUpload; + driveId: DriveID; + owner: ArweaveAddress; + conflictResolution: FileNameConflictResolution; +} +export type RecursivePrivateBulkUploadParams = RecursivePublicBulkUploadParams & WithDriveKey; + +export interface CreatePublicFolderParams { + folderName: string; + driveId: DriveID; + parentFolderId: FolderID; +} +export type CreatePrivateFolderParams = CreatePublicFolderParams & WithDriveKey; + +export const skipOnConflicts = 'skip'; +export const replaceOnConflicts = 'replace'; +export const upsertOnConflicts = 'upsert'; +// export const askOnConflicts = 'ask'; + +export type FileNameConflictResolution = typeof skipOnConflicts | typeof replaceOnConflicts | typeof upsertOnConflicts; +// | typeof askOnConflicts; + +export interface UploadParams { + parentFolderId: FolderID; + conflictResolution?: FileNameConflictResolution; +} + +export interface BulkPublicUploadParams extends UploadParams { + wrappedFolder: ArFSFolderToUpload; + destParentFolderName?: string; +} +export type BulkPrivateUploadParams = BulkPublicUploadParams & WithDriveKey; + +export interface UploadPublicFileParams extends UploadParams { + wrappedFile: ArFSFileToUpload; + destinationFileName?: string; +} +export type UploadPrivateFileParams = UploadPublicFileParams & WithDriveKey; + +export interface CommunityTipParams { + communityWinstonTip: Winston; + assertBalance?: boolean; +} + +interface MoveParams { + newParentFolderId: FolderID; +} + +export interface MovePublicFileParams extends MoveParams { + fileId: FileID; +} +export type MovePrivateFileParams = MovePublicFileParams & WithDriveKey; + +export interface MovePublicFolderParams extends MoveParams { + folderId: FolderID; +} +export type MovePrivateFolderParams = MovePublicFolderParams & WithDriveKey; + +export interface CreatePublicDriveParams { + driveName: string; +} +export interface CreatePrivateDriveParams extends CreatePublicDriveParams { + newPrivateDriveData: PrivateDriveKeyData; +} + +interface GetEntityParams { + owner?: ArweaveAddress; +} +export interface GetPublicDriveParams extends GetEntityParams { + driveId: DriveID; +} +export type GetPrivateDriveParams = GetPublicDriveParams & WithDriveKey; + +export interface GetPublicFolderParams extends GetEntityParams { + folderId: FolderID; +} +export type GetPrivateFolderParams = GetPublicFolderParams & WithDriveKey; + +export interface GetPublicFileParams extends GetEntityParams { + fileId: FileID; +} +export type GetPrivateFileParams = GetPublicFileParams & WithDriveKey; + +export interface GetAllDrivesForAddressParams { + address: ArweaveAddress; + privateKeyData: PrivateKeyData; +} diff --git a/src/ardrive_anonymous.ts b/src/ardrive_anonymous.ts new file mode 100644 index 00000000..2e711478 --- /dev/null +++ b/src/ardrive_anonymous.ts @@ -0,0 +1,80 @@ +import { + GetPublicDriveParams, + GetPublicFolderParams, + GetPublicFileParams, + GetAllDrivesForAddressParams, + ListPublicFolderParams +} from './ardrive.types'; +import { ArFSDAOAnonymous, ArFSDAOType } from './arfsdao_anonymous'; +import { + ArFSDriveEntity, + ArFSPublicDrive, + ArFSPublicFile, + ArFSPublicFileOrFolderWithPaths, + ArFSPublicFolder +} from './arfs_entities'; +import { ArweaveAddress, DriveID } from './types'; + +export abstract class ArDriveType { + protected abstract readonly arFsDao: ArFSDAOType; +} + +export class ArDriveAnonymous extends ArDriveType { + constructor(protected readonly arFsDao: ArFSDAOAnonymous) { + super(); + } + + public async getOwnerForDriveId(driveId: DriveID): Promise { + return this.arFsDao.getOwnerForDriveId(driveId); + } + + public async getPublicDrive({ driveId, owner }: GetPublicDriveParams): Promise { + if (!owner) { + owner = await this.getOwnerForDriveId(driveId); + } + + return this.arFsDao.getPublicDrive(driveId, owner); + } + + public async getPublicFolder({ folderId, owner }: GetPublicFolderParams): Promise { + if (!owner) { + owner = await this.arFsDao.getDriveOwnerForFolderId(folderId); + } + + return this.arFsDao.getPublicFolder(folderId, owner); + } + + public async getPublicFile({ fileId, owner }: GetPublicFileParams): Promise { + if (!owner) { + owner = await this.arFsDao.getDriveOwnerForFileId(fileId); + } + + return this.arFsDao.getPublicFile(fileId, owner); + } + + public async getAllDrivesForAddress({ + address, + privateKeyData + }: GetAllDrivesForAddressParams): Promise { + return this.arFsDao.getAllDrivesForAddress(address, privateKeyData); + } + + /** + * Lists the children of certain public folder + * @param {FolderID} folderId the folder ID to list children of + * @returns {ArFSPublicFileOrFolderWithPaths[]} an array representation of the children and parent folder + */ + public async listPublicFolder({ + folderId, + maxDepth = 0, + includeRoot = false, + owner + }: ListPublicFolderParams): Promise { + if (!owner) { + owner = await this.arFsDao.getDriveOwnerForFolderId(folderId); + } + + const children = await this.arFsDao.listPublicFolder({ folderId, maxDepth, includeRoot, owner }); + return children; + } +} diff --git a/src/arfs_entities.ts b/src/arfs_entities.ts index 85c9e430..cc492273 100644 --- a/src/arfs_entities.ts +++ b/src/arfs_entities.ts @@ -1,26 +1,64 @@ -import { - ArFSDriveEntity, - ArFSEntity, - ArFSFileFolderEntity, - ContentType, - DriveAuthMode, - DrivePrivacy, - EntityType -} from 'ardrive-core-js'; +import { ContentType, DriveAuthMode, DrivePrivacy, EntityType } from 'ardrive-core-js'; import { FolderHierarchy } from './folderHierarchy'; import { - ByteCount, CipherIV, DataContentType, DriveID, - EntityID, + AnyEntityID, FileID, FolderID, + ByteCount, JSON_CONTENT_TYPE, TransactionID, - UnixTime + UnixTime, + stubTransactionID } from './types'; -import { stubTransactionID } from './utils/stubs'; + +// The primary ArFS entity that all other entities inherit from. +export class ArFSEntity { + appName: string; // The app that has submitted this entity. Should not be longer than 64 characters. eg. ArDrive-Web + appVersion: string; // The app version that has submitted this entity. Must not be longer than 8 digits, numbers only. eg. 0.1.14 + arFS: string; // The version of Arweave File System that is used for this entity. Must not be longer than 4 digits. eg 0.11 + contentType: string; // the mime type of the file uploaded. in the case of drives and folders, it is always a JSON file. Public drive/folders must use "application/json" and priate drives use "application/octet-stream" since this data is encrypted. + driveId: DriveID; // the unique drive identifier, created with uuidv4 https://www.npmjs.com/package/uuidv4 eg. 41800747-a852-4dc9-9078-6c20f85c0f3a + entityType: string; // the type of ArFS entity this is. this can only be set to "drive", "folder", "file" + name: string; // user defined entity name, cannot be longer than 64 characters. This is stored in the JSON file that is uploaded along with the drive/folder/file metadata transaction + txId: TransactionID; // the arweave transaction id for this entity. 43 numbers/letters eg. 1xRhN90Mu5mEgyyrmnzKgZP0y3aK8AwSucwlCOAwsaI + unixTime: UnixTime; // seconds since unix epoch, taken at the time of upload, 10 numbers eg. 1620068042 + + constructor( + appName: string, + appVersion: string, + arFS: string, + contentType: string, + driveId: DriveID, + entityType: string, + name: string, + txId: TransactionID, + unixTime: UnixTime + ) { + this.appName = appName; + this.appVersion = appVersion; + this.arFS = arFS; + this.contentType = contentType; + this.driveId = driveId; + this.entityType = entityType; + this.name = name; + this.txId = txId; + this.unixTime = unixTime; + } +} + +export const ENCRYPTED_DATA_PLACEHOLDER = 'ENCRYPTED'; +export type ENCRYPTED_DATA_PLACEHOLDER_TYPE = 'ENCRYPTED'; + +// A Drive is a logical grouping of folders and files. All folders and files must be part of a drive, and reference the Drive ID. +// When creating a Drive, a corresponding folder must be created as well. This folder will act as the Drive Root Folder. +// This seperation of drive and folder entity enables features such as folder view queries. +export interface ArFSDriveEntity extends ArFSEntity { + drivePrivacy: string; // identifies if this drive is public or private (and encrypted) can only be "public" or "private" + rootFolderId: FolderID | ENCRYPTED_DATA_PLACEHOLDER_TYPE; // the uuid of the related drive root folder, stored in the JSON data that is uploaded with each Drive Entity metadata transaction +} export class ArFSPublicDrive extends ArFSEntity implements ArFSDriveEntity { constructor( @@ -36,7 +74,7 @@ export class ArFSPublicDrive extends ArFSEntity implements ArFSDriveEntity { readonly drivePrivacy: DrivePrivacy, readonly rootFolderId: FolderID ) { - super(appName, appVersion, arFS, contentType, driveId, entityType, name, 0, txId, unixTime); + super(appName, appVersion, arFS, contentType, driveId, entityType, name, txId, unixTime); } } @@ -57,10 +95,22 @@ export class ArFSPrivateDrive extends ArFSEntity implements ArFSDriveEntity { readonly cipher: string, readonly cipherIV: CipherIV ) { - super(appName, appVersion, arFS, contentType, driveId, entityType, name, 0, txId, unixTime); + super(appName, appVersion, arFS, contentType, driveId, entityType, name, txId, unixTime); } } +// A Folder is a logical group of folders and files. It contains a parent folder ID used to reference where this folder lives in the Drive hierarchy. +// Drive Root Folders must not have a parent folder ID, as they sit at the root of a drive. +// A File contains actual data, like a photo, document or movie. +// The File metadata transaction JSON references the File data transaction for retrieval. +// This separation allows for file metadata to be updated without requiring the file data to be reuploaded. +// Files and Folders leverage the same entity type since they have the same properties +export interface ArFSFileFolderEntity extends ArFSEntity { + parentFolderId: FolderID; // the uuid of the parent folder that this entity sits within. Folder Entities used for the drive root must not have a parent folder ID, eg. 41800747-a852-4dc9-9078-6c20f85c0f3a + entityId: FileID | FolderID; // the unique file or folder identifier, created with uuidv4 https://www.npmjs.com/package/uuidv4 eg. 41800747-a852-4dc9-9078-6c20f85c0f3a + lastModifiedDate: UnixTime; // the last modified date of the file or folder as seconds since unix epoch +} + export class ArFSFileOrFolderEntity extends ArFSEntity implements ArFSFileFolderEntity { folderId?: FolderID; @@ -79,9 +129,9 @@ export class ArFSFileOrFolderEntity extends ArFSEntity implements ArFSFileFolder public dataTxId: TransactionID, public dataContentType: DataContentType, readonly parentFolderId: FolderID, - readonly entityId: EntityID + readonly entityId: AnyEntityID ) { - super(appName, appVersion, arFS, contentType, driveId, entityType, name, lastModifiedDate, txId, unixTime); + super(appName, appVersion, arFS, contentType, driveId, entityType, name, txId, unixTime); } } @@ -253,10 +303,10 @@ export class ArFSPublicFolder extends ArFSFileOrFolderEntity { driveId, entityType, name, - 0, + new ByteCount(0), txId, unixTime, - 0, + new UnixTime(0), stubTransactionID, JSON_CONTENT_TYPE, parentFolderId, @@ -288,10 +338,10 @@ export class ArFSPrivateFolder extends ArFSFileOrFolderEntity { driveId, entityType, name, - 0, + new ByteCount(0), txId, unixTime, - 0, + new UnixTime(0), stubTransactionID, JSON_CONTENT_TYPE, parentFolderId, diff --git a/src/arfs_entity_result_factory.ts b/src/arfs_entity_result_factory.ts index 6bcb169c..46f656ed 100644 --- a/src/arfs_entity_result_factory.ts +++ b/src/arfs_entity_result_factory.ts @@ -1,5 +1,5 @@ import { ArFSFileMetadataTransactionData } from './arfs_trx_data_types'; -import { TransactionID, Winston, DriveID, FolderID, FileID, FileKey, DriveKey } from './types'; +import { DriveID, FolderID, FileID, FileKey, DriveKey, TransactionID, Winston } from './types'; export interface ArFSWriteResult { metaDataTrxId: TransactionID; diff --git a/src/arfs_file_wrapper.ts b/src/arfs_file_wrapper.ts index a6ff80da..65384b9d 100644 --- a/src/arfs_file_wrapper.ts +++ b/src/arfs_file_wrapper.ts @@ -2,8 +2,8 @@ import * as fs from 'fs'; import { extToMime } from 'ardrive-core-js'; import { basename, join } from 'path'; import { ByteCount, DataContentType, FileID, FolderID, Manifest, MANIFEST_CONTENT_TYPE, UnixTime } from './types'; -import { BulkFileBaseCosts, MetaDataBaseCosts } from './ardrive'; import { EntityNamesAndIds } from './utils/mapper_functions'; +import { BulkFileBaseCosts, MetaDataBaseCosts } from './ardrive.types'; type BaseFileName = string; type FilePath = string; @@ -14,7 +14,7 @@ type FilePath = string; * Public : 2147483647 bytes * Private: 2147483646 bytes */ -const maxFileSize: ByteCount = 2147483646; +const maxFileSize = new ByteCount(2147483646); export interface FileInfo { dataContentType: DataContentType; @@ -64,7 +64,7 @@ export class ArFSManifestToUpload implements ArFSEntityToUpload { public gatherFileInfo(): FileInfo { const dataContentType = MANIFEST_CONTENT_TYPE; - const lastModifiedDateMS = Math.round(Date.now() / 1000); // new unix time + const lastModifiedDateMS = new UnixTime(Math.round(Date.now() / 1000)); // new unix time return { dataContentType, lastModifiedDateMS, fileSize: this.size }; } @@ -78,13 +78,13 @@ export class ArFSManifestToUpload implements ArFSEntityToUpload { } public get size(): ByteCount { - return Buffer.byteLength(JSON.stringify(this.manifest)); + return new ByteCount(Buffer.byteLength(JSON.stringify(this.manifest))); } } export class ArFSFileToUpload implements ArFSEntityToUpload { constructor(public readonly filePath: FilePath, public readonly fileStats: fs.Stats) { - if (this.fileStats.size >= maxFileSize) { + if (+this.fileStats.size >= +maxFileSize) { throw new Error(`Files greater than "${maxFileSize}" bytes are not yet supported!`); } } @@ -95,15 +95,19 @@ export class ArFSFileToUpload implements ArFSEntityToUpload { hasSameLastModifiedDate = false; public gatherFileInfo(): FileInfo { - const dataContentType = this.getContentType(); + const dataContentType = this.contentType; const lastModifiedDateMS = this.lastModifiedDate; - const fileSize = this.fileStats.size; + const fileSize = this.size; return { dataContentType, lastModifiedDateMS, fileSize }; } + public get size(): ByteCount { + return new ByteCount(this.fileStats.size); + } + public get lastModifiedDate(): UnixTime { - return Math.floor(this.fileStats.mtimeMs); + return new UnixTime(Math.floor(this.fileStats.mtimeMs)); } public getBaseCosts(): BulkFileBaseCosts { @@ -117,7 +121,7 @@ export class ArFSFileToUpload implements ArFSEntityToUpload { return fs.readFileSync(this.filePath); } - public getContentType(): DataContentType { + public get contentType(): DataContentType { return extToMime(this.filePath); } @@ -127,7 +131,7 @@ export class ArFSFileToUpload implements ArFSEntityToUpload { /** Computes the size of a private file encrypted with AES256-GCM */ public encryptedDataSize(): ByteCount { - return (this.fileStats.size / 16 + 1) * 16; + return new ByteCount((this.fileStats.size / 16 + 1) * 16); } } @@ -192,7 +196,7 @@ export class ArFSFolderToUpload { if (existingFileAtDestConflict) { file.existingId = existingFileAtDestConflict.fileId; - if (existingFileAtDestConflict.lastModifiedDate === file.lastModifiedDate) { + if (existingFileAtDestConflict.lastModifiedDate.valueOf() === file.lastModifiedDate.valueOf()) { // Check last modified date and set to true to resolve upsert conditional file.hasSameLastModifiedDate = true; } @@ -242,12 +246,12 @@ export class ArFSFolderToUpload { let totalByteCount = 0; for (const file of this.files) { - totalByteCount += encrypted ? file.encryptedDataSize() : file.fileStats.size; + totalByteCount += encrypted ? +file.encryptedDataSize() : file.fileStats.size; } for (const folder of this.folders) { - totalByteCount += folder.getTotalByteCount(encrypted); + totalByteCount += +folder.getTotalByteCount(encrypted); } - return totalByteCount; + return new ByteCount(totalByteCount); } } diff --git a/src/arfs_meta_data_factory.ts b/src/arfs_meta_data_factory.ts index cc634099..e2b92d55 100644 --- a/src/arfs_meta_data_factory.ts +++ b/src/arfs_meta_data_factory.ts @@ -7,7 +7,7 @@ import { } from './arfs_prototypes'; import { ArFSFileMetadataTransactionData } from './arfs_trx_data_types'; -import { ByteCount, DataContentType, DriveID, FileID, FolderID, TransactionID, UnixTime } from './types'; +import { DataContentType, DriveID, FileID, FolderID, ByteCount, TransactionID, UnixTime } from './types'; export type MoveEntityMetaDataFactory = () => ArFSEntityMetaDataPrototype; diff --git a/src/arfs_prototypes.ts b/src/arfs_prototypes.ts index acb59431..1676133d 100644 --- a/src/arfs_prototypes.ts +++ b/src/arfs_prototypes.ts @@ -39,7 +39,7 @@ export abstract class ArFSEntityMetaDataPrototype extends ArFSObjectMetadataProt super(); // Get the current time so the app can display the "created" data later on - this.unixTime = Math.round(Date.now() / 1000); + this.unixTime = new UnixTime(Math.round(Date.now() / 1000)); } } @@ -57,7 +57,7 @@ export abstract class ArFSDriveMetaDataPrototype extends ArFSEntityMetaDataProto transaction.addTag('Content-Type', this.contentType); transaction.addTag('Entity-Type', 'drive'); transaction.addTag('Unix-Time', this.unixTime.toString()); - transaction.addTag('Drive-Id', this.driveId); + transaction.addTag('Drive-Id', `${this.driveId}`); transaction.addTag('Drive-Privacy', this.privacy); } } @@ -106,11 +106,11 @@ export abstract class ArFSFolderMetaDataPrototype extends ArFSEntityMetaDataProt transaction.addTag('Content-Type', this.contentType); transaction.addTag('Entity-Type', 'folder'); transaction.addTag('Unix-Time', this.unixTime.toString()); - transaction.addTag('Drive-Id', this.driveId); - transaction.addTag('Folder-Id', this.folderId); + transaction.addTag('Drive-Id', `${this.driveId}`); + transaction.addTag('Folder-Id', `${this.folderId}`); if (this.parentFolderId) { // Root folder transactions do not have Parent-Folder-Id - transaction.addTag('Parent-Folder-Id', this.parentFolderId); + transaction.addTag('Parent-Folder-Id', `${this.parentFolderId}`); } } } @@ -167,9 +167,9 @@ export abstract class ArFSFileMetaDataPrototype extends ArFSEntityMetaDataProtot transaction.addTag('Content-Type', this.contentType); transaction.addTag('Entity-Type', 'file'); transaction.addTag('Unix-Time', this.unixTime.toString()); - transaction.addTag('Drive-Id', this.driveId); - transaction.addTag('File-Id', this.fileId); - transaction.addTag('Parent-Folder-Id', this.parentFolderId); + transaction.addTag('Drive-Id', `${this.driveId}`); + transaction.addTag('File-Id', `${this.fileId}`); + transaction.addTag('Parent-Folder-Id', `${this.parentFolderId}`); } } export class ArFSPublicFileMetaDataPrototype extends ArFSFileMetaDataPrototype { diff --git a/src/arfs_trx_data_types.ts b/src/arfs_trx_data_types.ts index 562324fd..dbb08f03 100644 --- a/src/arfs_trx_data_types.ts +++ b/src/arfs_trx_data_types.ts @@ -7,27 +7,27 @@ import { fileEncrypt } from 'ardrive-core-js'; import { - ByteCount, CipherIV, DataContentType, DriveKey, FileID, FileKey, FolderID, + ByteCount, TransactionID, UnixTime } from './types'; export interface ArFSObjectTransactionData { asTransactionData(): string | Buffer; - sizeOf(): number; + sizeOf(): ByteCount; } export abstract class ArFSDriveTransactionData implements ArFSObjectTransactionData { abstract asTransactionData(): string | Buffer; // TODO: Share repeated sizeOf() function to all classes - sizeOf(): number { - return this.asTransactionData().length; + sizeOf(): ByteCount { + return new ByteCount(this.asTransactionData().length); } } @@ -78,8 +78,8 @@ export class ArFSPrivateDriveTransactionData extends ArFSDriveTransactionData { export abstract class ArFSFolderTransactionData implements ArFSObjectTransactionData { abstract asTransactionData(): string | Buffer; - sizeOf(): number { - return this.asTransactionData().length; + sizeOf(): ByteCount { + return new ByteCount(this.asTransactionData().length); } } @@ -124,8 +124,8 @@ export class ArFSPrivateFolderTransactionData extends ArFSFolderTransactionData export abstract class ArFSFileMetadataTransactionData implements ArFSObjectTransactionData { abstract asTransactionData(): string | Buffer; - sizeOf(): number { - return this.asTransactionData().length; + sizeOf(): ByteCount { + return new ByteCount(this.asTransactionData().length); } } @@ -171,7 +171,7 @@ export class ArFSPrivateFileMetadataTransactionData extends ArFSFileMetadataTran fileId: FileID, driveKey: DriveKey ): Promise { - const fileKey: FileKey = await deriveFileKey(fileId, driveKey); + const fileKey: FileKey = await deriveFileKey(`${fileId}`, driveKey); const { cipher, cipherIV, data }: ArFSEncryptedData = await fileEncrypt( fileKey, Buffer.from( @@ -194,8 +194,8 @@ export class ArFSPrivateFileMetadataTransactionData extends ArFSFileMetadataTran export abstract class ArFSFileDataTransactionData implements ArFSObjectTransactionData { abstract asTransactionData(): string | Buffer; - sizeOf(): number { - return this.asTransactionData().length; + sizeOf(): ByteCount { + return new ByteCount(this.asTransactionData().length); } } export class ArFSPublicFileDataTransactionData extends ArFSFileDataTransactionData { @@ -223,7 +223,7 @@ export class ArFSPrivateFileDataTransactionData extends ArFSFileDataTransactionD fileId: FileID, driveKey: DriveKey ): Promise { - const fileKey: FileKey = await deriveFileKey(fileId, driveKey); + const fileKey: FileKey = await deriveFileKey(`${fileId}`, driveKey); const { cipher, cipherIV, data }: ArFSEncryptedData = await fileEncrypt(fileKey, fileData); return new ArFSPrivateFileDataTransactionData(cipher, cipherIV, data); } diff --git a/src/arfsdao.ts b/src/arfsdao.ts index f7b8f6d0..58455be4 100644 --- a/src/arfsdao.ts +++ b/src/arfsdao.ts @@ -45,17 +45,17 @@ import { DEFAULT_APP_NAME, DEFAULT_APP_VERSION, CURRENT_ARFS_VERSION, - RewardSettings + RewardSettings, + ArweaveAddress, + W, + TxID, + EID } from './types'; import { CreateTransactionInterface } from 'arweave/node/common'; import { ArFSPrivateFileBuilder, ArFSPublicFileBuilder } from './utils/arfs_builders/arfs_file_builders'; import { ArFSPrivateFolderBuilder, ArFSPublicFolderBuilder } from './utils/arfs_builders/arfs_folder_builders'; import { latestRevisionFilter, fileFilter, folderFilter } from './utils/filter_methods'; -import { - ArFSPrivateDriveBuilder, - ENCRYPTED_DATA_PLACEHOLDER, - SafeArFSDriveBuilder -} from './utils/arfs_builders/arfs_drive_builders'; +import { ArFSPrivateDriveBuilder, SafeArFSDriveBuilder } from './utils/arfs_builders/arfs_drive_builders'; import { FolderHierarchy } from './folderHierarchy'; import { CreateDriveMetaDataFactory, @@ -89,19 +89,19 @@ import { ArFSPrivateFolder, ArFSPublicDrive, ArFSPublicFile, - ArFSPublicFolder + ArFSPublicFolder, + ENCRYPTED_DATA_PLACEHOLDER } from './arfs_entities'; import { ArFSAllPublicFoldersOfDriveParams, ArFSDAOAnonymous } from './arfsdao_anonymous'; import { ArFSFileOrFolderBuilder } from './utils/arfs_builders/arfs_builders'; import { PrivateKeyData } from './private_key_data'; -import { ArweaveAddress } from './arweave_address'; import { EntityNamesAndIds, entityToNameMap, fileConflictInfoMap, folderToNameAndIdMap } from './utils/mapper_functions'; -import { ListPrivateFolderParams } from './ardrive'; +import { ListPrivateFolderParams } from './ardrive.types'; export const graphQLURL = 'https://arweave.net/graphql'; @@ -111,7 +111,7 @@ export class PrivateDriveKeyData { static async from(drivePassword: string, privateKey: JWKInterface): Promise { const driveId = uuidv4(); const driveKey = await deriveDriveKey(drivePassword, driveId, JSON.stringify(privateKey)); - return new PrivateDriveKeyData(driveId, driveKey); + return new PrivateDriveKeyData(EID(driveId), driveKey); } } @@ -192,7 +192,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { // Assert that drive ID is consistent with parent folder ID const actualDriveId = await this.getDriveIdForFolderId(parentFolderId); - if (actualDriveId !== driveId) { + if (!actualDriveId.equals(driveId)) { throw new Error( `Drive id: ${driveId} does not match actual drive id: ${actualDriveId} for parent folder id` ); @@ -211,7 +211,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { } // Generate a new folder ID - const folderId = uuidv4(); + const folderId = EID(uuidv4()); // Create a root folder metadata transaction const folderMetadata = folderPrototypeFactory(folderId, parentFolderId); @@ -225,7 +225,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { } } - return { metaDataTrxId: folderTrx.id, metaDataTrxReward: folderTrx.reward, folderId }; + return { metaDataTrxId: TxID(folderTrx.id), metaDataTrxReward: W(folderTrx.reward), folderId }; } // Convenience wrapper for folder creation in a known-public use case @@ -293,8 +293,8 @@ export class ArFSDAO extends ArFSDAOAnonymous { } return resultFactory({ - metaDataTrxId: driveTrx.id, - metaDataTrxReward: driveTrx.reward, + metaDataTrxId: TxID(driveTrx.id), + metaDataTrxReward: W(driveTrx.reward), rootFolderTrxId: rootFolderTrxId, rootFolderTrxReward: rootFolderTrxReward, driveId: driveId, @@ -310,7 +310,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { ): Promise { return this.createDrive( driveRewardSettings, - () => uuidv4(), + () => EID(uuidv4()), async (driveId) => { const folderData = new ArFSPublicFolderTransactionData(driveName); return this.createPublicFolder({ @@ -386,7 +386,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { } } - return resultFactory({ metaDataTrxId: metaDataTrx.id, metaDataTrxReward: metaDataTrx.reward }); + return resultFactory({ metaDataTrxId: TxID(metaDataTrx.id), metaDataTrxReward: W(metaDataTrx.reward) }); } async movePublicFile({ @@ -490,7 +490,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { const destinationFileName = destFileName ?? wrappedFile.getBaseFileName(); // Use existing file ID (create a revision) or generate new file ID - const fileId = existingFileId ?? uuidv4(); + const fileId = existingFileId ?? EID(uuidv4()); // Gather file information const { fileSize, dataContentType, lastModifiedDateMS } = wrappedFile.gatherFileInfo(); @@ -515,7 +515,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { destinationFileName, fileSize, lastModifiedDateMS, - dataTrx.id, + TxID(dataTrx.id), dataContentType, fileId ); @@ -532,10 +532,10 @@ export class ArFSDAO extends ArFSDAOAnonymous { return resultFactoryFn( { - dataTrxId: dataTrx.id, - dataTrxReward: dataTrx.reward, - metaDataTrxId: metaDataTrx.id, - metaDataTrxReward: metaDataTrx.reward, + dataTrxId: TxID(dataTrx.id), + dataTrxReward: W(dataTrx.reward), + metaDataTrxId: TxID(metaDataTrx.id), + metaDataTrxReward: W(metaDataTrx.reward), fileId }, metadataTrxData @@ -619,27 +619,6 @@ export class ArFSDAO extends ArFSDAOAnonymous { ); } - // /** - // * Uploads a v2 transaction in chunks with progress logging - // * - // * @example await this.sendChunkedUpload(myTransaction); - // */ - // async sendChunkedUploadWithProgress(trx: Transaction): Promise { - // const dataUploader = await this.arweave.transactions.getUploader(trx); - - // while (!dataUploader.isComplete) { - // const nextChunk = await uploadDataChunk(dataUploader); - // if (nextChunk === null) { - // break; - // } else { - // // TODO: Add custom logger function that produces various levels of detail - // console.log( - // `${dataUploader.pctComplete}% complete, ${dataUploader.uploadedChunks}/${dataUploader.totalChunks}` - // ); - // } - // } - // } - async prepareArFSObjectTransaction( objectMetaData: ArFSObjectMetadataPrototype, rewardSettings: RewardSettings = {}, @@ -654,7 +633,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { // If we provided our own reward setting, use it now if (rewardSettings.reward) { - trxAttributes.reward = rewardSettings.reward; + trxAttributes.reward = rewardSettings.reward.toString(); } // TODO: Use a mock arweave server instead @@ -665,16 +644,15 @@ export class ArFSDAO extends ArFSDAOAnonymous { const transaction = await this.arweave.createTransaction(trxAttributes, wallet.getPrivateKey()); // If we've opted to boost the transaction, do so now - if (rewardSettings.feeMultiple && rewardSettings.feeMultiple > 1.0) { - // Round up with ceil because fractional Winston will cause an Arweave API failure - transaction.reward = Math.ceil(+transaction.reward * rewardSettings.feeMultiple).toString(); + if (rewardSettings.feeMultiple?.wouldBoostReward()) { + 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); - if (rewardSettings.feeMultiple && rewardSettings.feeMultiple > 1.0) { + if (rewardSettings.feeMultiple?.wouldBoostReward()) { transaction.addTag('Boost', rewardSettings.feeMultiple.toString()); } @@ -719,7 +697,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { while (hasNextPage) { const gqlQuery = buildQuery({ tags: [ - { name: 'Drive-Id', value: driveId }, + { name: 'Drive-Id', value: `${driveId}` }, { name: 'Entity-Type', value: 'folder' } ], cursor, @@ -756,7 +734,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { while (hasNextPage) { const gqlQuery = buildQuery({ tags: [ - { name: 'Parent-Folder-Id', value: folderIDs }, + { name: 'Parent-Folder-Id', value: folderIDs.map((fid) => fid.toString()) }, { name: 'Entity-Type', value: 'file' } ], cursor, @@ -798,7 +776,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { while (hasNextPage) { const gqlQuery = buildQuery({ tags: [ - { name: 'Parent-Folder-Id', value: parentFolderId }, + { name: 'Parent-Folder-Id', value: `${parentFolderId}` }, { name: 'Entity-Type', value: ['file', 'folder'] } ], cursor, @@ -1051,7 +1029,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { const safelyBuiltDrive = await safeDriveBuilder.build(); if ( safelyBuiltDrive.name === ENCRYPTED_DATA_PLACEHOLDER || - safelyBuiltDrive.rootFolderId === ENCRYPTED_DATA_PLACEHOLDER + `${safelyBuiltDrive.rootFolderId}` === ENCRYPTED_DATA_PLACEHOLDER ) { throw new Error(`Invalid password! Please type the same as your other private drives!`); } diff --git a/src/arfsdao_anonymous.ts b/src/arfsdao_anonymous.ts index 66d4645e..7851a886 100644 --- a/src/arfsdao_anonymous.ts +++ b/src/arfsdao_anonymous.ts @@ -1,16 +1,31 @@ /* eslint-disable no-console */ import Arweave from 'arweave'; -import { ArFSDriveEntity, GQLEdgeInterface } from 'ardrive-core-js'; +import { GQLEdgeInterface } from 'ardrive-core-js'; import { ASCENDING_ORDER, buildQuery } from './query'; -import { DriveID, FolderID, FileID, DEFAULT_APP_NAME, DEFAULT_APP_VERSION, EntityID } from './types'; +import { + DriveID, + FolderID, + FileID, + DEFAULT_APP_NAME, + DEFAULT_APP_VERSION, + AnyEntityID, + ArweaveAddress, + EID, + ADDR +} from './types'; import { latestRevisionFilter, latestRevisionFilterForDrives } from './utils/filter_methods'; import { FolderHierarchy } from './folderHierarchy'; import { ArFSPublicDriveBuilder, SafeArFSDriveBuilder } from './utils/arfs_builders/arfs_drive_builders'; import { ArFSPublicFolderBuilder } from './utils/arfs_builders/arfs_folder_builders'; import { ArFSPublicFileBuilder } from './utils/arfs_builders/arfs_file_builders'; -import { ArFSPublicDrive, ArFSPublicFile, ArFSPublicFileOrFolderWithPaths, ArFSPublicFolder } from './arfs_entities'; +import { + ArFSDriveEntity, + ArFSPublicDrive, + ArFSPublicFile, + ArFSPublicFileOrFolderWithPaths, + ArFSPublicFolder +} from './arfs_entities'; import { PrivateKeyData } from './private_key_data'; -import { ArweaveAddress } from './arweave_address'; export const graphQLURL = 'https://arweave.net/graphql'; @@ -64,11 +79,11 @@ export class ArFSDAOAnonymous extends ArFSDAOType { const driveOwnerAddress = edgeOfFirstDrive.node.owner.address; - return new ArweaveAddress(driveOwnerAddress); + return ADDR(driveOwnerAddress); } - async getDriveIDForEntityId(entityId: EntityID, gqlTypeTag: 'File-Id' | 'Folder-Id'): Promise { - const gqlQuery = buildQuery({ tags: [{ name: gqlTypeTag, value: entityId }] }); + async getDriveIDForEntityId(entityId: AnyEntityID, gqlTypeTag: 'File-Id' | 'Folder-Id'): Promise { + const gqlQuery = buildQuery({ tags: [{ name: gqlTypeTag, value: `${entityId}` }] }); const response = await this.arweave.api.post(graphQLURL, gqlQuery); const { data } = response.data; @@ -82,7 +97,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType { const driveIdTag = edges[0].node.tags.find((t) => t.name === 'Drive-Id'); if (driveIdTag) { - return driveIdTag.value; + return EID(driveIdTag.value); } throw new Error(`No Drive-Id tag found for meta data transaction of ${gqlTypeTag}: ${entityId}`); @@ -163,7 +178,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType { while (hasNextPage) { const gqlQuery = buildQuery({ tags: [ - { name: 'Parent-Folder-Id', value: folderIDs }, + { name: 'Parent-Folder-Id', value: folderIDs.map((fid) => fid.toString()) }, { name: 'Entity-Type', value: 'file' } ], cursor, @@ -198,7 +213,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType { while (hasNextPage) { const gqlQuery = buildQuery({ tags: [ - { name: 'Drive-Id', value: driveId }, + { name: 'Drive-Id', value: `${driveId}` }, { name: 'Entity-Type', value: 'folder' } ], cursor, @@ -255,7 +270,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType { const [, ...subFolderIDs]: FolderID[] = hierarchy.folderIdSubtreeFromFolderId(folderId, maxDepth); const childrenFolderEntities = allFolderEntitiesOfDrive.filter((folder) => - subFolderIDs.includes(folder.entityId) + subFolderIDs.some((fid) => fid.equals(folder.entityId)) ); if (includeRoot) { diff --git a/src/arweave_address.test.ts b/src/arweave_address.test.ts deleted file mode 100644 index 7ca718dd..00000000 --- a/src/arweave_address.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from 'chai'; -import { ArweaveAddress } from './arweave_address'; - -describe('The ArweaveAddress class', () => { - describe('constructor', () => { - it('creates a new address when given a valid address string', () => { - const validAddresses = [ - '-------------------------------------------', - '___________________________________________', - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ', - '0000000000000000000000000000000000000000000', - '0123456789012345678901234567890123456789012', - 'g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0' - ]; - validAddresses.forEach((address) => { - expect(() => new ArweaveAddress(address)).to.not.throw(); - }); - }); - - it('throws an error for input addresses that are not 43 characters in length', () => { - const invalidAddresses = ['', '-', 'g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms01']; - invalidAddresses.forEach((badAddress) => { - expect(() => new ArweaveAddress(badAddress)).to.throw(Error); - }); - }); - - it('throws an error for input addresses with invalid characters', () => { - const invalidAddresses = '!@#$%^&*()+=~`{[}]\\|;:\'"<,>.?/'.split('').map((char) => char.repeat(43)); - invalidAddresses.forEach((badAddress) => { - expect(() => new ArweaveAddress(badAddress)).to.throw(Error); - }); - }); - }); - - describe('interpolated toString function', () => { - it('returns the underlying address string', () => { - const address = new ArweaveAddress('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); - expect(`${address}`).to.equal('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); - }); - }); -}); diff --git a/src/arweave_address.ts b/src/arweave_address.ts deleted file mode 100644 index eac227ab..00000000 --- a/src/arweave_address.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class ArweaveAddress { - constructor(private readonly address: string) { - if (!address.match(new RegExp('^[a-zA-Z0-9_-]{43}$'))) { - throw new Error( - 'Arweave addresses must be 43 characters in length with characters in the following set: [a-zA-Z0-9_-]' - ); - } - } - - equalsAddress(other: ArweaveAddress): boolean { - return this.address === other.address; - } - - toString(): string { - return this.address; - } - - valueOf(): string { - return this.address; - } -} diff --git a/src/commands/create_drive.ts b/src/commands/create_drive.ts index 5f060a23..fc514301 100644 --- a/src/commands/create_drive.ts +++ b/src/commands/create_drive.ts @@ -7,35 +7,37 @@ import { } from '../parameter_declarations'; import { arDriveFactory } from '..'; import { JWKWallet, Wallet } from '../wallet'; -import { FeeMultiple } from '../types'; import { PrivateDriveKeyData } from '../arfsdao'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; new CLICommand({ name: 'create-drive', parameters: [...DriveCreationPrivacyParameters, DriveNameParameter, BoostParameter, DryRunParameter], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); const wallet: Wallet = await parameters.getRequiredWallet(); + const dryRun = !!parameters.getParameterValue(DryRunParameter); + const driveName = parameters.getRequiredParameterValue(DriveNameParameter); const ardrive = arDriveFactory({ wallet: wallet, - feeMultiple: options.boost as FeeMultiple, - dryRun: options.dryRun + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun }); const createDriveResult = await (async function () { if (await parameters.getIsPrivate()) { const drivePassword = await parameters.getDrivePassword(true); const walletPrivateKey = (wallet as JWKWallet).getPrivateKey(); - const newDriveData = await PrivateDriveKeyData.from(drivePassword, walletPrivateKey); + const newPrivateDriveData = await PrivateDriveKeyData.from(drivePassword, walletPrivateKey); await ardrive.assertValidPassword(drivePassword); - return ardrive.createPrivateDrive(options.driveName, newDriveData); + return ardrive.createPrivateDrive({ driveName, newPrivateDriveData }); } else { - return ardrive.createPublicDrive(options.driveName); + return ardrive.createPublicDrive({ driveName }); } })(); console.log(JSON.stringify(createDriveResult, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/create_folder.ts b/src/commands/create_folder.ts index f6ac3758..3ee37881 100644 --- a/src/commands/create_folder.ts +++ b/src/commands/create_folder.ts @@ -8,8 +8,9 @@ import { } from '../parameter_declarations'; import { arDriveFactory } from '..'; import { Wallet } from '../wallet'; -import { FeeMultiple } from '../types'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; +import { EID } from '../types'; new CLICommand({ name: 'create-folder', @@ -20,19 +21,20 @@ new CLICommand({ DryRunParameter, ...DrivePrivacyParameters ], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); const wallet: Wallet = await parameters.getRequiredWallet(); + const dryRun = !!parameters.getParameterValue(DryRunParameter); const ardrive = arDriveFactory({ - wallet: wallet, - feeMultiple: options.boost as FeeMultiple, - dryRun: options.dryRun + wallet, + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun }); - const parentFolderId = parameters.getRequiredParameterValue(ParentFolderIdParameter); - const driveId = await ardrive.getDriveIdForFolderId(options.parentFolderId); - const folderName = options.folderName; + const parentFolderId = parameters.getRequiredParameterValue(ParentFolderIdParameter, EID); + const driveId = await ardrive.getDriveIdForFolderId(parentFolderId); + const folderName = parameters.getRequiredParameterValue(FolderNameParameter); const createFolderResult = await (async function () { if (await parameters.getIsPrivate()) { @@ -50,5 +52,5 @@ new CLICommand({ console.log(JSON.stringify(createFolderResult, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index f220271c..0cfdc7f9 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -1,6 +1,7 @@ import { arDriveFactory, cliWalletDao } from '..'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { BoostParameter, FolderIdParameter, @@ -11,7 +12,7 @@ import { WalletFileParameter, DestinationManifestNameParameter } from '../parameter_declarations'; -import { FeeMultiple } from '../types'; +import { EID } from '../types'; new CLICommand({ name: 'create-manifest', @@ -25,15 +26,15 @@ new CLICommand({ SeedPhraseParameter, ...TreeDepthParams ], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options, cliWalletDao); const wallet = await parameters.getRequiredWallet(); const arDrive = arDriveFactory({ wallet: wallet, - feeMultiple: options.boost as FeeMultiple, - dryRun: options.dryRun + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun: !!options.dryRun }); // User can specify either a drive ID or a folder ID @@ -53,8 +54,8 @@ new CLICommand({ // TODO: Private manifests 🤔 const result = await arDrive.uploadPublicManifest({ - driveId, - folderId, + driveId: driveId ? EID(driveId) : undefined, + folderId: folderId ? EID(folderId) : undefined, maxDepth, destManifestName }); @@ -62,5 +63,5 @@ new CLICommand({ console.log(JSON.stringify(result, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/drive_info.ts b/src/commands/drive_info.ts index 90c3548a..394156b7 100644 --- a/src/commands/drive_info.ts +++ b/src/commands/drive_info.ts @@ -1,18 +1,18 @@ import { CLICommand, ParametersHelper } from '../CLICommand'; -import { DriveID } from '../types'; +import { EID } from '../types'; import { DriveIdParameter, GetAllRevisionsParameter, DrivePrivacyParameters } from '../parameter_declarations'; import { arDriveAnonymousFactory, arDriveFactory } from '..'; import { ArFSPrivateDrive, ArFSPublicDrive } from '../arfs_entities'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; new CLICommand({ name: 'drive-info', parameters: [DriveIdParameter, GetAllRevisionsParameter, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const driveId: DriveID = options.driveId; - // const shouldGetAllRevisions: boolean = options.getAllRevisions; + const driveId = EID(parameters.getRequiredParameterValue(DriveIdParameter)); const result: Partial = await (async function () { if (await parameters.getIsPrivate()) { @@ -20,17 +20,14 @@ new CLICommand({ const arDrive = arDriveFactory({ wallet: wallet }); const driveKey = await parameters.getDriveKey({ driveId }); - return arDrive.getPrivateDrive(driveId, driveKey /*, shouldGetAllRevisions*/); + return arDrive.getPrivateDrive({ driveId, driveKey }); } else { const arDrive = arDriveAnonymousFactory(); - return arDrive.getPublicDrive(driveId /*, shouldGetAllRevisions*/); + return arDrive.getPublicDrive({ driveId }); } })(); - // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 - delete result.syncStatus; - console.log(JSON.stringify(result, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/file_info.ts b/src/commands/file_info.ts index 9e04d851..fc2607fe 100644 --- a/src/commands/file_info.ts +++ b/src/commands/file_info.ts @@ -1,16 +1,17 @@ import { CLICommand, ParametersHelper } from '../CLICommand'; import { GetAllRevisionsParameter, FileIdParameter, DrivePrivacyParameters } from '../parameter_declarations'; import { arDriveAnonymousFactory, arDriveFactory, cliWalletDao } from '..'; -import { FileID } from '../types'; +import { FileID, EID } from '../types'; import { ArFSPrivateFile, ArFSPublicFile } from '../arfs_entities'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; new CLICommand({ name: 'file-info', parameters: [FileIdParameter, GetAllRevisionsParameter, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options, cliWalletDao); - const fileId: FileID = parameters.getRequiredParameterValue(FileIdParameter); + const fileId: FileID = EID(parameters.getRequiredParameterValue(FileIdParameter)); // const shouldGetAllRevisions: boolean = options.getAllRevisions; const result: Partial = await (async function () { @@ -24,17 +25,14 @@ new CLICommand({ // We have the drive id from deriving a key, we can derive the owner const driveOwner = await arDrive.getOwnerForDriveId(driveId); - return arDrive.getPrivateFile(fileId, driveKey, driveOwner /*, shouldGetAllRevisions*/); + return arDrive.getPrivateFile({ fileId, driveKey, owner: driveOwner }); } else { const arDrive = arDriveAnonymousFactory(); - return arDrive.getPublicFile(fileId /*, shouldGetAllRevisions*/); + return arDrive.getPublicFile({ fileId }); } })(); - // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 - delete result.syncStatus; - console.log(JSON.stringify(result, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/folder_info.ts b/src/commands/folder_info.ts index fd98bb3f..15c69826 100644 --- a/src/commands/folder_info.ts +++ b/src/commands/folder_info.ts @@ -1,19 +1,19 @@ import { CLICommand, ParametersHelper } from '../CLICommand'; -import { FolderID } from '../types'; +import { EID } from '../types'; import { GetAllRevisionsParameter, FolderIdParameter, DrivePrivacyParameters } from '../parameter_declarations'; import { arDriveAnonymousFactory, arDriveFactory } from '..'; import { ArFSPrivateFolder, ArFSPublicFolder } from '../arfs_entities'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; new CLICommand({ name: 'folder-info', parameters: [FolderIdParameter, GetAllRevisionsParameter, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - // const shouldGetAllRevisions: boolean = options.getAllRevisions; const result: Partial = await (async function () { - const folderId: FolderID = parameters.getRequiredParameterValue(FolderIdParameter); + const folderId = EID(parameters.getRequiredParameterValue(FolderIdParameter)); if (await parameters.getIsPrivate()) { const wallet = await parameters.getRequiredWallet(); @@ -25,11 +25,10 @@ new CLICommand({ // We have the drive id from deriving a key, we can derive the owner const driveOwner = await arDrive.getOwnerForDriveId(driveId); - return arDrive.getPrivateFolder(folderId, driveKey, driveOwner /*, shouldGetAllRevisions*/); + return arDrive.getPrivateFolder({ folderId, driveKey, owner: driveOwner }); } else { const arDrive = arDriveAnonymousFactory(); - const folderId: string = options.folderId; - return arDrive.getPublicFolder(folderId /*, shouldGetAllRevisions*/); + return arDrive.getPublicFolder({ folderId }); } })(); @@ -38,9 +37,8 @@ new CLICommand({ delete result.dataTxId; delete result.dataContentType; delete result.lastModifiedDate; - delete result.syncStatus; console.log(JSON.stringify(result, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/generate_seedphrase.ts b/src/commands/generate_seedphrase.ts index 89907451..bfcccb39 100644 --- a/src/commands/generate_seedphrase.ts +++ b/src/commands/generate_seedphrase.ts @@ -1,13 +1,14 @@ import { cliWalletDao } from '..'; import { CLICommand } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; new CLICommand({ name: 'generate-seedphrase', parameters: [], - async action() { + action: new CLIAction(async function action() { const seedPhrase = await cliWalletDao.generateSeedPhrase(); console.log(JSON.stringify(seedPhrase)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/generate_wallet.ts b/src/commands/generate_wallet.ts index 47d6b245..e4bce047 100644 --- a/src/commands/generate_wallet.ts +++ b/src/commands/generate_wallet.ts @@ -1,16 +1,18 @@ import { cliWalletDao } from '..'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { SeedPhraseParameter } from '../parameter_declarations'; +import { SeedPhrase } from '../types'; new CLICommand({ name: 'generate-wallet', parameters: [SeedPhraseParameter], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const seedPhrase = await parameters.getRequiredParameterValue(SeedPhraseParameter); - const wallet = await cliWalletDao.generateJWKWallet(seedPhrase); + const seedPhrase = parameters.getRequiredParameterValue(SeedPhraseParameter); + const wallet = await cliWalletDao.generateJWKWallet(new SeedPhrase(seedPhrase)); console.log(JSON.stringify(wallet)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/get_address.ts b/src/commands/get_address.ts index f769070b..47a90815 100644 --- a/src/commands/get_address.ts +++ b/src/commands/get_address.ts @@ -1,14 +1,15 @@ import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { SeedPhraseParameter, WalletFileParameter } from '../parameter_declarations'; new CLICommand({ name: 'get-address', parameters: [WalletFileParameter, SeedPhraseParameter], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); const address = await parameters.getWalletAddress(); console.log(`${address}`); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/get_balance.ts b/src/commands/get_balance.ts index f9ff7bef..242d4825 100644 --- a/src/commands/get_balance.ts +++ b/src/commands/get_balance.ts @@ -1,19 +1,20 @@ -import { winstonToAr } from 'ardrive-core-js'; import { cliWalletDao } from '..'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { AddressParameter, SeedPhraseParameter, WalletFileParameter } from '../parameter_declarations'; +import { AR } from '../types'; new CLICommand({ name: 'get-balance', parameters: [WalletFileParameter, SeedPhraseParameter, AddressParameter], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); const address = await parameters.getWalletAddress(); const balanceInWinston = await cliWalletDao.getAddressWinstonBalance(address); - const balanceInAR = winstonToAr(balanceInWinston); + const balanceInAR = new AR(balanceInWinston); console.log(`${balanceInWinston}\tWinston`); console.log(`${balanceInAR}\tAR`); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/get_drive_key.ts b/src/commands/get_drive_key.ts index cd2c2e11..9e54f321 100644 --- a/src/commands/get_drive_key.ts +++ b/src/commands/get_drive_key.ts @@ -1,19 +1,21 @@ import { arDriveFactory } from '..'; import { CLICommand, ParametersHelper } from '../CLICommand'; +import { CLIAction } from '../CLICommand/action'; import { DriveCreationPrivacyParameters, DriveIdParameter, NoVerifyParameter } from '../parameter_declarations'; +import { EID } from '../types'; import { urlEncodeHashKey } from '../utils'; new CLICommand({ name: 'get-drive-key', parameters: [...DriveCreationPrivacyParameters, DriveIdParameter, NoVerifyParameter], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const driveId = parameters.getRequiredParameterValue(DriveIdParameter); + const driveId = EID(parameters.getRequiredParameterValue(DriveIdParameter)); const driveKey = await parameters.getDriveKey({ driveId }); if (options.verify) { const arDrive = arDriveFactory({ wallet: await parameters.getRequiredWallet() }); - await arDrive.getPrivateDrive(driveId, driveKey); + await arDrive.getPrivateDrive({ driveId, driveKey }); } console.log(urlEncodeHashKey(driveKey)); - } + }) }); diff --git a/src/commands/get_file_key.ts b/src/commands/get_file_key.ts index d5cc0567..7173cbda 100644 --- a/src/commands/get_file_key.ts +++ b/src/commands/get_file_key.ts @@ -1,6 +1,7 @@ import { deriveFileKey } from 'ardrive-core-js'; import { cliArweave } from '..'; import { CLICommand, ParametersHelper } from '../CLICommand'; +import { CLIAction } from '../CLICommand/action'; import { DriveCreationPrivacyParameters, DriveIdParameter, @@ -8,7 +9,7 @@ import { FileIdParameter, NoVerifyParameter } from '../parameter_declarations'; -import { DriveID } from '../types'; +import { DriveID, EID } from '../types'; import { urlEncodeHashKey } from '../utils'; import { ArFSPrivateFileBuilder } from '../utils/arfs_builders/arfs_file_builders'; @@ -21,9 +22,9 @@ new CLICommand({ FileIdParameter, NoVerifyParameter ], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const fileId = parameters.getRequiredParameterValue(FileIdParameter); + const fileId = EID(parameters.getRequiredParameterValue(FileIdParameter)); // Obviate the need for a drive ID when a drive key is specified const driveKey = await (async () => { @@ -34,15 +35,15 @@ new CLICommand({ // Lean on getDriveKey with a specified driveID // TODO: In the future, loosen driveID requirement and fetch from fileID - const driveId: DriveID = parameters.getRequiredParameterValue(DriveIdParameter); + const driveId: DriveID = EID(parameters.getRequiredParameterValue(DriveIdParameter)); return await parameters.getDriveKey({ driveId }); })(); - const fileKey = await deriveFileKey(fileId, driveKey); + const fileKey = await deriveFileKey(`${fileId}`, driveKey); if (options.verify) { await new ArFSPrivateFileBuilder(fileId, cliArweave, driveKey, undefined, fileKey).build(); } console.log(urlEncodeHashKey(fileKey)); - } + }) }); diff --git a/src/commands/get_mempool.ts b/src/commands/get_mempool.ts index ff453e1d..e70201f1 100644 --- a/src/commands/get_mempool.ts +++ b/src/commands/get_mempool.ts @@ -1,13 +1,14 @@ import { CLICommand } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { fetchMempool } from '../utils'; new CLICommand({ name: 'get-mempool', parameters: [], - async action() { + action: new CLIAction(async function action() { const transactionsInMempool = await fetchMempool(); console.log(JSON.stringify(transactionsInMempool, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/index.ts b/src/commands/index.ts index 3f470782..ec6fe5da 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,3 @@ -import { CLICommand } from '../CLICommand'; import '../parameter_declarations'; import './create_drive'; import './drive_info'; @@ -21,5 +20,3 @@ import './move_folder'; import './get_drive_key'; import './get_file_key'; import './create_manifest'; - -CLICommand.parse(); diff --git a/src/commands/list_all_drives.ts b/src/commands/list_all_drives.ts index a2e7c2a2..dca5ba22 100644 --- a/src/commands/list_all_drives.ts +++ b/src/commands/list_all_drives.ts @@ -1,29 +1,25 @@ -import { ArFSDriveEntity } from 'ardrive-core-js'; import { arDriveAnonymousFactory, cliWalletDao } from '..'; +import { ArFSDriveEntity } from '../arfs_entities'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { AddressParameter, DrivePrivacyParameters } from '../parameter_declarations'; new CLICommand({ name: 'list-all-drives', parameters: [AddressParameter, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options, cliWalletDao); const ardrive = arDriveAnonymousFactory(); const address = await parameters.getWalletAddress(); const privateKeyData = await parameters.getPrivateKeyData(); - const drives: Partial[] = await ardrive.getAllDrivesForAddress(address, privateKeyData); - - // TODO: Fix base types so deleting un-used values is not necessary; Tickets: PE-525 + PE-556 - for (const drive of drives) { - delete drive.syncStatus; - } + const drives: Partial[] = await ardrive.getAllDrivesForAddress({ address, privateKeyData }); // Display data console.log(JSON.stringify(drives, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/list_drive.ts b/src/commands/list_drive.ts index 036231b3..6dc46c30 100644 --- a/src/commands/list_drive.ts +++ b/src/commands/list_drive.ts @@ -1,18 +1,20 @@ import { arDriveFactory, cliArweave, cliWalletDao } from '..'; -import { ArDriveAnonymous } from '../ardrive'; +import { ArDriveAnonymous } from '../ardrive_anonymous'; import { ArFSDAOAnonymous } from '../arfsdao_anonymous'; import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { DriveIdParameter, DrivePrivacyParameters, TreeDepthParams } from '../parameter_declarations'; +import { EID } from '../types'; import { alphabeticalOrder } from '../utils/sort_functions'; new CLICommand({ name: 'list-drive', parameters: [DriveIdParameter, ...TreeDepthParams, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options, cliWalletDao); - const driveId = parameters.getRequiredParameterValue(DriveIdParameter); + const driveId = EID(parameters.getRequiredParameterValue(DriveIdParameter)); let children: (ArFSPrivateFileOrFolderWithPaths | ArFSPublicFileOrFolderWithPaths)[]; const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); @@ -20,7 +22,7 @@ new CLICommand({ const wallet = await parameters.getRequiredWallet(); const arDrive = arDriveFactory({ wallet }); const driveKey = await parameters.getDriveKey({ driveId }); - const drive = await arDrive.getPrivateDrive(driveId, driveKey); + const drive = await arDrive.getPrivateDrive({ driveId, driveKey }); const rootFolderId = drive.rootFolderId; // We have the drive id from deriving a key, we can derive the owner @@ -35,7 +37,7 @@ new CLICommand({ }); } else { const arDrive = new ArDriveAnonymous(new ArFSDAOAnonymous(cliArweave)); - const drive = await arDrive.getPublicDrive(driveId); + const drive = await arDrive.getPublicDrive({ driveId }); const rootFolderId = drive.rootFolderId; children = await arDrive.listPublicFolder({ folderId: rootFolderId, maxDepth, includeRoot: true }); } @@ -53,11 +55,10 @@ new CLICommand({ delete fileOrFolderMetaData.dataTxId; delete fileOrFolderMetaData.dataContentType; } - delete fileOrFolderMetaData.syncStatus; }); // Display data console.log(JSON.stringify(sortedChildren, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/list_folder.ts b/src/commands/list_folder.ts index 8fb6f821..05f58e4f 100644 --- a/src/commands/list_folder.ts +++ b/src/commands/list_folder.ts @@ -1,16 +1,18 @@ import { arDriveAnonymousFactory, arDriveFactory } from '..'; import { ArFSPrivateFileOrFolderWithPaths, ArFSPublicFileOrFolderWithPaths } from '../arfs_entities'; import { CLICommand, ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { DrivePrivacyParameters, ParentFolderIdParameter, TreeDepthParams } from '../parameter_declarations'; +import { EID } from '../types'; import { alphabeticalOrder } from '../utils/sort_functions'; new CLICommand({ name: 'list-folder', parameters: [ParentFolderIdParameter, ...TreeDepthParams, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const folderId = parameters.getRequiredParameterValue(ParentFolderIdParameter); + const folderId = EID(parameters.getRequiredParameterValue(ParentFolderIdParameter)); let children: (ArFSPrivateFileOrFolderWithPaths | ArFSPublicFileOrFolderWithPaths)[]; const maxDepth = await parameters.getMaxDepth(0); @@ -43,11 +45,10 @@ new CLICommand({ delete fileOrFolderMetaData.dataTxId; delete fileOrFolderMetaData.dataContentType; } - delete fileOrFolderMetaData.syncStatus; }); // Display data console.log(JSON.stringify(sortedChildren, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/move_file.ts b/src/commands/move_file.ts index bd8050af..e39b92d2 100644 --- a/src/commands/move_file.ts +++ b/src/commands/move_file.ts @@ -8,36 +8,39 @@ import { } from '../parameter_declarations'; import { Wallet } from '../wallet'; import { arDriveFactory } from '..'; -import { FeeMultiple } from '../types'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; +import { EID } from '../types'; new CLICommand({ name: 'move-file', parameters: [FileIdParameter, ParentFolderIdParameter, BoostParameter, DryRunParameter, ...DrivePrivacyParameters], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const { fileId, parentFolderId, boost, dryRun } = options; + const dryRun = !!parameters.getParameterValue(DryRunParameter); + const fileId = parameters.getRequiredParameterValue(FileIdParameter, EID); + const newParentFolderId = parameters.getRequiredParameterValue(ParentFolderIdParameter, EID); const wallet: Wallet = await parameters.getRequiredWallet(); const ardrive = arDriveFactory({ wallet: wallet, - feeMultiple: boost as FeeMultiple, - dryRun: dryRun + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun }); const createDriveResult = await (async function () { if (await parameters.getIsPrivate()) { - const driveId = await ardrive.getDriveIdForFolderId(parentFolderId); + const driveId = await ardrive.getDriveIdForFolderId(newParentFolderId); const driveKey = await parameters.getDriveKey({ driveId }); - return ardrive.movePrivateFile(fileId, parentFolderId, driveKey); + return ardrive.movePrivateFile({ fileId, newParentFolderId, driveKey }); } else { - return ardrive.movePublicFile(fileId, parentFolderId); + return ardrive.movePublicFile({ fileId, newParentFolderId }); } })(); console.log(JSON.stringify(createDriveResult, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/move_folder.ts b/src/commands/move_folder.ts index d01e4999..206210a1 100644 --- a/src/commands/move_folder.ts +++ b/src/commands/move_folder.ts @@ -8,8 +8,9 @@ import { } from '../parameter_declarations'; import { Wallet } from '../wallet'; import { arDriveFactory } from '..'; -import { FeeMultiple } from '../types'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; +import { EID } from '../types'; new CLICommand({ name: 'move-folder', @@ -20,18 +21,18 @@ new CLICommand({ DryRunParameter, ...DrivePrivacyParameters ], - async action(options) { + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const { parentFolderId: newParentFolderId, boost, dryRun } = options; - - const folderId = parameters.getRequiredParameterValue(FolderIdParameter); + const dryRun = !!parameters.getParameterValue(DryRunParameter); + const folderId = parameters.getRequiredParameterValue(FolderIdParameter, EID); + const newParentFolderId = parameters.getRequiredParameterValue(ParentFolderIdParameter, EID); const wallet: Wallet = await parameters.getRequiredWallet(); const ardrive = arDriveFactory({ wallet: wallet, - feeMultiple: boost as FeeMultiple, - dryRun: dryRun + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun }); const moveFolderResult = await (async function () { @@ -48,5 +49,5 @@ new CLICommand({ console.log(JSON.stringify(moveFolderResult, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/send_ar.ts b/src/commands/send_ar.ts index a27a4f20..12745424 100644 --- a/src/commands/send_ar.ts +++ b/src/commands/send_ar.ts @@ -1,8 +1,9 @@ +import { AR, ADDR } from '../types'; import { cliWalletDao, CLI_APP_NAME, CLI_APP_VERSION } from '..'; -import { ArweaveAddress } from '../arweave_address'; import { CLICommand } from '../CLICommand'; import { ParametersHelper } from '../CLICommand'; -import { SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLIAction } from '../CLICommand/action'; +import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { ArAmountParameter, BoostParameter, @@ -10,29 +11,29 @@ import { DryRunParameter, WalletFileParameter } from '../parameter_declarations'; -import { assertARPrecision } from '../utils/ar_unit'; new CLICommand({ name: 'send-ar', parameters: [ArAmountParameter, DestinationAddressParameter, WalletFileParameter, BoostParameter, DryRunParameter], - async action(options) { - assertARPrecision(options.arAmount); + action: new CLIAction(async function action(options) { const parameters = new ParametersHelper(options); - const arAmount: number = +options.arAmount; - const destAddress = new ArweaveAddress(options.destAddress); + const arAmount = parameters.getRequiredParameterValue(ArAmountParameter, AR.from); + const destAddress = parameters.getRequiredParameterValue(DestinationAddressParameter, ADDR); const wallet = await parameters.getRequiredWallet(); const walletAddress = await wallet.getAddress(); + const boost = parameters.getOptionalBoostSetting(); console.log(`Source address: ${walletAddress}`); - console.log(`AR amount sent: ${arAmount.toFixed(12)}`); + console.log(`AR amount sent: ${arAmount.toString()}`); console.log(`Destination address: ${destAddress}`); - const rewardSetting = options.boost ? { feeMultiple: +options.boost } : undefined; + const rewardSetting = boost ? { feeMultiple: boost } : undefined; + const dryRun = !!parameters.getParameterValue(DryRunParameter); const arTransferResult = await cliWalletDao.sendARToAddress( arAmount, wallet, destAddress, rewardSetting, - options.dryRun, + dryRun, [ { name: 'App-Name', value: CLI_APP_NAME }, { name: 'App-Version', value: CLI_APP_VERSION }, @@ -43,5 +44,5 @@ new CLICommand({ console.log(JSON.stringify(arTransferResult, null, 4)); return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/tx_status.ts b/src/commands/tx_status.ts index 5cc54a56..dd0180f7 100644 --- a/src/commands/tx_status.ts +++ b/src/commands/tx_status.ts @@ -1,16 +1,20 @@ import { cliArweave } from '..'; -import { CLICommand } from '../CLICommand'; -import { ERROR_EXIT_CODE, SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { CLICommand, ParametersHelper } from '../CLICommand'; +import { CLIAction } from '../CLICommand/action'; +import { ERROR_EXIT_CODE, SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { ConfirmationsParameter, TransactionIdParameter } from '../parameter_declarations'; +import { TransactionID, TxID } from '../types'; import { fetchMempool } from '../utils'; new CLICommand({ name: 'tx-status', parameters: [TransactionIdParameter, ConfirmationsParameter], - async action(options) { - const { txId, confirmations } = options; - const transactionsInMempool = await fetchMempool(); - const pending = transactionsInMempool.includes(txId); + action: new CLIAction(async function action(options) { + const parameters = new ParametersHelper(options); + const confirmations = parameters.getParameterValue(ConfirmationsParameter); + const txId = parameters.getRequiredParameterValue(TransactionIdParameter, TxID); + const transactionsInMempool = (await fetchMempool()).map((id) => new TransactionID(id)); + const pending = transactionsInMempool.some((tx) => tx.equals(txId)); const confirmationAmount = confirmations ?? 15; if (pending) { @@ -18,7 +22,7 @@ new CLICommand({ return SUCCESS_EXIT_CODE; } - const confStatus = (await cliArweave.transactions.getStatus(txId)).confirmed; + const confStatus = (await cliArweave.transactions.getStatus(`${txId}`)).confirmed; if (!confStatus?.block_height) { console.log(`${txId}: Not found`); @@ -36,5 +40,5 @@ new CLICommand({ } return SUCCESS_EXIT_CODE; - } + }) }); diff --git a/src/commands/upload_file.ts b/src/commands/upload_file.ts index 25b56f82..87d5d0b3 100644 --- a/src/commands/upload_file.ts +++ b/src/commands/upload_file.ts @@ -9,11 +9,13 @@ import { DryRunParameter, LocalFilePathParameter, LocalFilesParameter, - ParentFolderIdParameter + ParentFolderIdParameter, + WalletFileParameter } from '../parameter_declarations'; -import { DriveKey, FeeMultiple, FolderID } from '../types'; +import { DriveKey, EID, FolderID } from '../types'; import { readJWKFile } from '../utils'; -import { ERROR_EXIT_CODE, SUCCESS_EXIT_CODE } from '../CLICommand/constants'; +import { ERROR_EXIT_CODE, SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; +import { CLIAction } from '../CLICommand/action'; interface UploadFileParameter { parentFolderId: FolderID; @@ -35,18 +37,22 @@ new CLICommand({ ...ConflictResolutionParams, ...DrivePrivacyParameters ], - async action(options) { + action: new CLIAction(async function action(options) { + const parameters = new ParametersHelper(options); const filesToUpload: UploadFileParameter[] = await (async function (): Promise { - if (options.localFiles) { + const localFiles = parameters.getParameterValue(LocalFilesParameter); + if (localFiles) { const COLUMN_SEPARATOR = ','; const ROW_SEPARATOR = '.'; - const csvRows = options.localFiles.split(ROW_SEPARATOR); + const csvRows = localFiles.split(ROW_SEPARATOR); const fileParameters: UploadFileParameter[] = csvRows.map((row: string) => { const csvFields = row.split(COLUMN_SEPARATOR).map((f: string) => f.trim()); - const [parentFolderId, localFilePath, destinationFileName, drivePassword, driveKey] = csvFields; + const [_parentFolderId, localFilePath, destinationFileName, drivePassword, _driveKey] = csvFields; // TODO: Make CSV uploads more bulk performant const wrappedEntity = wrapFileOrFolder(localFilePath); + const parentFolderId = EID(_parentFolderId); + const driveKey = Buffer.from(_driveKey); return { parentFolderId, @@ -63,25 +69,25 @@ new CLICommand({ throw new Error('Must provide a local file path!'); } + const parentFolderId: FolderID = parameters.getRequiredParameterValue(ParentFolderIdParameter, EID); + const localFilePath = parameters.getRequiredParameterValue(LocalFilePathParameter, wrapFileOrFolder); const singleParameter = { - parentFolderId: options.parentFolderId, - wrappedEntity: wrapFileOrFolder(options.localFilePath), - destinationFileName: options.destFileName + parentFolderId: parentFolderId, + wrappedEntity: localFilePath, + destinationFileName: options.destFileName as string }; return [singleParameter]; })(); if (filesToUpload.length) { - const parameters = new ParametersHelper(options); - - const wallet = readJWKFile(options.walletFile); + const wallet = parameters.getRequiredParameterValue(WalletFileParameter, readJWKFile); const conflictResolution = parameters.getFileNameConflictResolution(); const arDrive = arDriveFactory({ wallet: wallet, - feeMultiple: options.boost as FeeMultiple, - dryRun: options.dryRun + feeMultiple: parameters.getOptionalBoostSetting(), + dryRun: !!options.dryRun }); await Promise.all( @@ -143,5 +149,5 @@ new CLICommand({ } console.log(`No files to upload`); return ERROR_EXIT_CODE; - } + }) }); diff --git a/src/community/ardrive_community_oracle.ts b/src/community/ardrive_community_oracle.ts index fb32c7d3..805b1006 100644 --- a/src/community/ardrive_community_oracle.ts +++ b/src/community/ardrive_community_oracle.ts @@ -1,18 +1,17 @@ import { weightedRandom } from 'ardrive-core-js'; import { ContractOracle, ContractReader } from './contract_oracle'; import { CommunityOracle } from './community_oracle'; -import { Winston } from '../types'; import { ArDriveContractOracle } from './ardrive_contract_oracle'; import Arweave from 'arweave'; import { SmartweaveContractReader } from './smartweave_contract_oracle'; import { VertoContractReader } from './verto_contract_oracle'; -import { ArweaveAddress } from '../arweave_address'; +import { ADDR, ArweaveAddress, W, Winston } from '../types'; /** * Minimum ArDrive community tip from the Community Improvement Proposal Doc: * https://arweave.net/Yop13NrLwqlm36P_FDCdMaTBwSlj0sdNGAC4FqfRUgo */ -export const minArDriveCommunityWinstonTip = 10_000_000; +export const minArDriveCommunityWinstonTip = W(10_000_000); /** * Oracle class responsible for determining the community tip @@ -41,8 +40,8 @@ export class ArDriveCommunityOracle implements CommunityOracle { */ async getCommunityWinstonTip(winstonCost: Winston): Promise { const communityTipPercentage = await this.contractOracle.getTipPercentageFromContract(); - const arDriveCommunityTip = +winstonCost * communityTipPercentage; - return Math.round(Math.max(arDriveCommunityTip, minArDriveCommunityWinstonTip)).toString(); + const arDriveCommunityTip = winstonCost.times(communityTipPercentage); + return Winston.max(arDriveCommunityTip, minArDriveCommunityWinstonTip); } /** @@ -92,6 +91,6 @@ export class ArDriveCommunityOracle implements CommunityOracle { throw new Error('Token holder target could not be determined for community tip distribution..'); } - return new ArweaveAddress(randomHolder); + return ADDR(randomHolder); } } diff --git a/src/community/ardrive_contract_oracle.ts b/src/community/ardrive_contract_oracle.ts index 4f4df75d..d07b8f14 100644 --- a/src/community/ardrive_contract_oracle.ts +++ b/src/community/ardrive_contract_oracle.ts @@ -1,6 +1,6 @@ import { CommunityContractData, CommunityTipPercentage } from './contract_types'; import { ContractOracle, ContractReader } from './contract_oracle'; -import { TransactionID } from '../types'; +import { TransactionID, TxID } from '../types'; // ArDrive Profit Sharing Community Smart Contract export const communityTxId = '-8A6RexFkpfWwuyVO98wzSFZh0d6VJuI-buTJvlwOJQ'; @@ -92,7 +92,7 @@ export class ArDriveContractOracle implements ContractOracle { } // Begin new contract read; cast result to known ArDrive Community Contract type - this.contractPromise = this.readContract(communityTxId) as Promise; + this.contractPromise = this.readContract(TxID(communityTxId)) as Promise; this.communityContract = await this.contractPromise; diff --git a/src/community/community_oracle.ts b/src/community/community_oracle.ts index 59ef6c70..1d55024e 100644 --- a/src/community/community_oracle.ts +++ b/src/community/community_oracle.ts @@ -1,5 +1,4 @@ -import { ArweaveAddress } from '../arweave_address'; -import { Winston } from '../types'; +import { ArweaveAddress, Winston } from '../types'; export interface CommunityOracle { getCommunityWinstonTip(winstonCost: Winston): Promise; diff --git a/src/community/contract_types.ts b/src/community/contract_types.ts index 23852b20..f64b3cbd 100644 --- a/src/community/contract_types.ts +++ b/src/community/contract_types.ts @@ -1,4 +1,4 @@ -import { ArweaveAddress } from '../arweave_address'; +import { ArweaveAddress } from '../types'; export type CommunityTipPercentage = number; diff --git a/src/community/smartweave_contract_oracle.ts b/src/community/smartweave_contract_oracle.ts index 7639604a..954ec1f1 100644 --- a/src/community/smartweave_contract_oracle.ts +++ b/src/community/smartweave_contract_oracle.ts @@ -12,6 +12,6 @@ export class SmartweaveContractReader implements ContractReader { /** Fetches smartweave contracts from Arweave with smartweave-js */ async readContract(txId: TransactionID): Promise { - return readContract(this.arweave, txId); + return readContract(this.arweave, `${txId}`); } } diff --git a/src/folderHierarchy.ts b/src/folderHierarchy.ts index 8fa46dbc..3adcbc9a 100644 --- a/src/folderHierarchy.ts +++ b/src/folderHierarchy.ts @@ -1,5 +1,6 @@ import { ArFSFileOrFolderEntity } from './arfs_entities'; -import { FolderID } from './types'; +import { FolderID, EID } from './types'; +import { ROOT_FOLDER_ID_PLACEHOLDER } from './utils/arfs_builders/arfs_folder_builders'; export class FolderTreeNode { constructor( @@ -24,7 +25,7 @@ export class FolderHierarchy { static newFromEntities(entities: ArFSFileOrFolderEntity[]): FolderHierarchy { const folderIdToEntityMap = entities.reduce((accumulator, entity) => { - return Object.assign(accumulator, { [entity.entityId]: entity }); + return Object.assign(accumulator, { [`${entity.entityId}`]: entity }); }, {}); const folderIdToNodeMap: { [k: string]: FolderTreeNode } = {}; @@ -40,24 +41,24 @@ export class FolderHierarchy { folderIdToEntityMap: { [k: string]: ArFSFileOrFolderEntity }, folderIdToNodeMap: { [k: string]: FolderTreeNode } ): void { - const folderIdKeyIsPresent = Object.keys(folderIdToNodeMap).includes(entity.entityId); - const parentFolderIdKeyIsPresent = Object.keys(folderIdToNodeMap).includes(entity.parentFolderId); + const folderIdKeyIsPresent = Object.keys(folderIdToNodeMap).includes(`${entity.entityId}`); + const parentFolderIdKeyIsPresent = Object.keys(folderIdToNodeMap).includes(`${entity.parentFolderId}`); if (!folderIdKeyIsPresent) { if (!parentFolderIdKeyIsPresent) { - const parentFolderEntity = folderIdToEntityMap[entity.parentFolderId]; + const parentFolderEntity = folderIdToEntityMap[`${entity.parentFolderId}`]; if (parentFolderEntity) { this.setupNodesWithEntity(parentFolderEntity, folderIdToEntityMap, folderIdToNodeMap); } } - const parent = folderIdToNodeMap[entity.parentFolderId]; + const parent = folderIdToNodeMap[`${entity.parentFolderId}`]; if (parent) { const node = new FolderTreeNode(entity.entityId, parent); parent.children.push(node); - folderIdToNodeMap[entity.entityId] = node; + folderIdToNodeMap[`${entity.entityId}`] = node; } else { // this one is supposed to be the new root const rootNode = new FolderTreeNode(entity.entityId); - folderIdToNodeMap[entity.entityId] = rootNode; + folderIdToNodeMap[`${entity.entityId}`] = rootNode; } } } @@ -69,7 +70,7 @@ export class FolderHierarchy { const someFolderId = Object.keys(this.folderIdToEntityMap)[0]; let tmpNode = this.folderIdToNodeMap[someFolderId]; - while (tmpNode.parent && this.folderIdToNodeMap[tmpNode.parent.folderId]) { + while (tmpNode.parent && this.folderIdToNodeMap[`${tmpNode.parent.folderId}`]) { tmpNode = tmpNode.parent; } this._rootNode = tmpNode; @@ -77,22 +78,22 @@ export class FolderHierarchy { } public subTreeOf(folderId: FolderID, maxDepth = Number.MAX_SAFE_INTEGER): FolderHierarchy { - const newRootNode = this.folderIdToNodeMap[folderId]; + const newRootNode = this.folderIdToNodeMap[`${folderId}`]; const subTreeNodes = this.nodeAndChildrenOf(newRootNode, maxDepth); const entitiesMapping = subTreeNodes.reduce((accumulator, node) => { - return Object.assign(accumulator, { [node.folderId]: this.folderIdToEntityMap[node.folderId] }); + return Object.assign(accumulator, { [`${node.folderId}`]: this.folderIdToEntityMap[`${node.folderId}`] }); }, {}); const nodesMapping = subTreeNodes.reduce((accumulator, node) => { - return Object.assign(accumulator, { [node.folderId]: node }); + return Object.assign(accumulator, { [`${node.folderId}`]: node }); }, {}); return new FolderHierarchy(entitiesMapping, nodesMapping); } public allFolderIDs(): FolderID[] { - return Object.keys(this.folderIdToEntityMap); + return Object.keys(this.folderIdToEntityMap).map((eid) => EID(eid)); } public nodeAndChildrenOf(node: FolderTreeNode, maxDepth: number): FolderTreeNode[] { @@ -106,7 +107,7 @@ export class FolderHierarchy { } public folderIdSubtreeFromFolderId(folderId: FolderID, maxDepth: number): FolderID[] { - const rootNode = this.folderIdToNodeMap[folderId]; + const rootNode = this.folderIdToNodeMap[`${folderId}`]; const subTree: FolderID[] = [rootNode.folderId]; switch (maxDepth) { case -1: @@ -129,18 +130,18 @@ export class FolderHierarchy { if (this.rootNode.parent) { throw new Error(`Can't compute paths from sub-tree`); } - if (folderId === 'root folder') { + if (`${folderId}` === ROOT_FOLDER_ID_PLACEHOLDER) { return '/'; } - let folderNode = this.folderIdToNodeMap[folderId]; + let folderNode = this.folderIdToNodeMap[`${folderId}`]; const nodesInPathToFolder = [folderNode]; - while (folderNode.parent && folderNode.folderId !== this.rootNode.folderId) { + while (folderNode.parent && !folderNode.folderId.equals(this.rootNode.folderId)) { folderNode = folderNode.parent; nodesInPathToFolder.push(folderNode); } const olderFirstNodesInPathToFolder = nodesInPathToFolder.reverse(); const olderFirstNamesOfNodesInPath = olderFirstNodesInPathToFolder.map( - (n) => this.folderIdToEntityMap[n.folderId].name + (n) => this.folderIdToEntityMap[`${n.folderId}`].name ); const stringPath = olderFirstNamesOfNodesInPath.join('/'); return `/${stringPath}/`; @@ -150,12 +151,12 @@ export class FolderHierarchy { if (this.rootNode.parent) { throw new Error(`Can't compute paths from sub-tree`); } - if (folderId === 'root folder') { + if (`${folderId}` === ROOT_FOLDER_ID_PLACEHOLDER) { return '/'; } - let folderNode = this.folderIdToNodeMap[folderId]; + let folderNode = this.folderIdToNodeMap[`${folderId}`]; const nodesInPathToFolder = [folderNode]; - while (folderNode.parent && folderNode.folderId !== this.rootNode.folderId) { + while (folderNode.parent && !folderNode.folderId.equals(this.rootNode.folderId)) { folderNode = folderNode.parent; nodesInPathToFolder.push(folderNode); } @@ -169,18 +170,18 @@ export class FolderHierarchy { if (this.rootNode.parent) { throw new Error(`Can't compute paths from sub-tree`); } - if (folderId === 'root folder') { + if (`${folderId}` === ROOT_FOLDER_ID_PLACEHOLDER) { return '/'; } - let folderNode = this.folderIdToNodeMap[folderId]; + let folderNode = this.folderIdToNodeMap[`${folderId}`]; const nodesInPathToFolder = [folderNode]; - while (folderNode.parent && folderNode.folderId !== this.rootNode.folderId) { + while (folderNode.parent && !folderNode.folderId.equals(this.rootNode.folderId)) { folderNode = folderNode.parent; nodesInPathToFolder.push(folderNode); } const olderFirstNodesInPathToFolder = nodesInPathToFolder.reverse(); const olderFirstTxTDsOfNodesInPath = olderFirstNodesInPathToFolder.map( - (n) => this.folderIdToEntityMap[n.folderId].txId + (n) => this.folderIdToEntityMap[`${n.folderId}`].txId ); const stringPath = olderFirstTxTDsOfNodesInPath.join('/'); return `/${stringPath}/`; diff --git a/src/index.ts b/src/index.ts index ea5b3d13..418a8163 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,15 @@ import { Wallet, WalletDAO } from './wallet'; import Arweave from 'arweave'; import { ArDriveCommunityOracle } from './community/ardrive_community_oracle'; -import { ArDrive, ArDriveAnonymous } from './ardrive'; +import { ArDrive } from './ardrive'; import { ArFSDAO } from './arfsdao'; import { ARDataPriceEstimator } from './utils/ar_data_price_estimator'; import { ARDataPriceRegressionEstimator } from './utils/ar_data_price_regression_estimator'; import { FeeMultiple } from './types'; import { CommunityOracle } from './community/community_oracle'; import { ArFSDAOAnonymous } from './arfsdao_anonymous'; +import { CLICommand } from './CLICommand'; +import { ArDriveAnonymous } from './ardrive_anonymous'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version: CLI_APP_VERSION } = require('../package.json'); @@ -18,7 +20,13 @@ if (require.main === module) { // declare all parameters import('./parameter_declarations').then(() => { // declares the commands - import('./commands'); + import('./commands').then(() => { + try { + CLICommand.parse(); + } catch { + // do nothing, commander already logs the error + } + }); }); } diff --git a/src/parameter_declarations.ts b/src/parameter_declarations.ts index e8a2c35d..dfe4fb29 100644 --- a/src/parameter_declarations.ts +++ b/src/parameter_declarations.ts @@ -40,6 +40,34 @@ export const DriveCreationPrivacyParameters = [ ]; export const DrivePrivacyParameters = [DriveKeyParameter, ...DriveCreationPrivacyParameters]; export const TreeDepthParams = [AllParameter, MaxDepthParameter]; +export const AllParameters = [ + WalletFileParameter, + SeedPhraseParameter, + PrivateParameter, + UnsafeDrivePasswordParameter, + DriveNameParameter, + FolderNameParameter, + DriveKeyParameter, + AddressParameter, + DriveIdParameter, + ArAmountParameter, + DestinationAddressParameter, + TransactionIdParameter, + ConfirmationsParameter, + FolderIdParameter, + FileIdParameter, + ParentFolderIdParameter, + LocalFilePathParameter, + DestinationFileNameParameter, + LocalFilesParameter, + GetAllRevisionsParameter, + AllParameter, + MaxDepthParameter, + BoostParameter, + DryRunParameter, + NoVerifyParameter +] as const; +export type ParameterName = typeof AllParameters[number]; export const ConflictResolutionParams = [SkipParameter, ReplaceParameter, UpsertParameter /* , AskParameter */]; @@ -246,7 +274,7 @@ Parameter.declare({ name: UpsertParameter, aliases: ['--upsert'], description: - '(OPTIONAL) When there is a name conflict within the destination folder, if that file was last modified at the same time as the file to upload, skip the upload, otherwise upload that file as a new revision', + '(OPTIONAL) When there is a name conflict within the destination folder, only upload file if a modification is detected. Skip otherwise.', type: 'boolean', forbiddenConjunctionParameters: [SkipParameter, ReplaceParameter] }); diff --git a/src/private_key_data.ts b/src/private_key_data.ts index bfd079d2..1b471373 100644 --- a/src/private_key_data.ts +++ b/src/private_key_data.ts @@ -1,10 +1,10 @@ import { deriveDriveKey, driveDecrypt, Utf8ArrayToStr } from 'ardrive-core-js'; -import { CipherIV, DriveID, DriveKey } from './types'; +import { CipherIV, DriveID, DriveKey, EntityID } from './types'; import { JWKWallet } from './wallet'; type DriveIdKeyPair = { [key: string /* DriveID */]: DriveKey }; -export type EntityMetaDataTransactionData = { [key: string]: string | number }; +export type EntityMetaDataTransactionData = { [key: string]: string | number | EntityID }; // Users may optionally supply any drive keys, a password, or a wallet interface PrivateKeyDataParams { @@ -63,7 +63,7 @@ export class PrivateKeyData { const decryptedDriveJSON = await this.decryptToJson(cipherIV, dataBuffer, driveKey); // Correct key, add this pair to the cache - this.driveKeyCache[driveId] = driveKey; + this.driveKeyCache[`${driveId}`] = driveKey; this.unverifiedDriveKeys = this.unverifiedDriveKeys.filter((k) => k !== driveKey); return decryptedDriveJSON; @@ -76,7 +76,7 @@ export class PrivateKeyData { if (this.password && this.wallet) { const derivedDriveKey: DriveKey = await deriveDriveKey( this.password, - driveId, + `${driveId}`, JSON.stringify(this.wallet.getPrivateKey()) ); @@ -84,7 +84,7 @@ export class PrivateKeyData { const decryptedDriveJSON = await this.decryptToJson(cipherIV, dataBuffer, derivedDriveKey); // Correct key, add this pair to the cache - this.driveKeyCache[driveId] = derivedDriveKey; + this.driveKeyCache[`${driveId}`] = derivedDriveKey; return decryptedDriveJSON; } catch (error) { @@ -113,6 +113,6 @@ export class PrivateKeyData { /** Synchronously returns a driveKey from the cache by its driveId */ public driveKeyForDriveId(driveId: DriveID): DriveKey | false { - return this.driveKeyCache[driveId] ?? false; + return this.driveKeyCache[`${driveId}`] ?? false; } } diff --git a/src/query.ts b/src/query.ts index ded9e394..4f3f2e9b 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,4 +1,4 @@ -import { ArweaveAddress } from './arweave_address'; +import { ArweaveAddress } from './types'; const ownerFragment = ` owner { diff --git a/src/types/ar.test.ts b/src/types/ar.test.ts new file mode 100644 index 00000000..124dae96 --- /dev/null +++ b/src/types/ar.test.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai'; +import { AR } from './'; + +describe('AR class', () => { + describe('from function', () => { + it('constructs AR without error when provided with positive decimal numbers with less than 13 decimal places', () => { + expect(() => AR.from(0)).to.not.throw(Error); + expect(() => AR.from(0.1)).to.not.throw(Error); + expect(() => AR.from(1.123456789012)).to.not.throw(Error); + expect(() => AR.from(Number.MAX_SAFE_INTEGER)).to.not.throw(Error); + }); + + // Not concerned with other number notations for now, e.g. scientific notation + it('constructs AR without error when provided with positive decimal strings with less than 13 decimal places', () => { + expect(() => AR.from('0')).to.not.throw(Error); + expect(() => AR.from('0.1')).to.not.throw(Error); + expect(() => AR.from('1.123456789012')).to.not.throw(Error); + expect(() => AR.from('18014398509481982')).to.not.throw(Error); + expect(() => AR.from('18014398509481982.123456789012')).to.not.throw(Error); + }); + + it('throws an error when provided with positive decimal numbers or strings with more than 13 decimal places', () => { + expect(() => AR.from(1.1234567890123)).to.throw(Error); + expect(() => AR.from('1.1234567890123')).to.throw(Error); + expect(() => AR.from('18014398509481982.1234567890123')).to.throw(Error); + }); + + it('throws an error when provided with negative numbers', () => { + expect(() => AR.from('-0.1')).to.throw(Error); + expect(() => AR.from('-18014398509481982.123456789012')).to.throw(Error); + }); + + it('throws an error when provided with a non-number string', () => { + expect(() => AR.from('abc')).to.throw(Error); + expect(() => AR.from('!!!')).to.throw(Error); + expect(() => AR.from('-')).to.throw(Error); + expect(() => AR.from('+')).to.throw(Error); + }); + }); + + describe('toString function', () => { + it('returns the AR value as a BigNumber string', () => { + expect(AR.from(0).toString()).to.equal('0'); + expect(AR.from('18014398509481982').toString()).to.equal('18014398509481982'); + expect(AR.from('18014398509481982.123456789012').toString()).to.equal('18014398509481982.123456789012'); + }); + }); + + describe('valueOf function', () => { + it('returns the AR value as a BigNumber string', () => { + expect(AR.from(0).valueOf()).to.equal('0'); + expect(AR.from('18014398509481982').valueOf()).to.equal('18014398509481982'); + expect(AR.from('18014398509481982.123456789012').valueOf()).to.equal('18014398509481982.123456789012'); + }); + }); + + describe('toWinston function', () => { + it('returns the Winston value as a BigNumber string', () => { + expect(AR.from('18014398509481982.123456789012').toWinston().toString()).to.equal( + '18014398509481982123456789012' + ); + }); + }); +}); diff --git a/src/types/ar.ts b/src/types/ar.ts new file mode 100644 index 00000000..2bead9ba --- /dev/null +++ b/src/types/ar.ts @@ -0,0 +1,35 @@ +import { BigNumber } from 'bignumber.js'; +import { W, Winston } from './winston'; + +export class AR { + constructor(readonly winston: Winston) {} + + static from(arValue: BigNumber.Value): AR { + const bigWinston = new BigNumber(arValue).shiftedBy(12); + const numDecimalPlaces = bigWinston.decimalPlaces(); + if (numDecimalPlaces > 0) { + throw new Error( + `The AR amount must have a maximum of 12 digits of precision, but got ${numDecimalPlaces + 12}` + ); + } + return new AR(W(bigWinston)); + } + + toString(): string { + BigNumber.config({ DECIMAL_PLACES: 12 }); + const w = new BigNumber(this.winston.toString(), 10); + return w.shiftedBy(-12).toFixed(); + } + + valueOf(): string { + return this.toString(); + } + + toWinston(): Winston { + return this.winston; + } + + toJSON(): string { + return this.toString(); + } +} diff --git a/src/types/arweave_address.test.ts b/src/types/arweave_address.test.ts new file mode 100644 index 00000000..56da08c9 --- /dev/null +++ b/src/types/arweave_address.test.ts @@ -0,0 +1,82 @@ +import { expect } from 'chai'; +import { ADDR, ArweaveAddress } from './arweave_address'; + +describe('The ArweaveAddress class', () => { + describe('constructor', () => { + it('creates a new address when given a valid address string', () => { + const validAddresses = [ + '-------------------------------------------', + '___________________________________________', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ', + '0000000000000000000000000000000000000000000', + '0123456789012345678901234567890123456789012', + 'g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0' + ]; + validAddresses.forEach((address) => { + expect(() => new ArweaveAddress(address)).to.not.throw(); + }); + }); + + it('throws an error for input addresses that are not 43 characters in length', () => { + const invalidAddresses = ['', '-', 'g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms01']; + invalidAddresses.forEach((badAddress) => { + expect(() => ADDR(badAddress)).to.throw(Error); + }); + }); + + it('throws an error for input addresses with invalid characters', () => { + const invalidAddresses = '!@#$%^&*()+=~`{[}]\\|;:\'"<,>.?/'.split('').map((char) => char.repeat(43)); + invalidAddresses.forEach((badAddress) => { + expect(() => ADDR(badAddress)).to.throw(Error); + }); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct ByteCount string when hint is string', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(`${address}`).to.equal('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + }); + + it('throws when hint is number', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(() => +address).to.throw(); + }); + }); + + describe('equals function', () => { + it('returns true for mathing addresses', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + const addressOther = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(address.equals(addressOther)).to.be.true; + }); + + it('returns false for different addresses', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + const addressOther = ADDR('a1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(address.equals(addressOther)).to.be.false; + }); + }); + + describe('toString function', () => { + it('returns the correct ArweaveAddress string', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(address.toString()).to.equal('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct ArweaveAddress string value', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(address.valueOf()).to.equal('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const address = ADDR('g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0'); + expect(JSON.stringify({ address })).to.equal('{"address":"g1hzNXVbh2M6LMQSUYp7HgkgxdadYqYEfw-HAajlms0"}'); + }); + }); +}); diff --git a/src/types/arweave_address.ts b/src/types/arweave_address.ts new file mode 100644 index 00000000..1d3998d6 --- /dev/null +++ b/src/types/arweave_address.ts @@ -0,0 +1,39 @@ +import { Equatable } from './equatable'; + +export class ArweaveAddress implements Equatable { + constructor(private readonly address: string) { + if (!address.match(new RegExp('^[a-zA-Z0-9_-]{43}$'))) { + throw new Error( + 'Arweave addresses must be 43 characters in length with characters in the following set: [a-zA-Z0-9_-]' + ); + } + } + + [Symbol.toPrimitive](hint?: string): string { + if (hint === 'number') { + throw new Error('Arweave addresses cannot be interpreted as a number!'); + } + + return this.toString(); + } + + equals(other: ArweaveAddress): boolean { + return this.address === other.address; + } + + toString(): string { + return this.address; + } + + valueOf(): string { + return this.address; + } + + toJSON(): string { + return this.toString(); + } +} + +export function ADDR(arAddress: string): ArweaveAddress { + return new ArweaveAddress(arAddress); +} diff --git a/src/types/byte_count.test.ts b/src/types/byte_count.test.ts new file mode 100644 index 00000000..aab5ca5a --- /dev/null +++ b/src/types/byte_count.test.ts @@ -0,0 +1,65 @@ +import { expect } from 'chai'; +import { ByteCount } from './'; + +describe('ByteCount class', () => { + describe('constructor', () => { + it('constructs valid ByteCounts given healthy inputs', () => { + const byteCountInputs = [0, 1, Number.MAX_SAFE_INTEGER]; + byteCountInputs.forEach((byteCount) => expect(() => new ByteCount(byteCount)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const byteCountInputs = [-1, 0.5, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NaN]; + byteCountInputs.forEach((byteCount) => + expect(() => new ByteCount(byteCount), `${byteCount} should throw`).to.throw(Error) + ); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct ByteCount string when hint is string', () => { + const byteCount = new ByteCount(12345); + expect(`${byteCount}`).to.equal('12345'); + }); + + it('returns the correct ByteCount number when hint is number', () => { + const byteCount = new ByteCount(12345); + expect(+byteCount).to.equal(12345); + }); + }); + + describe('toString function', () => { + it('returns the correct ByteCount string', () => { + const byteCount = new ByteCount(12345); + expect(byteCount.toString()).to.equal('12345'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct ByteCount number value', () => { + const eid = new ByteCount(12345); + expect(eid.valueOf()).to.equal(12345); + }); + }); + + describe('equals function', () => { + it('correctly evaluates equality', () => { + const bc1 = new ByteCount(12345); + const bc2 = new ByteCount(12345); + const bc3 = new ByteCount(0); + expect(bc1.equals(bc2), `${bc1} and ${bc2}`).to.be.true; + expect(bc2.equals(bc1), `${bc2} and ${bc1}`).to.be.true; + expect(bc1.equals(bc3), `${bc1} and ${bc3}`).to.be.false; + expect(bc3.equals(bc1), `${bc3} and ${bc1}`).to.be.false; + expect(bc2.equals(bc3), `${bc2} and ${bc3}`).to.be.false; + expect(bc3.equals(bc2), `${bc3} and ${bc2}`).to.be.false; + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const byteCount = new ByteCount(12345); + expect(JSON.stringify({ byteCount })).to.equal('{"byteCount":12345}'); + }); + }); +}); diff --git a/src/types/byte_count.ts b/src/types/byte_count.ts new file mode 100644 index 00000000..85a94c7e --- /dev/null +++ b/src/types/byte_count.ts @@ -0,0 +1,33 @@ +import { Equatable } from './equatable'; + +export class ByteCount implements Equatable { + constructor(private readonly byteCount: number) { + if (!Number.isFinite(this.byteCount) || !Number.isInteger(this.byteCount) || this.byteCount < 0) { + throw new Error('Byte count must be a non-negative integer value!'); + } + } + + [Symbol.toPrimitive](hint?: string): number | string { + if (hint === 'string') { + this.toString(); + } + + return this.byteCount; + } + + toString(): string { + return `${this.byteCount}`; + } + + valueOf(): number { + return this.byteCount; + } + + toJSON(): number { + return this.byteCount; + } + + equals(other: ByteCount): boolean { + return this.byteCount === other.byteCount; + } +} diff --git a/src/types/entity_id.test.ts b/src/types/entity_id.test.ts new file mode 100644 index 00000000..6e24da25 --- /dev/null +++ b/src/types/entity_id.test.ts @@ -0,0 +1,85 @@ +import { expect } from 'chai'; +import { EID, EntityID } from './'; + +describe('EntityID class', () => { + describe('constructor', () => { + it('constructs valid EntityIDs given healthy inputs', () => { + const eidInputs = [ + '00000000-0000-0000-0000-000000000000', + '99999999-9999-9999-9999-999999999999', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'ffffffff-ffff-ffff-ffff-ffffffffffff', + 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA', + 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF' + ]; + eidInputs.forEach((eid) => expect(() => new EntityID(eid)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const eidInputs = [ + '', + '9999999909999-9999-9999-999999999999', + '99999999-999909999-9999-999999999999', + '99999999-9999-999909999-999999999999', + '99999999-9999-9999-99990999999999999', + 'gggggggg-gggg-gggg-gggg-gggggggggggg', + '!@#$%^&*-()_+-=;"<->,./-?-----------' + ]; + eidInputs.forEach((eid) => expect(() => new EntityID(eid), `${eid} should throw`).to.throw(Error)); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct Entity ID string when hint is string', () => { + const eid = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(`${eid}`).to.equal('01234567-89ab-cdef-0000-000000000000'); + }); + + it('throws an error when hint is number', () => { + const eid = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(() => +eid).to.throw(Error); + }); + }); + + describe('toString function', () => { + it('returns the correct Entity ID string', () => { + const eid = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(eid.toString()).to.equal('01234567-89ab-cdef-0000-000000000000'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct Entity ID string value', () => { + const eid = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(eid.valueOf()).to.equal('01234567-89ab-cdef-0000-000000000000'); + }); + }); + + describe('equals function', () => { + it('correctly evaluates equality', () => { + const eid1 = new EntityID('01234567-89ab-cdef-0000-000000000000'); + const eid2 = new EntityID('01234567-89ab-cdef-0000-000000000000'); + const eid3 = new EntityID('01234567-89ab-cdef-0000-000000000001'); + expect(eid1.equals(eid2), `${eid1} and ${eid2}`).to.be.true; + expect(eid2.equals(eid1), `${eid2} and ${eid1}`).to.be.true; + expect(eid1.equals(eid3), `${eid1} and ${eid3}`).to.be.false; + expect(eid3.equals(eid1), `${eid3} and ${eid1}`).to.be.false; + expect(eid2.equals(eid3), `${eid2} and ${eid3}`).to.be.false; + expect(eid3.equals(eid2), `${eid3} and ${eid2}`).to.be.false; + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const entityId = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(JSON.stringify({ entityId })).to.equal('{"entityId":"01234567-89ab-cdef-0000-000000000000"}'); + }); + }); +}); + +describe('EID function', () => { + it('returns the correct EntityID', () => { + const expected = new EntityID('01234567-89ab-cdef-0000-000000000000'); + expect(`${EID('01234567-89ab-cdef-0000-000000000000')}`).to.equal(`${expected}`); + }); +}); diff --git a/src/types/entity_id.ts b/src/types/entity_id.ts new file mode 100644 index 00000000..4dcbeb41 --- /dev/null +++ b/src/types/entity_id.ts @@ -0,0 +1,40 @@ +import { Equatable } from './equatable'; + +// RFC 4122 Section 3 requires that the characters be generated in lower case, while being case-insensitive on input. +const entityIdRegex = /^[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}$/i; + +export class EntityID implements Equatable { + constructor(protected entityId: string) { + if (!entityId.match(entityIdRegex)) { + throw new Error(`Invalid entity ID '${entityId}'!'`); + } + } + + [Symbol.toPrimitive](hint?: string): string { + if (hint === 'number') { + throw new Error('Entity IDs cannot be interpreted as a number!'); + } + + return this.toString(); + } + + toString(): string { + return this.entityId; + } + + valueOf(): string { + return this.entityId; + } + + equals(entityId: EntityID): boolean { + return this.entityId === entityId.entityId; + } + + toJSON(): string { + return this.toString(); + } +} + +export function EID(entityId: string): EntityID { + return new EntityID(entityId); +} diff --git a/src/types/equatable.ts b/src/types/equatable.ts new file mode 100644 index 00000000..6f88cc31 --- /dev/null +++ b/src/types/equatable.ts @@ -0,0 +1,3 @@ +export interface Equatable { + equals(other: T): boolean; +} diff --git a/src/types/fee_multiple.test.ts b/src/types/fee_multiple.test.ts new file mode 100644 index 00000000..af33b145 --- /dev/null +++ b/src/types/fee_multiple.test.ts @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import { FeeMultiple } from './'; + +describe('FeeMultiple class', () => { + describe('constructor', () => { + it('constructs valid FeeMultiples given healthy inputs', () => { + const feeMultiples = [1.0, 1.1, Number.MAX_SAFE_INTEGER]; + feeMultiples.forEach((feeMultiple) => expect(() => new FeeMultiple(feeMultiple)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const feeMultiples = [0, 0.999999999999, -1.0, Number.POSITIVE_INFINITY, Number.NaN]; + feeMultiples.forEach((feeMultiple) => + expect(() => new FeeMultiple(feeMultiple), `${feeMultiple} should throw`).to.throw(Error) + ); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct FeeMultiple string when hint is string', () => { + const feeMultiple = new FeeMultiple(2.5); + expect(`${feeMultiple}`).to.equal('2.5'); + }); + + it('returns the correct FeeMultiple string when hint is number', () => { + // TODO: very big numbers and long decimals + const feeMultiple = new FeeMultiple(2.5); + expect(+feeMultiple).to.equal(2.5); + }); + }); + + describe('toString function', () => { + it('returns the correct FeeMultiple string', () => { + const feeMultiple = new FeeMultiple(2.123456789); + expect(feeMultiple.toString()).to.equal('2.123456789'); + }); + + it('returns rounded FeeMultiple strings when precision is excessive', () => { + const feeMultipleRoundDown = new FeeMultiple(2.000000000000000000000000000000001); + expect(`${feeMultipleRoundDown}`).to.equal('2'); + + const feeMultipleRoundUp = new FeeMultiple(0.999999999999999999999999999); + expect(`${feeMultipleRoundUp}`).to.equal('1'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct FeeMultiple number value', () => { + const feeMultiple = new FeeMultiple(2.123456789); + expect(feeMultiple.valueOf()).to.equal(2.123456789); + }); + }); + + describe('wouldBoostReward function', () => { + it('returns true when the FeeMultiple > 1.0', () => { + const feeMultiple = new FeeMultiple(1.0001); + expect(feeMultiple.wouldBoostReward()).to.be.true; + }); + + it('returns false when the FeeMultiple equals 1.0', () => { + const feeMultiple = new FeeMultiple(1); + expect(feeMultiple.wouldBoostReward()).to.be.false; + }); + }); + + describe('boostReward function', () => { + it('boosts an input reward and rounds up', () => { + const feeMultiple = new FeeMultiple(1.56789); + expect(feeMultiple.boostReward('3')).to.equal('5'); + }); + + it('can boost large rewards', () => { + const feeMultiple = new FeeMultiple(2); + expect(feeMultiple.boostReward(`${Number.MAX_SAFE_INTEGER}`)).to.equal('18014398509481982'); + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const feeMultiple = new FeeMultiple(1.5); + expect(JSON.stringify({ feeMultiple })).to.equal('{"feeMultiple":1.5}'); + }); + }); +}); diff --git a/src/types/fee_multiple.ts b/src/types/fee_multiple.ts new file mode 100644 index 00000000..e12e380c --- /dev/null +++ b/src/types/fee_multiple.ts @@ -0,0 +1,38 @@ +import BigNumber from 'bignumber.js'; + +export class FeeMultiple { + constructor(private readonly feeMultiple: number) { + if (this.feeMultiple < 1.0 || Number.isNaN(feeMultiple) || !Number.isFinite(feeMultiple)) { + throw new Error('Fee multiple must be >= 1.0!'); + } + } + + [Symbol.toPrimitive](hint?: string): string | number { + if (hint === 'string') { + return this.toString(); + } + + return this.feeMultiple; + } + + toString(): string { + return `${this.feeMultiple}`; + } + + valueOf(): number { + return this.feeMultiple; + } + + toJSON(): number { + return this.feeMultiple; + } + + wouldBoostReward(): boolean { + return this.feeMultiple > 1.0; + } + + boostReward(reward: string): string { + // Round up with because fractional Winston will cause an Arweave API failure + return new BigNumber(reward).times(new BigNumber(this.feeMultiple)).toFixed(0, BigNumber.ROUND_UP); + } +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..4ea0609d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,10 @@ +export * from './ar'; +export * from './arweave_address'; +export * from './entity_id'; +export * from './transaction_id'; +export * from './winston'; +export * from './seed_phrase'; +export * from './fee_multiple'; +export * from './byte_count'; +export * from './unix_time'; +export * from './types'; diff --git a/src/types/seed_phrase.test.ts b/src/types/seed_phrase.test.ts new file mode 100644 index 00000000..811f645c --- /dev/null +++ b/src/types/seed_phrase.test.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { SeedPhrase } from './seed_phrase'; + +describe('SeedPhrase class', () => { + describe('constructor', () => { + it('constructs valid SeedPhrase given healthy input', () => { + const eidInputs = ['the quick brown fox jumps over the lazy dog every single day']; + eidInputs.forEach((seedPhrase) => expect(() => new SeedPhrase(seedPhrase)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const eidInputs = [ + '', + 'the quick brown fox jumps over the lazy dog', + 'the quick brown fox jumps over the lazy dog every single day except today', + '99999999-9999-999909999-999999999999' + ]; + eidInputs.forEach((seedPhrase) => + expect(() => new SeedPhrase(seedPhrase), `${seedPhrase} should throw`).to.throw(Error) + ); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct Seed Phrase string when hint is string', () => { + const eid = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + expect(`${eid}`).to.equal('the quick brown fox jumps over the lazy dog every single day'); + }); + + it('throws an error when hint is number', () => { + const seedPhrase = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + expect(() => +seedPhrase).to.throw(Error); + }); + }); + + describe('toString function', () => { + it('returns the correct Seed Phrase string', () => { + const seedPhrase = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + expect(seedPhrase.toString()).to.equal('the quick brown fox jumps over the lazy dog every single day'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct Seed Phrase string value', () => { + const seedPhrase = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + expect(seedPhrase.valueOf()).to.equal('the quick brown fox jumps over the lazy dog every single day'); + }); + }); + + describe('equals function', () => { + it('correctly evaluates equality', () => { + const seedPhrase1 = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + const seedPhrase2 = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + const seedPhrase3 = new SeedPhrase('the quick brown fox jumps over the lazy dog every other day'); + expect(seedPhrase1.equals(seedPhrase2), `${seedPhrase1} and ${seedPhrase2}`).to.be.true; + expect(seedPhrase2.equals(seedPhrase1), `${seedPhrase2} and ${seedPhrase1}`).to.be.true; + expect(seedPhrase1.equals(seedPhrase3), `${seedPhrase1} and ${seedPhrase3}`).to.be.false; + expect(seedPhrase3.equals(seedPhrase1), `${seedPhrase3} and ${seedPhrase1}`).to.be.false; + expect(seedPhrase2.equals(seedPhrase3), `${seedPhrase2} and ${seedPhrase3}`).to.be.false; + expect(seedPhrase3.equals(seedPhrase2), `${seedPhrase3} and ${seedPhrase2}`).to.be.false; + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const seedPhrase = new SeedPhrase('the quick brown fox jumps over the lazy dog every single day'); + expect(JSON.stringify({ seedPhrase })).to.equal( + '{"seedPhrase":"the quick brown fox jumps over the lazy dog every single day"}' + ); + }); + }); +}); diff --git a/src/types/seed_phrase.ts b/src/types/seed_phrase.ts new file mode 100644 index 00000000..063f13ec --- /dev/null +++ b/src/types/seed_phrase.ts @@ -0,0 +1,35 @@ +import { Equatable } from './equatable'; + +const seedPhraseRegex = /^(\b[a-z]+\b(\s+\b|$)){12}$/i; + +export class SeedPhrase implements Equatable { + constructor(private readonly seedPhrase: string) { + if (!this.seedPhrase.match(seedPhraseRegex)) { + throw new Error(`'${this.seedPhrase}' is not a valid 12 word seed phrase!`); + } + } + + [Symbol.toPrimitive](hint?: string): string { + if (hint === 'number') { + throw new Error('Seed phrase cannot be interpreted as a number!'); + } + + return this.toString(); + } + + toString(): string { + return this.seedPhrase; + } + + valueOf(): string { + return this.seedPhrase; + } + + toJSON(): string { + return this.toString(); + } + + equals(other: SeedPhrase): boolean { + return this.seedPhrase === other.seedPhrase; + } +} diff --git a/src/types/transaction_id.test.ts b/src/types/transaction_id.test.ts new file mode 100644 index 00000000..ab8528ff --- /dev/null +++ b/src/types/transaction_id.test.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import { TransactionID, TxID } from './'; + +describe('TransactionID class', () => { + describe('constructor', () => { + it('constructs valid TransactionIDs given healthy inputs', () => { + const txidInputs = [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ', + '0000000000000000000000000000000000000000000', + '9999999999999999999999999999999999999999999', + '-------------------------------------------', + '___________________________________________', + 'abcdefghijklmnopqrstuvwxyz0123456789_-ABCXY' + ]; + txidInputs.forEach((txid) => expect(() => new TransactionID(txid)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const txidInputs = [ + '', + ' ', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + '!@#$%^&*()+={}[]|;"<>/?}ZZZZZZZZZZZZZZZZZZZZ' + ]; + txidInputs.forEach((txid) => expect(() => new TransactionID(txid)).to.throw(Error)); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct Transaction ID string when hint is string', () => { + const txid = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(`${txid}`).to.equal('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + }); + + it('throws an error when hint is number', () => { + const txid = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(() => +txid).to.throw(Error); + }); + }); + + describe('toString function', () => { + it('returns the correct Transaction ID string', () => { + const txid = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(txid.toString()).to.equal('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct Transaction ID string value', () => { + const txid = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(txid.valueOf()).to.equal('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + }); + }); + + describe('equals function', () => { + it('correctly evaluates equality', () => { + const txid1 = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + const txid2 = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + const txid3 = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vR'); + expect(txid1.equals(txid2), `${txid1} and ${txid2}`).to.be.true; + expect(txid2.equals(txid1), `${txid2} and ${txid1}`).to.be.true; + expect(txid1.equals(txid3), `${txid1} and ${txid3}`).to.be.false; + expect(txid3.equals(txid1), `${txid3} and ${txid1}`).to.be.false; + expect(txid2.equals(txid3), `${txid2} and ${txid3}`).to.be.false; + expect(txid3.equals(txid2), `${txid3} and ${txid2}`).to.be.false; + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const txId = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(JSON.stringify({ txId })).to.equal('{"txId":"XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP"}'); + }); + }); +}); + +describe('TxID function', () => { + it('returns the correct Transaction ID', () => { + const expected = new TransactionID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP'); + expect(`${TxID('XHGs4-ibl7Bct2Vqt4LDq9FxzjxU7CqqETg9oFz83vP')}`).to.equal(`${expected}`); + }); +}); diff --git a/src/types/transaction_id.ts b/src/types/transaction_id.ts new file mode 100644 index 00000000..8d2102be --- /dev/null +++ b/src/types/transaction_id.ts @@ -0,0 +1,42 @@ +import { Equatable } from './equatable'; + +const trxIdRegex = /^(\w|-){43}$/; +export class TransactionID implements Equatable { + constructor(private readonly transactionId: string) { + if (!transactionId.match(trxIdRegex)) { + throw new Error( + 'Transaction ID should be a 43-character, alphanumeric string potentially including "=" and "_" characters.' + ); + } + } + + [Symbol.toPrimitive](hint?: string): string { + if (hint === 'number') { + throw new Error('Transaction IDs cannot be interpreted as a number!'); + } + + return this.toString(); + } + + toString(): string { + return this.transactionId; + } + + valueOf(): string { + return this.transactionId; + } + + equals(entityId: TransactionID): boolean { + return this.transactionId === entityId.transactionId; + } + + toJSON(): string { + return this.toString(); + } +} + +export function TxID(transactionId: string): TransactionID { + return new TransactionID(transactionId); +} + +export const stubTransactionID = TxID('0000000000000000000000000000000000000000000'); diff --git a/src/types.ts b/src/types/types.ts similarity index 65% rename from src/types.ts rename to src/types/types.ts index cf8e06ab..d9ec7e3c 100644 --- a/src/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import { EntityID, Winston, FeeMultiple } from '.'; + export const ArFS_O_11 = '0.11'; export const CURRENT_ARFS_VERSION = ArFS_O_11; export const DEFAULT_APP_NAME = 'ArDrive-Core'; @@ -8,37 +10,29 @@ export const PRIVATE_CONTENT_TYPE = 'application/octet-stream'; export const MANIFEST_CONTENT_TYPE = 'application/x.arweave-manifest+json'; export type PublicKey = string; -export type SeedPhrase = string; -/** TODO: Use big int library on Winston types */ -export type Winston = string; // TODO: make a type that checks validity export type NetworkReward = Winston; -export type FolderID = string; -export type FileID = string; -export type DriveID = string; -export type EntityID = DriveID | FolderID | FileID; +export type FolderID = EntityID; +export type FileID = EntityID; +export type DriveID = EntityID; +export type AnyEntityID = DriveID | FolderID | FileID; export type CipherIV = string; export type EntityKey = Buffer; export type DriveKey = EntityKey; export type FileKey = EntityKey; -export type UnixTime = number; -export type ByteCount = number; export type DataContentType = string; -export type TransactionID = string; // TODO: make a type that checks lengths - export interface ArDriveCommunityTip { tipPercentage: number; - minWinstonFee: number; // TODO: Align with Winston type? + minWinstonFee: Winston; } export type TipType = 'data upload'; export type GQLCursor = string; -export type FeeMultiple = number; // TODO: assert always >= 1.0 export type RewardSettings = { reward?: Winston; @@ -48,7 +42,11 @@ export type RewardSettings = { type Omit = Pick>; export type MakeOptional = Omit & Partial; -// These interfaces taken from arweave-deploy +export type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +// These manifest interfaces taken from arweave-deploy export interface ManifestPathMap { [index: string]: { id: string }; } diff --git a/src/types/unix_time.test.ts b/src/types/unix_time.test.ts new file mode 100644 index 00000000..3f4f4af4 --- /dev/null +++ b/src/types/unix_time.test.ts @@ -0,0 +1,51 @@ +import { expect } from 'chai'; +import { UnixTime } from './'; + +describe('UnixTime class', () => { + describe('constructor', () => { + it('constructs valid UnixTimes given healthy inputs', () => { + const unixTimeInputs = [0, 1, Number.MAX_SAFE_INTEGER]; + unixTimeInputs.forEach((unixTime) => expect(() => new UnixTime(unixTime)).to.not.throw(Error)); + }); + + it('throws an error when provided invalid inputs', () => { + const unixTimeInputs = [-1, 0.5, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NaN]; + unixTimeInputs.forEach((unixTime) => + expect(() => new UnixTime(unixTime), `${unixTime} should throw`).to.throw(Error) + ); + }); + }); + + describe('toPrimitive function', () => { + it('returns the correct UnixTime string when hint is string', () => { + const unixTime = new UnixTime(12345); + expect(`${unixTime}`).to.equal('12345'); + }); + + it('returns the correct UnixTime number when hint is number', () => { + const unixTime = new UnixTime(12345); + expect(+unixTime).to.equal(12345); + }); + }); + + describe('toString function', () => { + it('returns the correct UnixTime string', () => { + const unixTime = new UnixTime(12345); + expect(unixTime.toString()).to.equal('12345'); + }); + }); + + describe('valueOf function', () => { + it('returns the correct UnixTime number value', () => { + const eid = new UnixTime(12345); + expect(eid.valueOf()).to.equal(12345); + }); + }); + + describe('toJSON function', () => { + it('returns the correct JSON value', () => { + const unixTime = new UnixTime(12345); + expect(JSON.stringify({ unixTime })).to.equal('{"unixTime":12345}'); + }); + }); +}); diff --git a/src/types/unix_time.ts b/src/types/unix_time.ts new file mode 100644 index 00000000..271eee5a --- /dev/null +++ b/src/types/unix_time.ts @@ -0,0 +1,27 @@ +export class UnixTime { + constructor(private readonly unixTime: number) { + if (this.unixTime < 0 || !Number.isInteger(this.unixTime) || !Number.isFinite(this.unixTime)) { + throw new Error('Unix time must be a positive integer!'); + } + } + + [Symbol.toPrimitive](hint?: string): number | string { + if (hint === 'string') { + this.toString(); + } + + return this.unixTime; + } + + toString(): string { + return `${this.unixTime}`; + } + + valueOf(): number { + return this.unixTime; + } + + toJSON(): number { + return this.unixTime; + } +} diff --git a/src/types/winston.test.ts b/src/types/winston.test.ts new file mode 100644 index 00000000..9aeadc95 --- /dev/null +++ b/src/types/winston.test.ts @@ -0,0 +1,159 @@ +import { expect } from 'chai'; +import { Winston } from './winston'; + +describe('Winston class', () => { + describe('constructor', () => { + it('constructor throws an exception when a negative Winston value is provided', () => { + expect(() => new Winston(-1)).to.throw(Error); + expect(() => new Winston('-1')).to.throw(Error); + }); + + it('constructor throws an exception when a non-integer Winston value is provided', () => { + expect(() => new Winston(0.5)).to.throw(Error); + expect(() => new Winston('0.5')).to.throw(Error); + expect(() => new Winston('abc')).to.throw(Error); + expect(() => new Winston('!!!')).to.throw(Error); + expect(() => new Winston('-')).to.throw(Error); + expect(() => new Winston('+')).to.throw(Error); + }); + + it('constructor builds Winston values for positive integer number values without throwing an error', () => { + expect(() => new Winston(0)).to.not.throw(Error); + expect(() => new Winston(1)).to.not.throw(Error); + expect(() => new Winston(Number.MAX_SAFE_INTEGER)).to.not.throw(Error); + }); + + // Not concerned with other number notations for now, e.g. scientific notation + it('constructor builds Winston values for positive integer strings without throwing an error', () => { + expect(() => new Winston('0')).to.not.throw(Error); + expect(() => new Winston('1')).to.not.throw(Error); + }); + + it('constructor builds Winston values for positive BigNumber integer strings', () => { + expect(() => new Winston('18014398509481982')).to.not.throw(Error); + }); + }); + + describe('plus function', () => { + it('correctly sums up Winston values', () => { + expect(new Winston(1).plus(new Winston(2)).toString()).to.equal('3'); + }); + + it('correctly sums up Winston values in the BigNumber ranges', () => { + expect(new Winston(Number.MAX_SAFE_INTEGER).plus(new Winston(Number.MAX_SAFE_INTEGER)).toString()).to.equal( + '18014398509481982' + ); + }); + }); + + describe('minus function', () => { + it('correctly subtracts Winston values', () => { + expect(new Winston(2).minus(new Winston(1)).toString()).to.equal('1'); + }); + + it('correctly subtracts Winston values in the BigNumber ranges', () => { + expect(new Winston('18014398509481982').minus(new Winston(Number.MAX_SAFE_INTEGER)).toString()).to.equal( + '9007199254740991' + ); + }); + + it('throws an error when the subtraction result is less than 0', () => { + expect(() => new Winston(1).minus(new Winston(2))).to.throw(Error); + }); + }); + + describe('times function', () => { + it('correctly multiplies Winston values by whole and fractional numbres', () => { + expect(new Winston(2).times(3).toString()).to.equal('6'); + expect(new Winston(2).times(1.5).toString()).to.equal('3'); + }); + + it('correctly multiplies Winston values by whole and fractional BigNumbers', () => { + expect(new Winston(2).times(Number.MAX_SAFE_INTEGER).toString()).to.equal('18014398509481982'); + expect(new Winston(2).times('18014398509481982').toString()).to.equal('36028797018963964'); + }); + + it('rounds down multiplications that result in fractional numbers', () => { + expect(new Winston(2).times(1.6).toString()).to.equal('3'); + expect(new Winston(Number.MAX_SAFE_INTEGER).times(1.5).toString()).to.equal('13510798882111486'); + }); + + it('throws an error when the multiplying by negative numbers', () => { + expect(() => new Winston(1).times(-1)).to.throw(Error); + }); + }); + + describe('dividedBy function', () => { + it('correctly divides Winston values by whole and fractional numbres', () => { + expect(new Winston(6).dividedBy(3).toString()).to.equal('2'); + expect(new Winston(6).dividedBy(1.5).toString()).to.equal('4'); + }); + + it('correctly divides Winston values by whole and fractional BigNumbers', () => { + expect(new Winston('18014398509481982').dividedBy(Number.MAX_SAFE_INTEGER).toString()).to.equal('2'); + expect(new Winston('36028797018963965').dividedBy('18014398509481982.5').toString()).to.equal('2'); + }); + + it('rounds up divisions that result in fractional numbers', () => { + expect(new Winston(3).dividedBy(2).toString()).to.equal('2'); + expect(new Winston('13510798882111487').dividedBy(2).toString()).to.equal('6755399441055744'); + }); + + it('throws an error when dividing by negative numbers', () => { + expect(() => new Winston(1).dividedBy(-1)).to.throw(Error); + }); + }); + + describe('isGreaterThan function', () => { + it('returns false when other Winston is greater', () => { + expect(new Winston(1).isGreaterThan(new Winston(2))).to.be.false; + }); + + it('returns true when other Winston is lesser', () => { + expect(new Winston(2).isGreaterThan(new Winston(1))).to.be.true; + }); + + it('returns false when other Winston is equal', () => { + expect(new Winston(2).isGreaterThan(new Winston(2))).to.be.false; + }); + }); + + describe('difference function', () => { + it('can return a positive difference between Winstons', () => { + expect(Winston.difference(new Winston(2), new Winston(1))).to.equal('1'); + }); + + it('can return a negative difference between Winstons', () => { + expect(Winston.difference(new Winston(1), new Winston(2))).to.equal('-1'); + }); + }); + + describe('toString function', () => { + it('returns the Winston value as a BigNumber string', () => { + expect(new Winston(0).toString()).to.equal('0'); + expect(new Winston(1).toString()).to.equal('1'); + expect(new Winston('18014398509481982').toString()).to.equal('18014398509481982'); + }); + }); + + describe('valueOf function', () => { + it('returns the Winston value as a BigNumber string', () => { + expect(new Winston(0).valueOf()).to.equal('0'); + expect(new Winston(1).valueOf()).to.equal('1'); + expect(new Winston('18014398509481982').valueOf()).to.equal('18014398509481982'); + }); + }); + + describe('max function', () => { + it('correctly computes the max Winston value from an aritrarily large list of Winston values', () => { + expect( + `${Winston.max( + new Winston('18014398509481982'), + new Winston(Number.MAX_SAFE_INTEGER), + new Winston(1), + new Winston(0) + )}` + ).to.equal('18014398509481982'); + }); + }); +}); diff --git a/src/types/winston.ts b/src/types/winston.ts new file mode 100644 index 00000000..346d0a9e --- /dev/null +++ b/src/types/winston.ts @@ -0,0 +1,57 @@ +import { BigNumber } from 'bignumber.js'; + +export class Winston { + private amount: BigNumber; + constructor(amount: BigNumber.Value) { + this.amount = new BigNumber(amount); + if (this.amount.isLessThan(0) || !this.amount.isInteger()) { + throw new Error('Winston value should be a non-negative integer!'); + } + } + + plus(winston: Winston): Winston { + return W(this.amount.plus(winston.amount)); + } + + minus(winston: Winston): Winston { + return W(this.amount.minus(winston.amount)); + } + + times(multiplier: BigNumber.Value): Winston { + return W(this.amount.times(multiplier).decimalPlaces(0, BigNumber.ROUND_DOWN)); + } + + dividedBy(divisor: BigNumber.Value): Winston { + // TODO: Best rounding strategy? Up or down? + return W(this.amount.dividedBy(divisor).decimalPlaces(0, BigNumber.ROUND_CEIL)); + } + + isGreaterThan(winston: Winston): boolean { + return this.amount.isGreaterThan(winston.amount); + } + + static difference(a: Winston, b: Winston): string { + return a.amount.minus(b.amount).toString(); + } + + toString(): string { + return this.amount.toFixed(); + } + + valueOf(): string { + return this.amount.toFixed(); + } + + toJSON(): string { + return this.toString(); + } + + static max(...winstons: Winston[]): Winston { + BigNumber.max(); + return winstons.reduce((max, next) => (next.amount.isGreaterThan(max.amount) ? next : max)); + } +} + +export function W(amount: BigNumber.Value): Winston { + return new Winston(amount); +} diff --git a/src/utils/ar_data_price.test.ts b/src/utils/ar_data_price.test.ts deleted file mode 100644 index b6bca565..00000000 --- a/src/utils/ar_data_price.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from 'chai'; -import { ARDataPrice } from './ar_data_price'; - -describe('ARDataPrice class', () => { - it('constructor throws an exception when a negative data volume is provided', () => { - expect(() => new ARDataPrice(-1, -1)).to.throw(Error); - expect(() => new ARDataPrice(-1, 0)).to.throw(Error); - expect(() => new ARDataPrice(-1, 0.5)).to.throw(Error); - expect(() => new ARDataPrice(-1, 1)).to.throw(Error); - }); - - it('constructor throws an exception when a non-integer data volume is provided', () => { - expect(() => new ARDataPrice(0.5, -1)).to.throw(Error); - expect(() => new ARDataPrice(0.5, 0)).to.throw(Error); - expect(() => new ARDataPrice(0.5, 0.5)).to.throw(Error); - expect(() => new ARDataPrice(0.5, 1)).to.throw(Error); - }); - - it('constructor throws an exception when a negative Winston value is provided', () => { - expect(() => new ARDataPrice(-1, -1)).to.throw(Error); - expect(() => new ARDataPrice(0, -1)).to.throw(Error); - expect(() => new ARDataPrice(0.5, -1)).to.throw(Error); - expect(() => new ARDataPrice(1, -1)).to.throw(Error); - }); - - it('constructor throws an exception when a non-integer Winston value is provided', () => { - expect(() => new ARDataPrice(-1, 0.5)).to.throw(Error); - expect(() => new ARDataPrice(0, 0.5)).to.throw(Error); - expect(() => new ARDataPrice(0.5, 0.5)).to.throw(Error); - expect(() => new ARDataPrice(1, 0.5)).to.throw(Error); - }); - - it('constructs a valid object when zeros are provided', () => { - const actual = new ARDataPrice(0, 0); - expect(actual.numBytes).to.equal(0); - expect(actual.winstonPrice).to.equal(0); - }); - - it('constructs a valid object when non-zero values are provided', () => { - const actual = new ARDataPrice(1, 1); - expect(actual.numBytes).to.equal(1); - expect(actual.winstonPrice).to.equal(1); - }); - - it('constructs a valid object when max Int values are provided', () => { - const actual = new ARDataPrice(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); - expect(actual.numBytes).to.equal(Number.MAX_SAFE_INTEGER); - expect(actual.winstonPrice).to.equal(Number.MAX_SAFE_INTEGER); - }); -}); diff --git a/src/utils/ar_data_price.ts b/src/utils/ar_data_price.ts index c30dfefb..a37b74ca 100644 --- a/src/utils/ar_data_price.ts +++ b/src/utils/ar_data_price.ts @@ -1,22 +1,10 @@ -import { ByteCount } from '../types'; +import { ByteCount, Winston } from '../types'; /** - * Immutable data container class representing a market price in Winston for a particular volume + * Immutable data container type representing a market price in Winston for a particular volume * of data that enforces valid number ranges for byte counts and Winston price values. */ -export class ARDataPrice { - /** - * @returns an ARDataPrice instance with the given byte count and Winston amount - * @throws {@link Error} if negative or non-integer values are provided for either value - */ - constructor(readonly numBytes: ByteCount, readonly winstonPrice: number) { - if (numBytes < 0 || !Number.isInteger(numBytes) || winstonPrice < 0 || !Number.isInteger(winstonPrice)) { - throw new Error( - `numBytes (${numBytes}) and winstonPrice (${winstonPrice}) should be non-negative integer values.` - ); - } - - this.numBytes = numBytes; - this.winstonPrice = winstonPrice; - } +export interface ARDataPrice { + readonly numBytes: ByteCount; + readonly winstonPrice: Winston; } diff --git a/src/utils/ar_data_price_estimator.ts b/src/utils/ar_data_price_estimator.ts index 580551d1..ed03b216 100644 --- a/src/utils/ar_data_price_estimator.ts +++ b/src/utils/ar_data_price_estimator.ts @@ -1,14 +1,14 @@ -import type { ArDriveCommunityTip, ByteCount } from '../types'; +import { ArDriveCommunityTip, AR, ByteCount, Winston } from '../types'; export const arPerWinston = 0.000_000_000_001; export interface ARDataPriceEstimator { - getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise; - getARPriceForByteCount: (byteCount: ByteCount, arDriveCommunityTip: ArDriveCommunityTip) => Promise; + getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise; + getARPriceForByteCount: (byteCount: ByteCount, arDriveCommunityTip: ArDriveCommunityTip) => Promise; } export abstract class AbstractARDataPriceEstimator implements ARDataPriceEstimator { - abstract getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise; + abstract getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise; /** * Estimates the price in AR for a given byte count, including the ArDrive community tip @@ -16,25 +16,25 @@ export abstract class AbstractARDataPriceEstimator implements ARDataPriceEstimat async getARPriceForByteCount( byteCount: ByteCount, { minWinstonFee, tipPercentage }: ArDriveCommunityTip - ): Promise { + ): Promise { const winstonPrice = await this.getBaseWinstonPriceForByteCount(byteCount); - const communityWinstonFee = Math.max(winstonPrice * tipPercentage, minWinstonFee); + const communityWinstonFee = Winston.max(winstonPrice.times(tipPercentage), minWinstonFee); - const totalWinstonPrice = winstonPrice + communityWinstonFee; + const totalWinstonPrice = winstonPrice.plus(communityWinstonFee); - return totalWinstonPrice * arPerWinston; + return new AR(totalWinstonPrice); } } export interface ARDataCapacityEstimator { - getByteCountForWinston: (winston: number) => Promise; - getByteCountForAR: (arPrice: number, arDriveCommunityTip: ArDriveCommunityTip) => Promise; + getByteCountForWinston: (winston: Winston) => Promise; + getByteCountForAR: (arPrice: AR, arDriveCommunityTip: ArDriveCommunityTip) => Promise; } // prettier-ignore export abstract class AbstractARDataPriceAndCapacityEstimator extends AbstractARDataPriceEstimator implements ARDataCapacityEstimator { - abstract getByteCountForWinston(winston: number): Promise; + abstract getByteCountForWinston(winston: Winston): Promise; /** * Estimates the number of bytes that can be stored for a given amount of AR @@ -42,20 +42,16 @@ export abstract class AbstractARDataPriceAndCapacityEstimator extends AbstractAR * @remarks Returns 0 bytes when the price does not cover minimum ArDrive community fee */ public async getByteCountForAR( - arPrice: number, + arPrice: AR, { minWinstonFee, tipPercentage }: ArDriveCommunityTip - ): Promise { - const winstonPrice = arPrice / arPerWinston; - - const communityWinstonFee = Math.max(winstonPrice - winstonPrice / (1 + tipPercentage), minWinstonFee); - - const winstonPriceWithoutFee = Math.round(winstonPrice - communityWinstonFee); - - if (winstonPriceWithoutFee > 0) { - return this.getByteCountForWinston(winstonPriceWithoutFee); + ): Promise { + const winstonPrice = arPrice.toWinston(); + const communityWinstonFee = Winston.max(winstonPrice.minus(winstonPrice.dividedBy(1 + tipPercentage)), minWinstonFee); + if (winstonPrice.isGreaterThan(communityWinstonFee)) { + return this.getByteCountForWinston(winstonPrice.minus(communityWinstonFee)); } // Specified `arPrice` does not cover provided `minimumWinstonFee` - return 0; + return new ByteCount(0); } } diff --git a/src/utils/ar_data_price_oracle_estimator.ts b/src/utils/ar_data_price_oracle_estimator.ts index c99b8721..7536c169 100644 --- a/src/utils/ar_data_price_oracle_estimator.ts +++ b/src/utils/ar_data_price_oracle_estimator.ts @@ -1,7 +1,7 @@ import { GatewayOracle } from './gateway_oracle'; import type { ArweaveOracle } from './arweave_oracle'; import { AbstractARDataPriceEstimator } from './ar_data_price_estimator'; -import { ByteCount } from '../types'; +import { ByteCount, Winston } from '../types'; export const arPerWinston = 0.000_000_000_001; @@ -21,7 +21,7 @@ export class ARDataPriceOracleEstimator extends AbstractARDataPriceEstimator { * * @returns Promise for the price of an upload of size `byteCount` in Winston */ - public async getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise { + public async getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise { return this.oracle.getWinstonPriceForByteCount(byteCount); } } diff --git a/src/utils/ar_data_price_regression_estimator.test.ts b/src/utils/ar_data_price_regression_estimator.test.ts index 56cbc463..712028e8 100644 --- a/src/utils/ar_data_price_regression_estimator.test.ts +++ b/src/utils/ar_data_price_regression_estimator.test.ts @@ -3,56 +3,47 @@ import type { ArweaveOracle } from './arweave_oracle'; import { expect } from 'chai'; import { SinonStubbedInstance, stub } from 'sinon'; import { ARDataPriceRegressionEstimator } from './ar_data_price_regression_estimator'; -import { expectAsyncErrorThrow } from './test_helpers'; -import type { ArDriveCommunityTip } from '../types'; +import { ArDriveCommunityTip, W, AR, ByteCount } from '../types'; describe('ARDataPriceEstimator class', () => { let spyedOracle: SinonStubbedInstance; let calculator: ARDataPriceRegressionEstimator; - const arDriveCommunityTip: ArDriveCommunityTip = { minWinstonFee: 10, tipPercentage: 0.15 }; + const arDriveCommunityTip: ArDriveCommunityTip = { minWinstonFee: W(10), tipPercentage: 0.15 }; beforeEach(() => { // Set pricing algo up as x = y (bytes = Winston) // TODO: Get ts-sinon working with snowpack so we don't have to use a concrete type here spyedOracle = stub(new GatewayOracle()); - spyedOracle.getWinstonPriceForByteCount.callsFake((input) => Promise.resolve(input)); + spyedOracle.getWinstonPriceForByteCount.callsFake((input) => Promise.resolve(W(+input))); calculator = new ARDataPriceRegressionEstimator(true, spyedOracle); }); it('can be instantiated without making oracle calls', async () => { const gatewayOracleStub = stub(new GatewayOracle()); - gatewayOracleStub.getWinstonPriceForByteCount.callsFake(() => Promise.resolve(123)); + gatewayOracleStub.getWinstonPriceForByteCount.callsFake(() => Promise.resolve(W(123))); new ARDataPriceRegressionEstimator(true, gatewayOracleStub); expect(gatewayOracleStub.getWinstonPriceForByteCount.notCalled).to.be.true; }); it('makes 3 oracle calls during routine instantiation', async () => { const gatewayOracleStub = stub(new GatewayOracle()); - gatewayOracleStub.getWinstonPriceForByteCount.callsFake(() => Promise.resolve(123)); + gatewayOracleStub.getWinstonPriceForByteCount.callsFake(() => Promise.resolve(W(123))); new ARDataPriceRegressionEstimator(false, gatewayOracleStub); expect(gatewayOracleStub.getWinstonPriceForByteCount.calledThrice).to.be.true; }); it('makes three oracle calls after the first price estimation request', async () => { - await calculator.getBaseWinstonPriceForByteCount(0); + await calculator.getBaseWinstonPriceForByteCount(new ByteCount(0)); expect(spyedOracle.getWinstonPriceForByteCount.calledThrice).to.be.true; }); it('throws an error when constructed with a byte volume array that has only one number', () => { - expect(() => new ARDataPriceRegressionEstimator(true, spyedOracle, [1])).to.throw(Error); - }); - - it('throws an error when constructed with a byte volume array that has negative integers', () => { - expect(() => new ARDataPriceRegressionEstimator(true, spyedOracle, [-1, -2])).to.throw(Error); - }); - - it('throws an error when constructed with a byte volume array that has non-integer decimal values', () => { - expect(() => new ARDataPriceRegressionEstimator(true, spyedOracle, [0.1, 5.5])).to.throw(Error); + expect(() => new ARDataPriceRegressionEstimator(true, spyedOracle, [new ByteCount(1)])).to.throw(Error); }); it('uses byte volumes from provided byte volume array', () => { - const byteVolumes = [1, 5, 10]; + const byteVolumes = [1, 5, 10].map((vol) => new ByteCount(vol)); new ARDataPriceRegressionEstimator(false, spyedOracle, byteVolumes); expect(spyedOracle.getWinstonPriceForByteCount.firstCall.args[0]).to.equal(byteVolumes[0]); @@ -61,65 +52,60 @@ describe('ARDataPriceEstimator class', () => { }); it('getWinstonPriceForByteCount function returns the expected value', async () => { - const actualWinstonPriceEstimation = await calculator.getBaseWinstonPriceForByteCount(100); - expect(actualWinstonPriceEstimation).to.equal(100); + const actualWinstonPriceEstimation = await calculator.getBaseWinstonPriceForByteCount(new ByteCount(100)); + expect(`${actualWinstonPriceEstimation}`).to.equal('100'); }); describe('getByteCountForWinston function', () => { it('returns the expected value', async () => { - const actualByteCountEstimation = await calculator.getByteCountForWinston(100); - expect(actualByteCountEstimation).to.equal(100); - }); - - it('throws an error when provided winston value is a negative integer', async () => { - await expectAsyncErrorThrow({ promiseToError: calculator.getByteCountForWinston(-1) }); - }); - - it('throws an error when provided winston value is represented as a decimal', async () => { - await expectAsyncErrorThrow({ promiseToError: calculator.getByteCountForWinston(0.1) }); + const actualByteCountEstimation = await calculator.getByteCountForWinston(W(100)); + expect(actualByteCountEstimation.equals(new ByteCount(100))).to.be.true; }); it('makes three oracle calls after the first price estimation request', async () => { - await calculator.getByteCountForWinston(0); + await calculator.getByteCountForWinston(W(0)); expect(spyedOracle.getWinstonPriceForByteCount.calledThrice).to.be.true; }); it('returns 0 if provided winston value does not cover baseWinstonPrice', async () => { - const stubRegressionByteVolumes = [0, 1]; + const stubRegressionByteVolumes = [0, 1].map((vol) => new ByteCount(vol)); const priceEstimator = new ARDataPriceRegressionEstimator(true, spyedOracle, stubRegressionByteVolumes); // Stub out the returned prices for each byte value to arrive at base price 5 and marginal price 1 - spyedOracle.getWinstonPriceForByteCount.onFirstCall().callsFake(() => Promise.resolve(5)); - spyedOracle.getWinstonPriceForByteCount.onSecondCall().callsFake(() => Promise.resolve(6)); + spyedOracle.getWinstonPriceForByteCount.onFirstCall().callsFake(() => Promise.resolve(W(5))); + spyedOracle.getWinstonPriceForByteCount.onSecondCall().callsFake(() => Promise.resolve(W(6))); // Expect 4 to be reduced to 0 because it does not cover baseWinstonPrice of 5 - expect(await priceEstimator.getByteCountForWinston(4)).to.equal(0); + expect(await (await priceEstimator.getByteCountForWinston(W(4))).equals(new ByteCount(0))).to.be.true; }); }); describe('getByteCountForAR function', () => { it('returns the expected value', async () => { const actualByteCountEstimation = await calculator.getByteCountForAR( - 0.000_000_000_100, + AR.from(0.000_000_000_100), arDriveCommunityTip ); - expect(actualByteCountEstimation).to.equal(87); + expect(actualByteCountEstimation.equals(new ByteCount(87))).to.be.true; }); it('returns 0 if estimation does not cover the minimum winston fee', async () => { const actualByteCountEstimation = await calculator.getByteCountForAR( - 0.000_000_000_010, + AR.from(0.000_000_000_010), arDriveCommunityTip ); - expect(actualByteCountEstimation).to.equal(0); + expect(actualByteCountEstimation.equals(new ByteCount(0))).to.be.true; }); }); it('getARPriceForByteCount function returns the expected value', async () => { - const actualARPriceEstimation = await calculator.getARPriceForByteCount(100, arDriveCommunityTip); + const actualARPriceEstimation = await calculator.getARPriceForByteCount( + new ByteCount(100), + arDriveCommunityTip + ); - expect(actualARPriceEstimation).to.equal(0.000_000_000_115); + expect(`${actualARPriceEstimation}`).to.equal('0.000000000115'); }); describe('refreshPriceData function', () => { diff --git a/src/utils/ar_data_price_regression_estimator.ts b/src/utils/ar_data_price_regression_estimator.ts index 23bb7fb7..6b01c4fb 100644 --- a/src/utils/ar_data_price_regression_estimator.ts +++ b/src/utils/ar_data_price_regression_estimator.ts @@ -1,9 +1,8 @@ import { GatewayOracle } from './gateway_oracle'; import type { ArweaveOracle } from './arweave_oracle'; import { ARDataPriceRegression } from './data_price_regression'; -import { ARDataPrice } from './ar_data_price'; import { AbstractARDataPriceAndCapacityEstimator } from './ar_data_price_estimator'; -import type { ArDriveCommunityTip, ByteCount } from '../types'; +import { ArDriveCommunityTip, AR, ByteCount, Winston } from '../types'; /** * A utility class for Arweave data pricing estimation. @@ -14,7 +13,7 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci Math.pow(2, 10) * 100, // 100 KiB Math.pow(2, 20) * 100, // 100 MiB Math.pow(2, 30) * 10 // 10 GiB - ]; + ].map((volume) => new ByteCount(volume)); private predictor?: ARDataPriceRegression; private setupPromise?: Promise; @@ -40,12 +39,6 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci throw new Error('Byte volume array must contain at least 2 values to calculate regression'); } - for (const volume of byteVolumes) { - if (!Number.isInteger(volume) || volume < 0) { - throw new Error(`Byte volume (${volume}) on byte volume array should be a positive integer!`); - } - } - if (!skipSetup) { this.refreshPriceData(); } @@ -65,10 +58,10 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci // Fetch the price for all values in byteVolume array and feed them into a linear regression this.setupPromise = Promise.all( // TODO: What to do if one fails? - this.byteVolumes.map( - async (sampleByteCount) => - new ARDataPrice(sampleByteCount, await this.oracle.getWinstonPriceForByteCount(sampleByteCount)) - ) + this.byteVolumes.map(async (sampleByteCount) => { + const winstonPrice = await this.oracle.getWinstonPriceForByteCount(sampleByteCount); + return { numBytes: sampleByteCount, winstonPrice }; + }) ).then((pricingData) => new ARDataPriceRegression(pricingData)); this.predictor = await this.setupPromise; @@ -84,7 +77,7 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci * * @remarks Will fetch pricing data for regression modeling if a regression has not yet been run. */ - public async getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise { + public async getBaseWinstonPriceForByteCount(byteCount: ByteCount): Promise { // Lazily generate the price predictor if (!this.predictor) { await this.refreshPriceData(); @@ -105,11 +98,7 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci * @remarks Will fetch pricing data for regression modeling if a regression has not yet been run. * @remarks The ArDrive community fee is not considered in this estimation */ - public async getByteCountForWinston(winston: number): Promise { - if (winston < 0 || !Number.isInteger(winston)) { - throw new Error('winston value should be a non-negative integer!'); - } - + public async getByteCountForWinston(winston: Winston): Promise { // Lazily generate the price predictor if (!this.predictor) { await this.refreshPriceData(); @@ -119,7 +108,14 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci } // Return 0 if winston price given does not cover the base winston price for a transaction - return Math.max(0, (winston - this.predictor.baseWinstonPrice()) / this.predictor.marginalWinstonPrice()); + // TODO: Is number sufficient here vs. BigNumber? + const baseWinstonPrice = this.predictor.baseWinstonPrice(); + const marginalWinstonPrice = this.predictor.marginalWinstonPrice(); + if (winston.isGreaterThan(baseWinstonPrice)) { + return new ByteCount(+winston.minus(baseWinstonPrice).dividedBy(marginalWinstonPrice).toString()); + } + + return new ByteCount(0); } /** @@ -129,7 +125,7 @@ export class ARDataPriceRegressionEstimator extends AbstractARDataPriceAndCapaci * @remarks Returns 0 bytes when the price does not cover minimum ArDrive community fee */ public async getByteCountForAR( - arPrice: number, + arPrice: AR, { minWinstonFee, tipPercentage }: ArDriveCommunityTip ): Promise { // Lazily generate the price predictor diff --git a/src/utils/ar_unit.test.ts b/src/utils/ar_unit.test.ts deleted file mode 100644 index 9a7eb374..00000000 --- a/src/utils/ar_unit.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from 'chai'; -import { assertARPrecision } from './ar_unit'; - -describe('The assertARPrecision method', () => { - const VALID_INTEGER = '1'; - const VALID_FLOATING_POINT = '0.0'; - const VALID_FLOATING_POINT_NO_LEADING_DIGITS = '.0123'; - const VALID_FLOATING_POINT_WITH_TRAILING_ZEROS = '.00000000000100000000000000000'; - const EXCESSIVE_NONZERO_DIGITS = '.000000000001010'; - const A_HALF_A_WINSTON = '.0000000000005'; - const NOT_A_NUMBER = 'not a number >:b .00000'; - const NEGATIVE_VALUE = '-10'; - - it('Passes when asserting an integer', () => { - expect(assertARPrecision.bind(this, VALID_INTEGER)).to.not.throw(); - }); - - it('Passes when asserting a floating point', () => { - expect(assertARPrecision.bind(this, VALID_FLOATING_POINT)).to.not.throw(); - }); - - it('Passes when asserting a floating point with no leading digits', () => { - expect(assertARPrecision.bind(this, VALID_FLOATING_POINT_NO_LEADING_DIGITS)).to.not.throw(); - }); - - it('Passes when asserting a valid floating point with trailing zeros at the end', () => { - expect(assertARPrecision.bind(this, VALID_FLOATING_POINT_WITH_TRAILING_ZEROS)).to.not.throw(); - }); - - it('Throws when asserting a fraction of a Winston', () => { - expect(assertARPrecision.bind(this, A_HALF_A_WINSTON)).to.throw(); - expect(assertARPrecision.bind(this, EXCESSIVE_NONZERO_DIGITS)).to.throw(); - }); - - it('Throws when asserting an NaN', () => { - expect(assertARPrecision.bind(this, NOT_A_NUMBER)).to.throw(); - }); - - it('Throws when asserting a negative value', () => { - expect(assertARPrecision.bind(this, NEGATIVE_VALUE)).to.throw(); - }); -}); diff --git a/src/utils/ar_unit.ts b/src/utils/ar_unit.ts deleted file mode 100644 index 99eb61ae..00000000 --- a/src/utils/ar_unit.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Capture group 1: decimal precision beyond 12 digits - */ -export const FLOATING_POINT_REGEXP = /^\d*\.\d{12}(\d+)$/; - -export function assertARPrecision(arAmount: string): void { - if (Number.isNaN(+arAmount)) { - throw new Error(`The AR amount must be a number. Got: ${arAmount}`); - } - - if (+arAmount < 0.0) { - throw new Error(`The AR amount must be a positive number. Got: ${arAmount}`); - } - - const excessiveDigits = arAmount.match(FLOATING_POINT_REGEXP)?.[1] || ''; - if (+excessiveDigits !== 0.0) { - throw new Error( - `The AR amount must have a maximum of 12 digits of precision, but got ${12 + excessiveDigits.length}` - ); - } -} diff --git a/src/utils/ardrive.test.ts b/src/utils/ardrive.test.ts index 63dea349..4e25e403 100644 --- a/src/utils/ardrive.test.ts +++ b/src/utils/ardrive.test.ts @@ -7,7 +7,7 @@ import { ArFSPublicFileMetadataTransactionData, ArFSPublicFolderTransactionData } from '../../src/arfs_trx_data_types'; -import { TipType } from '../../src/types'; +import { stubTransactionID, TipType } from '../../src/types'; import { readJWKFile } from '../../src/utils'; import { ArweaveOracle } from '../../src/utils/arweave_oracle'; import { ARDataPriceRegressionEstimator } from '../../src/utils/ar_data_price_regression_estimator'; @@ -17,7 +17,8 @@ import { expectAsyncErrorThrow } from '../../src/utils/test_helpers'; import { ArDriveCommunityOracle } from '../../src/community/ardrive_community_oracle'; import { CommunityOracle } from '../../src/community/community_oracle'; import { ArFSDAO } from '../arfsdao'; -import { stubEntityID, stubTransactionID } from './stubs'; +import { stubEntityID } from './stubs'; +import { W, FeeMultiple, ByteCount, UnixTime } from '../types'; describe('ArDrive class', () => { let arDrive: ArDrive; @@ -34,8 +35,8 @@ describe('ArDrive class', () => { const wallet = readJWKFile('./test_wallet.json'); const stubPublicFileTransactionData = new ArFSPublicFileMetadataTransactionData( 'stubName', - 12345, - 0, + new ByteCount(12345), + new UnixTime(0), stubTransactionID, 'application/json' ); @@ -45,7 +46,7 @@ describe('ArDrive class', () => { beforeEach(async () => { // Set pricing algo up as x = y (bytes = Winston) arweaveOracleStub = stub(new GatewayOracle()); - arweaveOracleStub.getWinstonPriceForByteCount.callsFake((input) => Promise.resolve(input)); + arweaveOracleStub.getWinstonPriceForByteCount.callsFake((input) => Promise.resolve(W(+input))); communityOracleStub = stub(new ArDriveCommunityOracle(fakeArweave)); priceEstimator = new ARDataPriceRegressionEstimator(true, arweaveOracleStub); walletDao = new WalletDAO(fakeArweave, 'Unit Test', '1.0'); @@ -57,22 +58,14 @@ describe('ArDrive class', () => { 'Unit Test', '1.0', priceEstimator, - 1.0, + new FeeMultiple(1.0), true ); }); describe('encryptedDataSize function', () => { - it('throws an error when passed a negative value', () => { - expect(() => arDrive.encryptedDataSize(-1)).to.throw(Error); - }); - - it('throws an error when passed a non-integer value', () => { - expect(() => arDrive.encryptedDataSize(0.5)).to.throw(Error); - }); - it('throws an error when passed a value too large for computation', () => { - expect(() => arDrive.encryptedDataSize(Number.MAX_SAFE_INTEGER - 15)).to.throw(Error); + expect(() => arDrive.encryptedDataSize(new ByteCount(Number.MAX_SAFE_INTEGER - 15))).to.throw(Error); }); it('returns the expected values for valid inputs', () => { @@ -83,9 +76,10 @@ describe('ArDrive class', () => { [16, 32], [17, 33], [Number.MAX_SAFE_INTEGER - 16, Number.MAX_SAFE_INTEGER] - ]; + ].map((pair) => pair.map((vol) => new ByteCount(vol))); inputsAndExpectedOutputs.forEach(([input, expectedOutput]) => { - expect(arDrive.encryptedDataSize(input)).to.equal(expectedOutput); + const actualSize = arDrive.encryptedDataSize(input); + expect(actualSize.equals(expectedOutput), `${actualSize} === ${expectedOutput}`).to.be.true; }); }); }); @@ -104,34 +98,28 @@ describe('ArDrive class', () => { ] ]; inputsAndExpectedOutputs.forEach(([input, expectedOutput]) => { - console.log(JSON.stringify(expectedOutput, null, 4)); expect(arDrive.getTipTags(input as TipType)).to.deep.equal(expectedOutput); }); }); }); describe('estimateAndAssertCostOfFileUpload function', () => { - it('throws an error when decryptedFileSize is negative', async () => { - await expectAsyncErrorThrow({ - promiseToError: arDrive.estimateAndAssertCostOfFileUpload(-1, stubPublicFileTransactionData, 'private') - }); - }); - - it('throws an error when decryptedFileSize is not an integer', async () => { - await expectAsyncErrorThrow({ - promiseToError: arDrive.estimateAndAssertCostOfFileUpload(0.1, stubPublicFileTransactionData, 'private') - }); - }); - it('throws an error when there is an insufficient wallet balance', async () => { stub(walletDao, 'walletHasBalance').callsFake(() => { return Promise.resolve(false); }); stub(walletDao, 'getWalletWinstonBalance').callsFake(() => { - return Promise.resolve(0); + return Promise.resolve(W(0)); + }); + communityOracleStub.getCommunityWinstonTip.callsFake(() => { + return Promise.resolve(W(9876543210)); }); await expectAsyncErrorThrow({ - promiseToError: arDrive.estimateAndAssertCostOfFileUpload(1, stubPublicFileTransactionData, 'private') + promiseToError: arDrive.estimateAndAssertCostOfFileUpload( + new ByteCount(1), + stubPublicFileTransactionData, + 'private' + ) }); }); @@ -140,19 +128,17 @@ describe('ArDrive class', () => { return Promise.resolve(true); }); communityOracleStub.getCommunityWinstonTip.callsFake(() => { - return Promise.resolve('9876543210'); + return Promise.resolve(W(9876543210)); }); const actual = await arDrive.estimateAndAssertCostOfFileUpload( - 1234567, + new ByteCount(1234567), stubPublicFileTransactionData, 'private' ); - expect(actual).to.deep.equal({ - metaDataBaseReward: '147', - fileDataBaseReward: '1234583', - communityWinstonTip: '9876543210' - }); + expect(`${actual.metaDataBaseReward}`).to.equal('147'); + expect(`${actual.fileDataBaseReward}`).to.equal('1234583'); + expect(`${actual.communityWinstonTip}`).to.equal('9876543210'); }); }); @@ -162,7 +148,7 @@ describe('ArDrive class', () => { return Promise.resolve(false); }); stub(walletDao, 'getWalletWinstonBalance').callsFake(() => { - return Promise.resolve(0); + return Promise.resolve(W(0)); }); await expectAsyncErrorThrow({ promiseToError: arDrive.estimateAndAssertCostOfFolderUpload(stubPublicFolderTransactionData) @@ -175,9 +161,8 @@ describe('ArDrive class', () => { }); const actual = await arDrive.estimateAndAssertCostOfFolderUpload(stubPublicFileTransactionData); - expect(actual).to.deep.equal({ - metaDataBaseReward: '147' - }); + // TODO: Bummer to lose deep equal verification + expect(`${actual.metaDataBaseReward}`).to.equal('147'); }); }); @@ -187,7 +172,7 @@ describe('ArDrive class', () => { return Promise.resolve(false); }); stub(walletDao, 'getWalletWinstonBalance').callsFake(() => { - return Promise.resolve(0); + return Promise.resolve(W(0)); }); await expectAsyncErrorThrow({ promiseToError: arDrive.estimateAndAssertCostOfDriveCreation( @@ -206,10 +191,8 @@ describe('ArDrive class', () => { stubPublicDriveMetadataTransactionData, stubPublicFolderTransactionData ); - expect(actual).to.deep.equal({ - driveMetaDataBaseReward: '73', - rootFolderMetaDataBaseReward: '19' - }); + expect(`${actual.driveMetaDataBaseReward}`).to.equal('73'); + expect(`${actual.rootFolderMetaDataBaseReward}`).to.equal('19'); }); }); @@ -219,7 +202,7 @@ describe('ArDrive class', () => { return Promise.resolve(false); }); stub(walletDao, 'getWalletWinstonBalance').callsFake(() => { - return Promise.resolve(0); + return Promise.resolve(W(0)); }); await expectAsyncErrorThrow({ promiseToError: arDrive.estimateAndAssertCostOfMoveFile(stubPublicFileTransactionData) @@ -232,9 +215,7 @@ describe('ArDrive class', () => { }); const actual = await arDrive.estimateAndAssertCostOfMoveFile(stubPublicFileTransactionData); - expect(actual).to.deep.equal({ - metaDataBaseReward: '147' - }); + expect(`${actual.metaDataBaseReward}`).to.equal('147'); }); }); }); diff --git a/src/utils/arfs_builders/arfs_builders.ts b/src/utils/arfs_builders/arfs_builders.ts index 894c5580..bac81a22 100644 --- a/src/utils/arfs_builders/arfs_builders.ts +++ b/src/utils/arfs_builders/arfs_builders.ts @@ -1,5 +1,4 @@ import { - ArFSEntity, ContentType, EntityType, GQLNodeInterface, @@ -8,13 +7,21 @@ import { } from 'ardrive-core-js'; import Arweave from 'arweave'; import { graphQLURL } from '../../arfsdao'; -import { ArFSFileOrFolderEntity } from '../../arfs_entities'; -import { ArweaveAddress } from '../../arweave_address'; +import { ArFSEntity, ArFSFileOrFolderEntity } from '../../arfs_entities'; import { buildQuery } from '../../query'; -import { DriveID, EntityID, EntityKey, FolderID, TransactionID, UnixTime } from '../../types'; - +import { + ArweaveAddress, + DriveID, + AnyEntityID, + EntityKey, + FolderID, + TransactionID, + TxID, + UnixTime, + EID +} from '../../types'; export interface ArFSMetadataEntityBuilderParams { - entityId: EntityID; + entityId: AnyEntityID; arweave: Arweave; owner?: ArweaveAddress; } @@ -39,7 +46,7 @@ export abstract class ArFSMetadataEntityBuilder { name?: string; txId?: TransactionID; unixTime?: UnixTime; - protected readonly entityId: EntityID; + protected readonly entityId: AnyEntityID; protected readonly arweave: Arweave; protected readonly owner?: ArweaveAddress; @@ -78,7 +85,7 @@ export abstract class ArFSMetadataEntityBuilder { node = edges[0].node; } - this.txId = node.id; + this.txId = TxID(node.id); const { tags } = node; tags.forEach((tag: GQLTagInterface) => { const key = tag.name; @@ -97,13 +104,13 @@ export abstract class ArFSMetadataEntityBuilder { this.contentType = value as ContentType; break; case 'Drive-Id': - this.driveId = value; + this.driveId = EID(value); break; case 'Entity-Type': this.entityType = value as EntityType; break; case 'Unix-Time': - this.unixTime = +value; + this.unixTime = new UnixTime(+value); break; default: unparsedTags.push(tag); @@ -131,7 +138,7 @@ export abstract class ArFSFileOrFolderBuilder const { value } = tag; switch (key) { case 'Parent-Folder-Id': - this.parentFolderId = value; + this.parentFolderId = EID(value); break; default: unparsedTags.push(tag); diff --git a/src/utils/arfs_builders/arfs_drive_builders.ts b/src/utils/arfs_builders/arfs_drive_builders.ts index dbd447df..698e7ee2 100644 --- a/src/utils/arfs_builders/arfs_drive_builders.ts +++ b/src/utils/arfs_builders/arfs_drive_builders.ts @@ -1,5 +1,4 @@ import { - ArFSDriveEntity, DriveAuthMode, driveDecrypt, DrivePrivacy, @@ -8,9 +7,10 @@ import { Utf8ArrayToStr } from 'ardrive-core-js'; import Arweave from 'arweave'; -import { ArFSPrivateDrive, ArFSPublicDrive } from '../../arfs_entities'; +import { ArFSDriveEntity, ArFSPrivateDrive, ArFSPublicDrive, ENCRYPTED_DATA_PLACEHOLDER } from '../../arfs_entities'; import { EntityMetaDataTransactionData, PrivateKeyData } from '../../private_key_data'; -import { CipherIV, DriveKey, FolderID } from '../../types'; +import { CipherIV, DriveKey, FolderID, EID, EntityID } from '../../types'; +import { stubEntityID } from '../stubs'; import { ArFSMetadataEntityBuilder, ArFSMetadataEntityBuilderParams, @@ -22,8 +22,6 @@ interface DriveMetaDataTransactionData extends EntityMetaDataTransactionData { rootFolderId: FolderID; } -export const ENCRYPTED_DATA_PLACEHOLDER = 'ENCRYPTED'; - export class ArFSPublicDriveBuilder extends ArFSMetadataEntityBuilder { drivePrivacy?: DrivePrivacy; rootFolderId?: FolderID; @@ -34,13 +32,13 @@ export class ArFSPublicDriveBuilder extends ArFSMetadataEntityBuilder( this.cipherIV, diff --git a/src/utils/arfs_builders/arfs_file_builders.ts b/src/utils/arfs_builders/arfs_file_builders.ts index 7984ad31..38c32aa7 100644 --- a/src/utils/arfs_builders/arfs_file_builders.ts +++ b/src/utils/arfs_builders/arfs_file_builders.ts @@ -9,8 +9,17 @@ import { } from 'ardrive-core-js'; import Arweave from 'arweave'; import { ArFSPrivateFile, ArFSPublicFile } from '../../arfs_entities'; -import { ArweaveAddress } from '../../arweave_address'; -import { ByteCount, CipherIV, DriveKey, FileID, FileKey, TransactionID, UnixTime } from '../../types'; +import { + ArweaveAddress, + CipherIV, + DriveKey, + FileID, + FileKey, + ByteCount, + TransactionID, + UnixTime, + EID +} from '../../types'; import { ArFSFileOrFolderBuilder } from './arfs_builders'; interface FileMetaDataTransactionData { @@ -28,7 +37,7 @@ export abstract class ArFSFileBuilder { if (!fileId) { throw new Error('File-ID tag missing!'); } - const fileBuilder = new ArFSPublicFileBuilder({ entityId: fileId, arweave }); + const fileBuilder = new ArFSPublicFileBuilder({ entityId: EID(fileId), arweave }); return fileBuilder; } @@ -51,14 +60,14 @@ export class ArFSPublicFileBuilder extends ArFSFileBuilder { this.appVersion?.length && this.arFS?.length && this.contentType?.length && - this.driveId?.length && + this.driveId && this.entityType?.length && - this.txId?.length && + this.txId && this.unixTime && - this.parentFolderId?.length && - this.entityId?.length + this.parentFolderId && + this.entityId ) { - const txData = await this.arweave.transactions.getData(this.txId, { decode: true }); + const txData = await this.arweave.transactions.getData(`${this.txId}`, { decode: true }); const dataString = await Utf8ArrayToStr(txData); const dataJSON: FileMetaDataTransactionData = await JSON.parse(dataString); @@ -123,7 +132,7 @@ export class ArFSPrivateFileBuilder extends ArFSFileBuilder { if (!fileId) { throw new Error('File-ID tag missing!'); } - const fileBuilder = new ArFSPrivateFileBuilder(fileId, arweave, driveKey); + const fileBuilder = new ArFSPrivateFileBuilder(EID(fileId), arweave, driveKey); return fileBuilder; } @@ -154,18 +163,18 @@ export class ArFSPrivateFileBuilder extends ArFSFileBuilder { this.appVersion?.length && this.arFS?.length && this.contentType?.length && - this.driveId?.length && + this.driveId && this.entityType?.length && - this.txId?.length && + this.txId && this.unixTime && - this.parentFolderId?.length && - this.entityId?.length && + this.parentFolderId && + this.entityId && this.cipher?.length && this.cipherIV?.length ) { - const txData = await this.arweave.transactions.getData(this.txId, { decode: true }); + const txData = await this.arweave.transactions.getData(`${this.txId}`, { decode: true }); const dataBuffer = Buffer.from(txData); - const fileKey = this.fileKey ?? (await deriveFileKey(this.fileId, this.driveKey)); + const fileKey = this.fileKey ?? (await deriveFileKey(`${this.fileId}`, this.driveKey)); const decryptedFileBuffer: Buffer = await fileDecrypt(this.cipherIV, fileKey, dataBuffer); const decryptedFileString: string = await Utf8ArrayToStr(decryptedFileBuffer); diff --git a/src/utils/arfs_builders/arfs_folder_builders.ts b/src/utils/arfs_builders/arfs_folder_builders.ts index f9c36556..cd7cff90 100644 --- a/src/utils/arfs_builders/arfs_folder_builders.ts +++ b/src/utils/arfs_builders/arfs_folder_builders.ts @@ -1,16 +1,26 @@ import { fileDecrypt, GQLNodeInterface, GQLTagInterface, Utf8ArrayToStr } from 'ardrive-core-js'; import Arweave from 'arweave'; import { ArFSPrivateFolder, ArFSPublicFolder } from '../../arfs_entities'; -import { ArweaveAddress } from '../../arweave_address'; -import { CipherIV, DriveKey, FolderID } from '../../types'; import { ArFSFileOrFolderBuilder } from './arfs_builders'; +import { ArweaveAddress, CipherIV, DriveKey, FolderID, EID, EntityID } from '../../types'; +import { stubEntityID } from '../stubs'; + +export const ROOT_FOLDER_ID_PLACEHOLDER = 'root folder'; + +// A utility type to provide a FolderID placeholder for root folders (which never have a parentFolderId) +export class RootFolderID extends EntityID { + constructor() { + super(`${stubEntityID}`); // Unused after next line + this.entityId = ROOT_FOLDER_ID_PLACEHOLDER; + } +} export abstract class ArFSFolderBuilder< T extends ArFSPublicFolder | ArFSPrivateFolder > extends ArFSFileOrFolderBuilder { getGqlQueryParameters(): GQLTagInterface[] { return [ - { name: 'Folder-Id', value: this.entityId }, + { name: 'Folder-Id', value: `${this.entityId}` }, { name: 'Entity-Type', value: 'folder' } ]; } @@ -23,14 +33,14 @@ export class ArFSPublicFolderBuilder extends ArFSFolderBuilder if (!folderId) { throw new Error('Folder-ID tag missing!'); } - const folderBuilder = new ArFSPublicFolderBuilder({ entityId: folderId, arweave }); + const folderBuilder = new ArFSPublicFolderBuilder({ entityId: EID(folderId), arweave }); return folderBuilder; } protected async buildEntity(): Promise { if (!this.parentFolderId) { // Root folders do not have a Parent-Folder-Id tag - this.parentFolderId = 'root folder'; + this.parentFolderId = new RootFolderID(); } if ( @@ -38,21 +48,21 @@ export class ArFSPublicFolderBuilder extends ArFSFolderBuilder this.appVersion?.length && this.arFS?.length && this.contentType?.length && - this.driveId?.length && + this.driveId && this.entityType?.length && - this.txId?.length && + this.txId && this.unixTime && - this.parentFolderId?.length && - this.entityId?.length + this.parentFolderId && + this.entityId ) { - const txData = await this.arweave.transactions.getData(this.txId, { decode: true }); + const txData = await this.arweave.transactions.getData(`${this.txId}`, { decode: true }); const dataString = await Utf8ArrayToStr(txData); const dataJSON = await JSON.parse(dataString); // Get the folder name this.name = dataJSON.name; if (!this.name) { - throw new Error('Invalid folder state'); + throw new Error('Invalid public folder state: name not found!'); } return Promise.resolve( @@ -71,7 +81,7 @@ export class ArFSPublicFolderBuilder extends ArFSFolderBuilder ) ); } - throw new Error('Invalid folder state'); + throw new Error('Invalid public folder state'); } } @@ -94,7 +104,7 @@ export class ArFSPrivateFolderBuilder extends ArFSFolderBuilder { if (!this.parentFolderId) { // Root folders do not have a Parent-Folder-Id tag - this.parentFolderId = 'root folder'; + this.parentFolderId = new RootFolderID(); } if ( @@ -130,16 +140,16 @@ export class ArFSPrivateFolderBuilder extends ArFSFolderBuilder; + getWinstonPriceForByteCount(byteCount: ByteCount): Promise; } diff --git a/src/utils/data_price_regression.test.ts b/src/utils/data_price_regression.test.ts index d4373069..9219f1ac 100644 --- a/src/utils/data_price_regression.test.ts +++ b/src/utils/data_price_regression.test.ts @@ -1,47 +1,60 @@ import { expect } from 'chai'; +import { ByteCount, W } from '../types'; import { ARDataPrice } from './ar_data_price'; import { ARDataPriceRegression } from './data_price_regression'; describe('ARDataPriceRegression class', () => { + const oneWinston = W(1); it('static constructor throws an error if no input data was supplied', () => { expect(() => new ARDataPriceRegression([])).to.throw(Error); }); it('static constructor can create a regression from a single datapoint', () => { - const inputDataPrice = new ARDataPrice(1, 1); - const predictedPrice = new ARDataPriceRegression([inputDataPrice]).predictedPriceForByteCount(1); + const inputDataPrice: ARDataPrice = { numBytes: new ByteCount(1), winstonPrice: oneWinston }; + const predictedPrice = new ARDataPriceRegression([inputDataPrice]).predictedPriceForByteCount(new ByteCount(1)); expect(predictedPrice).to.deep.equal(inputDataPrice); }); - it('predictedPriceForByteCount throws an error for negative and non-integer byte counts', () => { - const inputDataPrice = new ARDataPrice(1, 1); - const predictor = new ARDataPriceRegression([inputDataPrice]); - expect(() => predictor.predictedPriceForByteCount(-1)).to.throw(Error); - expect(() => predictor.predictedPriceForByteCount(0.5)).to.throw(Error); - }); - it('predictedPriceForByteCount returns an accurate linear prediction', () => { const predictor = new ARDataPriceRegression([ - new ARDataPrice(1, 1), - new ARDataPrice(100, 100), - new ARDataPrice(10000, 10000) + { numBytes: new ByteCount(1), winstonPrice: oneWinston }, + { numBytes: new ByteCount(100), winstonPrice: W(100) }, + { numBytes: new ByteCount(10000), winstonPrice: W(10000) } ]); - expect(predictor.predictedPriceForByteCount(0)).to.deep.equal(new ARDataPrice(0, 0)); - expect(predictor.predictedPriceForByteCount(1000000)).to.deep.equal(new ARDataPrice(1000000, 1000000)); + expect(predictor.predictedPriceForByteCount(new ByteCount(0))).to.deep.equal({ + numBytes: new ByteCount(0), + winstonPrice: W(0) + }); + expect(predictor.predictedPriceForByteCount(new ByteCount(1000000))).to.deep.equal({ + numBytes: new ByteCount(1000000), + winstonPrice: W(1000000) + }); }); it('predictedPriceForByteCount returns a rounded up estimate when the Winston price would otherwise be predicted as non-integer', () => { - const predictor = new ARDataPriceRegression([new ARDataPrice(0, 0), new ARDataPrice(2, 3)]); - expect(predictor.predictedPriceForByteCount(1)).to.deep.equal(new ARDataPrice(1, 2)); + const predictor = new ARDataPriceRegression([ + { numBytes: new ByteCount(0), winstonPrice: W(0) }, + { numBytes: new ByteCount(2), winstonPrice: W(3) } + ]); + expect(predictor.predictedPriceForByteCount(new ByteCount(1))).to.deep.equal({ + numBytes: new ByteCount(1), + winstonPrice: W(2) + }); }); it('baseWinstonPrice returns the correct base value', () => { - const predictor = new ARDataPriceRegression([new ARDataPrice(0, 100), new ARDataPrice(5, 600)]); - expect(predictor.baseWinstonPrice()).to.equal(100); + const predictor = new ARDataPriceRegression([ + { numBytes: new ByteCount(0), winstonPrice: W(100) }, + { numBytes: new ByteCount(5), winstonPrice: W(600) } + ]); + expect(`${predictor.baseWinstonPrice()}`).to.equal('100'); }); it('marginalWinstonPrice returns the correct marginal value', () => { - const predictor = new ARDataPriceRegression([new ARDataPrice(0, 1), new ARDataPrice(5, 11)]); + const predictor = new ARDataPriceRegression([ + { numBytes: new ByteCount(0), winstonPrice: W(1) }, + { numBytes: new ByteCount(5), winstonPrice: W(11) } + ]); expect(predictor.marginalWinstonPrice()).to.equal(2); }); }); diff --git a/src/utils/data_price_regression.ts b/src/utils/data_price_regression.ts index c549e90f..39cb2f5d 100644 --- a/src/utils/data_price_regression.ts +++ b/src/utils/data_price_regression.ts @@ -1,5 +1,5 @@ import regression, { DataPoint } from 'regression'; -import { ByteCount } from '../types'; +import { W, Winston, ByteCount } from '../types'; import { ARDataPrice } from './ar_data_price'; /** @@ -21,7 +21,8 @@ export class ARDataPriceRegression { } const dataPoints: DataPoint[] = pricingData.map( - (pricingDatapoint) => [pricingDatapoint.numBytes, pricingDatapoint.winstonPrice] as DataPoint + // TODO: BigNumber regressions + (pricingDatapoint) => [+pricingDatapoint.numBytes, +pricingDatapoint.winstonPrice.toString()] as DataPoint ); this.regression = regression.linear(dataPoints); @@ -34,20 +35,17 @@ export class ARDataPriceRegression { * @throws {@link Error} if `numBytes` is negative or not an integer */ predictedPriceForByteCount(numBytes: ByteCount): ARDataPrice { - if (numBytes < 0 || !Number.isInteger(numBytes)) { - throw new Error(`numBytes (${numBytes}) should be a positive integer`); - } - - const regressionResult = this.regression.predict(numBytes); - return new ARDataPrice(regressionResult[0], Math.ceil(regressionResult[1])); + const regressionResult = this.regression.predict(+numBytes); + // TODO: BigNumber regressions + return { numBytes: new ByteCount(regressionResult[0]), winstonPrice: W(Math.ceil(regressionResult[1])) }; } /** * Returns the current base AR price in Winston for submitting an Arweave transaction, * which has been calculated by the regression model */ - baseWinstonPrice(): number { - return this.regression.equation[1]; + baseWinstonPrice(): Winston { + return W(this.regression.equation[1]); } /** diff --git a/src/utils/filter_methods.test.ts b/src/utils/filter_methods.test.ts new file mode 100644 index 00000000..0b13497e --- /dev/null +++ b/src/utils/filter_methods.test.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai'; +import { ArFSPublicDrive } from '../arfs_entities'; +import { Mutable, TxID } from '../types'; +import { fileFilter, folderFilter, latestRevisionFilter, latestRevisionFilterForDrives } from './filter_methods'; +import { stubPrivateFile, stubPrivateFolder, stubPublicDrive, stubPublicFile, stubPublicFolder } from './stubs'; + +describe('The latestRevisionFilter function', () => { + it('returns true when only entry in array matches the search entry', () => { + const stubFile = stubPublicFile({}); + expect(latestRevisionFilter(stubFile, 0, [stubFile])).to.be.true; + }); + + it('returns true when search entry is the first in the entity array', () => { + const stubFile = stubPublicFile({}); + const stubFile2 = stubPublicFile({ txId: TxID('0000000000000000000000000000000000000000001') }); + expect(latestRevisionFilter(stubFile, 0, [stubFile, stubFile2])).to.be.true; + }); + + it('returns false when search entry is not first in the entity array', () => { + const stubFile = stubPublicFile({}); + const stubFile2 = stubPublicFile({ txId: TxID('0000000000000000000000000000000000000000001') }); + expect(latestRevisionFilter(stubFile, 0, [stubFile2, stubFile])).to.be.false; + }); +}); + +describe('The latestRevisionFilterForDrives function', () => { + it('returns true when only entry in array matches the search entry', () => { + const stubDrive = stubPublicDrive(); + expect(latestRevisionFilterForDrives(stubDrive, 0, [stubDrive])).to.be.true; + }); + + it('returns true when search entry is the first in the entity array', () => { + const stubDrive = stubPublicDrive(); + const stubDrive2 = stubPublicDrive(); + (stubDrive2 as Mutable).txId = TxID('0000000000000000000000000000000000000000001'); + expect(latestRevisionFilterForDrives(stubDrive, 0, [stubDrive, stubDrive2])).to.be.true; + }); + + it('returns false when search entry is not first in the entity array', () => { + const stubDrive = stubPublicDrive(); + const stubDrive2 = stubPublicDrive(); + (stubDrive2 as Mutable).txId = TxID('0000000000000000000000000000000000000000001'); + expect(latestRevisionFilterForDrives(stubDrive, 0, [stubDrive2, stubDrive])).to.be.false; + }); +}); + +describe('The fileFilter function', () => { + it('returns true for an ArFSPublicFile', () => { + expect(fileFilter(stubPublicFile({}))).to.be.true; + }); + + it('returns true for an ArFSPrivateFile', () => { + expect(fileFilter(stubPrivateFile({}))).to.be.true; + }); + + it('returns false for an ArFSPublicFolder', () => { + expect(fileFilter(stubPublicFolder({}))).to.be.false; + }); + + it('returns false for an ArFSPrivateFolder', () => { + expect(fileFilter(stubPrivateFolder({}))).to.be.false; + }); +}); + +describe('The folderFilter function', () => { + it('returns false for an ArFSPublicFile', () => { + expect(folderFilter(stubPublicFile({}))).to.be.false; + }); + + it('returns false for an ArFSPrivateFile', () => { + expect(folderFilter(stubPrivateFile({}))).to.be.false; + }); + + it('returns false for an ArFSPublicFolder', () => { + expect(folderFilter(stubPublicFolder({}))).to.be.true; + }); + + it('returns false for an ArFSPrivateFolder', () => { + expect(folderFilter(stubPrivateFolder({}))).to.be.true; + }); +}); diff --git a/src/utils/filter_methods.ts b/src/utils/filter_methods.ts index b455bad8..71bc218d 100644 --- a/src/utils/filter_methods.ts +++ b/src/utils/filter_methods.ts @@ -1,5 +1,4 @@ -import { ArFSDriveEntity } from 'ardrive-core-js'; -import { ArFSFileOrFolderEntity } from '../arfs_entities'; +import { ArFSDriveEntity, ArFSFileOrFolderEntity } from '../arfs_entities'; /** * @name lastRevisionFilter is a standard JS find/filter function intended to @@ -15,9 +14,9 @@ export function latestRevisionFilter( _index: number, allEntities: ArFSFileOrFolderEntity[] ): boolean { - const allRevisions = allEntities.filter((e) => e.entityId === entity.entityId); + const allRevisions = allEntities.filter((e) => e.entityId.equals(entity.entityId)); const latestRevision = allRevisions[0]; - return entity.txId === latestRevision.txId; + return entity.txId.equals(latestRevision.txId); } /** @@ -34,9 +33,9 @@ export function latestRevisionFilterForDrives( _index: number, allEntities: ArFSDriveEntity[] ): boolean { - const allRevisions = allEntities.filter((e) => e.driveId === entity.driveId); + const allRevisions = allEntities.filter((e) => e.driveId.equals(entity.driveId)); const latestRevision = allRevisions[0]; - return entity.txId === latestRevision.txId; + return entity.txId.equals(latestRevision.txId); } export function fileFilter(entity: ArFSFileOrFolderEntity): boolean { diff --git a/src/utils/gateway_oracle.ts b/src/utils/gateway_oracle.ts index 35893a8e..ce45ee9d 100644 --- a/src/utils/gateway_oracle.ts +++ b/src/utils/gateway_oracle.ts @@ -1,11 +1,12 @@ import type { ArweaveOracle } from './arweave_oracle'; import fetch from 'node-fetch'; -import { ByteCount } from '../types'; +import { ByteCount, W, Winston } from '../types'; +import { BigNumber } from 'bignumber.js'; export class GatewayOracle implements ArweaveOracle { - async getWinstonPriceForByteCount(byteCount: ByteCount): Promise { + async getWinstonPriceForByteCount(byteCount: ByteCount): Promise { const response = await fetch(`https://arweave.net/price/${byteCount}`); const winstonAsString = await response.text(); - return +winstonAsString; + return W(new BigNumber(winstonAsString)); } } diff --git a/src/utils/stubs.ts b/src/utils/stubs.ts index d8a4c9b4..3d10c2ec 100644 --- a/src/utils/stubs.ts +++ b/src/utils/stubs.ts @@ -6,35 +6,47 @@ import { ArFSPublicFile, ArFSPrivateFile } from '../arfs_entities'; -import { ArweaveAddress } from '../arweave_address'; -import { ArFS_O_11, DriveID, FolderID, JSON_CONTENT_TYPE, PRIVATE_CONTENT_TYPE } from '../types'; - -export const stubArweaveAddress = (address = 'abcdefghijklmnopqrxtuvwxyz123456789ABCDEFGH'): ArweaveAddress => - new ArweaveAddress(address); +import { + ADDR, + ArFS_O_11, + ArweaveAddress, + ByteCount, + DriveID, + EID, + FolderID, + JSON_CONTENT_TYPE, + PRIVATE_CONTENT_TYPE, + stubTransactionID, + TransactionID, + UnixTime +} from '../types'; -export const stubTransactionID = '0000000000000000000000000000000000000000000'; +export const stubArweaveAddress = (address = 'abcdefghijklmnopqrxtuvwxyz123456789ABCDEFGH'): ArweaveAddress => { + return ADDR(address); +}; -export const stubEntityID = '00000000-0000-0000-0000-000000000000'; -export const stubEntityIDAlt = 'caa8b54a-eb5e-4134-8ae2-a3946a428ec7'; +export const stubEntityID = EID('00000000-0000-0000-0000-000000000000'); +export const stubEntityIDAlt = EID('caa8b54a-eb5e-4134-8ae2-a3946a428ec7'); -export const stubEntityIDRoot = '00000000-0000-0000-0000-000000000002'; -export const stubEntityIDParent = '00000000-0000-0000-0000-000000000003'; -export const stubEntityIDChild = '00000000-0000-0000-0000-000000000004'; -export const stubEntityIDGrandchild = '00000000-0000-0000-0000-000000000005'; +export const stubEntityIDRoot = EID('00000000-0000-0000-0000-000000000002'); +export const stubEntityIDParent = EID('00000000-0000-0000-0000-000000000003'); +export const stubEntityIDChild = EID('00000000-0000-0000-0000-000000000004'); +export const stubEntityIDGrandchild = EID('00000000-0000-0000-0000-000000000005'); -export const stubPublicDrive = new ArFSPublicDrive( - 'Integration Test', - '1.0', - ArFS_O_11, - JSON_CONTENT_TYPE, - stubEntityID, - 'drive', - 'STUB DRIVE', - stubTransactionID, - 0, - 'public', - stubEntityID -); +export const stubPublicDrive = (): ArFSPublicDrive => + new ArFSPublicDrive( + 'Integration Test', + '1.0', + ArFS_O_11, + JSON_CONTENT_TYPE, + stubEntityID, + 'drive', + 'STUB DRIVE', + stubTransactionID, + new UnixTime(0), + 'public', + stubEntityID + ); export const stubPrivateDrive = new ArFSPrivateDrive( 'Integration Test', @@ -45,7 +57,7 @@ export const stubPrivateDrive = new ArFSPrivateDrive( 'drive', 'STUB DRIVE', stubTransactionID, - 0, + new UnixTime(0), 'private', stubEntityID, 'password', @@ -75,7 +87,7 @@ export const stubPublicFolder = ({ 'folder', folderName, stubTransactionID, - 0, + new UnixTime(0), parentFolderId, folderId ); @@ -95,7 +107,7 @@ export const stubPrivateFolder = ({ 'folder', folderName, stubTransactionID, - 0, + new UnixTime(0), parentFolderId, folderId, 'stubCipher', @@ -105,9 +117,14 @@ export const stubPrivateFolder = ({ interface StubFileParams { driveId?: DriveID; fileName?: string; + txId?: TransactionID; } -export const stubPublicFile = ({ driveId = stubEntityID, fileName = 'STUB NAME' }: StubFileParams): ArFSPublicFile => +export const stubPublicFile = ({ + driveId = stubEntityID, + fileName = 'STUB NAME', + txId = stubTransactionID +}: StubFileParams): ArFSPublicFile => new ArFSPublicFile( 'Integration Test', '1.0', @@ -116,12 +133,12 @@ export const stubPublicFile = ({ driveId = stubEntityID, fileName = 'STUB NAME' driveId, 'file', fileName, - stubTransactionID, - 0, + txId, + new UnixTime(0), stubEntityID, stubEntityID, - 1234567890, - 0, + new ByteCount(1234567890), + new UnixTime(0), stubTransactionID, JSON_CONTENT_TYPE ); @@ -136,11 +153,11 @@ export const stubPrivateFile = ({ driveId = stubEntityID, fileName = 'STUB NAME' 'file', fileName, stubTransactionID, - 0, + new UnixTime(0), stubEntityID, stubEntityID, - 1234567890, - 0, + new ByteCount(1234567890), + new UnixTime(0), stubTransactionID, JSON_CONTENT_TYPE, 'stubCipher', diff --git a/src/wallet.ts b/src/wallet.ts index 213e5442..cf015d3c 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -5,17 +5,21 @@ import jwkToPem, { JWK } from 'jwk-to-pem'; import Arweave from 'arweave'; import * as mnemonicKeys from 'arweave-mnemonic-keys'; import { - TransactionID, - Winston, NetworkReward, PublicKey, - SeedPhrase, DEFAULT_APP_NAME, DEFAULT_APP_VERSION, - RewardSettings + RewardSettings, + W, + Winston, + ArweaveAddress, + AR, + TransactionID, + TxID, + SeedPhrase, + ADDR } from './types'; import { CreateTransactionInterface } from 'arweave/node/common'; -import { ArweaveAddress } from './arweave_address'; export type ARTransferResult = { trxID: TransactionID; @@ -45,7 +49,7 @@ export class JWKWallet implements Wallet { .createHash('sha256') .update(b64UrlToBuffer(await this.getPublicKey())) .digest(); - return Promise.resolve(new ArweaveAddress(bufferTob64Url(result))); + return Promise.resolve(ADDR(bufferTob64Url(result))); } // Use cases: generating drive keys, file keys, etc. @@ -75,25 +79,25 @@ export class WalletDAO { } async generateJWKWallet(seedPhrase: SeedPhrase): Promise { - const jwkWallet: JWKInterface = await mnemonicKeys.getKeyFromMnemonic(seedPhrase); + const jwkWallet: JWKInterface = await mnemonicKeys.getKeyFromMnemonic(seedPhrase.toString()); return Promise.resolve(new JWKWallet(jwkWallet)); } - async getWalletWinstonBalance(wallet: Wallet): Promise { + async getWalletWinstonBalance(wallet: Wallet): Promise { return this.getAddressWinstonBalance(await wallet.getAddress()); } - async getAddressWinstonBalance(address: ArweaveAddress): Promise { - return Promise.resolve(+(await this.arweave.wallets.getBalance(address.toString()))); + async getAddressWinstonBalance(address: ArweaveAddress): Promise { + return Promise.resolve(W(+(await this.arweave.wallets.getBalance(address.toString())))); } async walletHasBalance(wallet: Wallet, winstonPrice: Winston): Promise { const walletBalance = await this.getWalletWinstonBalance(wallet); - return +walletBalance > +winstonPrice; + return walletBalance.isGreaterThan(winstonPrice); } async sendARToAddress( - arAmount: number, + arAmount: AR, fromWallet: Wallet, toAddress: ArweaveAddress, rewardSettings: RewardSettings = {}, @@ -108,14 +112,17 @@ export class WalletDAO { ): Promise { // TODO: Figure out how this works for other wallet types const jwkWallet = fromWallet as JWKWallet; - const winston: Winston = this.arweave.ar.arToWinston(arAmount.toString()); + const winston: Winston = arAmount.toWinston(); // Create transaction - const trxAttributes: Partial = { target: toAddress.toString(), quantity: winston }; + const trxAttributes: Partial = { + target: toAddress.toString(), + quantity: winston.toString() + }; // If we provided our own reward settings, use them now if (rewardSettings.reward) { - trxAttributes.reward = rewardSettings.reward; + trxAttributes.reward = rewardSettings.reward.toString(); } // TODO: Use a mock arweave server instead @@ -123,16 +130,15 @@ export class WalletDAO { trxAttributes.last_tx = 'STUB'; } const transaction = await this.arweave.createTransaction(trxAttributes, jwkWallet.getPrivateKey()); - if (rewardSettings.feeMultiple && rewardSettings.feeMultiple > 1.0) { - // Round up with ceil because fractional Winston will cause an Arweave API failure - transaction.reward = Math.ceil(+transaction.reward * rewardSettings.feeMultiple).toString(); + if (rewardSettings.feeMultiple?.wouldBoostReward()) { + transaction.reward = rewardSettings.feeMultiple.boostReward(transaction.reward); } if (assertBalance) { const fromAddress = await fromWallet.getAddress(); const balanceInWinston = await this.getAddressWinstonBalance(fromAddress); - const total = +transaction.reward + +transaction.quantity; - if (total > balanceInWinston) { + const total = W(transaction.reward).plus(W(transaction.quantity)); + if (total.isGreaterThan(balanceInWinston)) { throw new Error( [ `Insufficient funds for this transaction`, @@ -140,7 +146,7 @@ export class WalletDAO { `minerReward: ${transaction.reward}`, `balance: ${balanceInWinston}`, `total: ${total}`, - `difference: ${total - balanceInWinston}` + `difference: ${Winston.difference(total, balanceInWinston)}` ].join('\n\t') ); } @@ -150,7 +156,7 @@ export class WalletDAO { transaction.addTag('App-Name', appName); transaction.addTag('App-Version', appVersion); transaction.addTag('Type', trxType); - if (rewardSettings.feeMultiple && rewardSettings.feeMultiple > 1.0) { + if (rewardSettings.feeMultiple?.wouldBoostReward()) { transaction.addTag('Boost', rewardSettings.feeMultiple.toString()); } otherTags?.forEach((tag) => { @@ -172,9 +178,9 @@ export class WalletDAO { })(); if (response.status === 200 || response.status === 202) { return Promise.resolve({ - trxID: transaction.id, + trxID: TxID(transaction.id), winston, - reward: transaction.reward + reward: W(transaction.reward) }); } else { throw new Error(`Transaction failed. Response: ${response}`); diff --git a/tests/integration/ardrive.int.test.ts b/tests/integration/ardrive.int.test.ts index cc15ebdd..57f10f60 100644 --- a/tests/integration/ardrive.int.test.ts +++ b/tests/integration/ardrive.int.test.ts @@ -1,7 +1,7 @@ import Arweave from 'arweave'; import { expect } from 'chai'; import { stub } from 'sinon'; -import { ArDrive, ArFSResult } from '../../src/ardrive'; +import { ArDrive } from '../../src/ardrive'; import { readJWKFile, urlEncodeHashKey } from '../../src/utils'; import { stubArweaveAddress, @@ -25,18 +25,21 @@ import { expectAsyncErrorThrow } from '../../src/utils/test_helpers'; import { ArDriveCommunityOracle } from '../../src/community/ardrive_community_oracle'; import { ArFSDAO, PrivateDriveKeyData } from '../../src/arfsdao'; import { deriveDriveKey, DrivePrivacy } from 'ardrive-core-js'; -import { DriveKey, FileID, Winston } from '../../src/types'; +import { DriveKey, FileID, W, Winston, FeeMultiple, EID, UnixTime } from '../../src/types'; import { ArFSFileToUpload, wrapFileOrFolder } from '../../src/arfs_file_wrapper'; +import { RootFolderID } from '../../src/utils/arfs_builders/arfs_folder_builders'; +import { ArFSResult } from '../../src/ardrive.types'; -const entityIdRegex = /^([a-f]|[0-9]){8}-([a-f]|[0-9]){4}-([a-f]|[0-9]){4}-([a-f]|[0-9]){4}-([a-f]|[0-9]){12}$/; -const trxIdRegex = /^([a-zA-Z]|[0-9]|-|_){43}$/; +// Don't use the existing constants just to make sure our expectations don't change +const entityIdRegex = /^[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}$/i; +const trxIdRegex = /^(\w|-){43}$/; const fileKeyRegex = /^([a-zA-Z]|[0-9]|-|_|\/|\+){43}$/; describe('ArDrive class - integrated', () => { const wallet = readJWKFile('./test_wallet.json'); const getStubDriveKey = async (): Promise => { - return deriveDriveKey('stubPassword', stubEntityID, JSON.stringify((wallet as JWKWallet).getPrivateKey())); + return deriveDriveKey('stubPassword', `${stubEntityID}`, JSON.stringify((wallet as JWKWallet).getPrivateKey())); }; const fakeArweave = Arweave.init({ @@ -60,20 +63,21 @@ describe('ArDrive class - integrated', () => { 'Integration Test', '1.0', priceEstimator, - 1.0, + new FeeMultiple(1.0), true ); const walletOwner = stubArweaveAddress(); const unexpectedOwner = stubArweaveAddress('0987654321klmnopqrxtuvwxyz123456789ABCDEFGH'); - const expectedDriveId = stubEntityID; - const unexpectedDriveId = stubEntityIDAlt; - const existingFileId = stubEntityIDAlt; + // Use copies to expose any issues with object equality in tested code + const expectedDriveId = EID(stubEntityID.toString()); + const unexpectedDriveId = EID(stubEntityIDAlt.toString()); + const existingFileId = EID(stubEntityIDAlt.toString()); beforeEach(() => { // Set pricing algo up as x = y (bytes = Winston) - stub(arweaveOracle, 'getWinstonPriceForByteCount').callsFake((input) => Promise.resolve(input)); + stub(arweaveOracle, 'getWinstonPriceForByteCount').callsFake((input) => Promise.resolve(W(+input))); // Declare common stubs stub(walletDao, 'walletHasBalance').resolves(true); @@ -86,13 +90,13 @@ describe('ArDrive class - integrated', () => { it('returns the correct TipResult', async () => { stub(communityOracle, 'selectTokenHolder').resolves(stubArweaveAddress()); - const result = await arDrive.sendCommunityTip('12345'); + const result = await arDrive.sendCommunityTip({ communityWinstonTip: W('12345') }); // Can't know the txID ahead of time without mocking arweave deeply expect(result.tipData.txId).to.match(trxIdRegex); expect(`${result.tipData.recipient}`).to.equal(`${stubArweaveAddress()}`); - expect(result.tipData.winston).to.equal('12345'); - expect(result.reward).to.equal('0'); + expect(`${result.tipData.winston}`).to.equal('12345'); + expect(`${result.reward}`).to.equal('0'); }); }); }); @@ -100,8 +104,8 @@ describe('ArDrive class - integrated', () => { describe('drive function', () => { describe('createPublicDrive', () => { it('returns the correct ArFSResult', async () => { - const result = await arDrive.createPublicDrive('TEST_DRIVE'); - assertCreateDriveExpectations(result, 75, 21); + const result = await arDrive.createPublicDrive({ driveName: 'TEST_DRIVE' }); + assertCreateDriveExpectations(result, W(75), W(21)); }); }); @@ -113,8 +117,11 @@ describe('ArDrive class - integrated', () => { driveKey: stubDriveKey }; - const result = await arDrive.createPrivateDrive('TEST_DRIVE', stubPrivateDriveData); - assertCreateDriveExpectations(result, 91, 37, urlEncodeHashKey(stubDriveKey)); + const result = await arDrive.createPrivateDrive({ + driveName: 'TEST_DRIVE', + newPrivateDriveData: stubPrivateDriveData + }); + assertCreateDriveExpectations(result, W(91), W(37), urlEncodeHashKey(stubDriveKey)); }); }); }); @@ -153,14 +160,14 @@ describe('ArDrive class - integrated', () => { it('returns the correct ArFSResult', async () => { stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); - stub(arfsDao, 'getPublicDrive').resolves(stubPublicDrive); + stub(arfsDao, 'getPublicDrive').resolves(stubPublicDrive()); const result = await arDrive.createPublicFolder({ folderName: 'TEST_FOLDER', driveId: stubEntityID, parentFolderId: stubEntityID }); - assertCreateFolderExpectations(result, 22); + assertCreateFolderExpectations(result, W(22)); }); }); @@ -208,18 +215,24 @@ describe('ArDrive class - integrated', () => { parentFolderId: stubEntityID, driveKey: stubDriveKey }); - assertCreateFolderExpectations(result, 38, urlEncodeHashKey(stubDriveKey)); + assertCreateFolderExpectations(result, W(38), urlEncodeHashKey(stubDriveKey)); }); }); describe('movePublicFolder', () => { const folderHierarchy = { - rootFolder: stubPublicFolder({ folderId: stubEntityIDRoot, parentFolderId: 'root folder' }), - parentFolder: stubPublicFolder({ folderId: stubEntityIDParent, parentFolderId: stubEntityIDRoot }), - childFolder: stubPublicFolder({ folderId: stubEntityIDChild, parentFolderId: stubEntityIDParent }), + rootFolder: stubPublicFolder({ folderId: stubEntityIDRoot, parentFolderId: new RootFolderID() }), + parentFolder: stubPublicFolder({ + folderId: stubEntityIDParent, + parentFolderId: EID(stubEntityIDRoot.toString()) + }), + childFolder: stubPublicFolder({ + folderId: stubEntityIDChild, + parentFolderId: EID(stubEntityIDParent.toString()) + }), grandChildFolder: stubPublicFolder({ folderId: stubEntityIDGrandchild, - parentFolderId: stubEntityIDChild + parentFolderId: EID(stubEntityIDChild.toString()) }) }; @@ -246,7 +259,7 @@ describe('ArDrive class - integrated', () => { await expectAsyncErrorThrow({ promiseToError: arDrive.movePublicFolder({ - folderId: stubEntityID, + folderId: EID(stubEntityID.toString()), newParentFolderId: stubEntityIDAlt }), errorMessage: 'Entity name already exists in destination folder!' @@ -308,8 +321,8 @@ describe('ArDrive class - integrated', () => { await expectAsyncErrorThrow({ promiseToError: arDrive.movePublicFolder({ - folderId: 'not used here', - newParentFolderId: 'we will error for drive ID reasons' + folderId: stubEntityID, + newParentFolderId: stubEntityIDAlt }), errorMessage: 'Entity must stay in the same drive!' }); @@ -324,18 +337,24 @@ describe('ArDrive class - integrated', () => { folderId: folderHierarchy.grandChildFolder.entityId, newParentFolderId: folderHierarchy.parentFolder.entityId }); - assertCreateFolderExpectations(result, 20); + assertCreateFolderExpectations(result, W(20)); }); }); describe('movePrivateFolder', () => { const folderHierarchy = { - rootFolder: stubPrivateFolder({ folderId: stubEntityIDRoot, parentFolderId: 'root folder' }), - parentFolder: stubPrivateFolder({ folderId: stubEntityIDParent, parentFolderId: stubEntityIDRoot }), - childFolder: stubPrivateFolder({ folderId: stubEntityIDChild, parentFolderId: stubEntityIDParent }), + rootFolder: stubPrivateFolder({ folderId: stubEntityIDRoot, parentFolderId: new RootFolderID() }), + parentFolder: stubPrivateFolder({ + folderId: stubEntityIDParent, + parentFolderId: EID(stubEntityIDRoot.toString()) + }), + childFolder: stubPrivateFolder({ + folderId: stubEntityIDChild, + parentFolderId: EID(stubEntityIDParent.toString()) + }), grandChildFolder: stubPrivateFolder({ folderId: stubEntityIDGrandchild, - parentFolderId: stubEntityIDChild + parentFolderId: EID(stubEntityIDChild.toString()) }) }; @@ -428,8 +447,8 @@ describe('ArDrive class - integrated', () => { await expectAsyncErrorThrow({ promiseToError: arDrive.movePrivateFolder({ - folderId: 'not used here', - newParentFolderId: 'we will error for drive ID reasons', + folderId: stubEntityID, + newParentFolderId: stubEntityIDAlt, driveKey: await getStubDriveKey() }), errorMessage: 'Entity must stay in the same drive!' @@ -446,19 +465,19 @@ describe('ArDrive class - integrated', () => { newParentFolderId: folderHierarchy.parentFolder.entityId, driveKey: await getStubDriveKey() }); - assertCreateFolderExpectations(result, 36, urlEncodeHashKey(await getStubDriveKey())); + assertCreateFolderExpectations(result, W(36), urlEncodeHashKey(await getStubDriveKey())); }); }); describe('file function', () => { - const matchingLastModifiedDate = 420; - const differentLastModifiedDate = 1337; + const matchingLastModifiedDate = new UnixTime(420); + const differentLastModifiedDate = new UnixTime(1337); describe('uploadPublicFile', () => { const wrappedFile = wrapFileOrFolder('test_wallet.json') as ArFSFileToUpload; beforeEach(() => { - stub(communityOracle, 'getCommunityWinstonTip').resolves('1'); + stub(communityOracle, 'getCommunityWinstonTip').resolves(W('1')); stub(communityOracle, 'selectTokenHolder').resolves(stubArweaveAddress()); stub(arfsDao, 'getPublicNameConflictInfoInFolder').resolves({ @@ -477,7 +496,10 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile }), + promiseToError: arDrive.uploadPublicFile({ + parentFolderId: EID(stubEntityID.toString()), + wrappedFile + }), errorMessage: 'Supplied wallet is not the owner of this drive!' }); }); @@ -516,14 +538,14 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPublicFile({ - parentFolderId: stubEntityID, + parentFolderId: EID(stubEntityID.toString()), wrappedFile, destinationFileName: 'CONFLICTING_FILE_NAME', conflictResolution: 'replace' }); // Pass expected existing file id, so that the file would be considered a revision - assertUploadFileExpectations(result, 3204, 171, 0, '1', 'public', existingFileId); + assertUploadFileExpectations(result, W(3204), W(171), W(0), W(1), 'public', existingFileId); }); it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => { @@ -556,14 +578,17 @@ describe('ArDrive class - integrated', () => { }); // Pass expected existing file id, so that the file would be considered a revision - assertUploadFileExpectations(result, 3204, 162, 0, '1', 'public', existingFileId); + assertUploadFileExpectations(result, W(3204), W(162), W(0), W('1'), 'public', existingFileId); }); it('returns the correct ArFSResult', async () => { stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); - const result = await arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile }); - assertUploadFileExpectations(result, 3204, 166, 0, '1', 'public'); + const result = await arDrive.uploadPublicFile({ + parentFolderId: EID(stubEntityID.toString()), + wrappedFile + }); + assertUploadFileExpectations(result, W(3204), W(166), W(0), W(1), 'public'); }); }); @@ -571,7 +596,7 @@ describe('ArDrive class - integrated', () => { const wrappedFile = wrapFileOrFolder('test_wallet.json') as ArFSFileToUpload; beforeEach(() => { - stub(communityOracle, 'getCommunityWinstonTip').resolves('1'); + stub(communityOracle, 'getCommunityWinstonTip').resolves(W('1')); stub(communityOracle, 'selectTokenHolder').resolves(stubArweaveAddress()); stub(arfsDao, 'getPrivateNameConflictInfoInFolder').resolves({ @@ -591,7 +616,7 @@ describe('ArDrive class - integrated', () => { await expectAsyncErrorThrow({ promiseToError: arDrive.uploadPrivateFile({ - parentFolderId: stubEntityID, + parentFolderId: EID(stubEntityID.toString()), wrappedFile, driveKey: await getStubDriveKey() }), @@ -643,7 +668,7 @@ describe('ArDrive class - integrated', () => { }); // Pass expected existing file id, so that the file would be considered a revision - assertUploadFileExpectations(result, 3220, 187, 0, '1', 'private', existingFileId); + assertUploadFileExpectations(result, W(3220), W(187), W(0), W(1), 'private', existingFileId); }); it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => { @@ -678,7 +703,7 @@ describe('ArDrive class - integrated', () => { }); // Pass expected existing file id, so that the file would be considered a revision - assertUploadFileExpectations(result, 3220, 178, 0, '1', 'private', existingFileId); + assertUploadFileExpectations(result, W(3220), W(178), W(0), W('1'), 'private', existingFileId); }); it('returns the correct ArFSResult', async () => { @@ -686,11 +711,11 @@ describe('ArDrive class - integrated', () => { const stubDriveKey = await getStubDriveKey(); const result = await arDrive.uploadPrivateFile({ - parentFolderId: stubEntityID, + parentFolderId: EID(stubEntityID.toString()), wrappedFile, driveKey: stubDriveKey }); - assertUploadFileExpectations(result, 3220, 182, 0, '1', 'private'); + assertUploadFileExpectations(result, W(3220), W(182), W(0), W(1), 'private'); }); }); @@ -703,7 +728,10 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePublicFile(stubEntityID, stubEntityIDAlt), + promiseToError: arDrive.movePublicFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt + }), errorMessage: 'Supplied wallet is not the owner of this drive!' }); }); @@ -713,7 +741,10 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePublicFile(stubEntityID, stubEntityIDAlt), + promiseToError: arDrive.movePublicFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt + }), errorMessage: 'Entity name already exists in destination folder!' }); }); @@ -722,7 +753,12 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getPublicFile').resolves(stubPublicFile({})); stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); - await expectAsyncErrorThrow({ promiseToError: arDrive.movePublicFile(stubEntityID, stubEntityID) }); + await expectAsyncErrorThrow({ + promiseToError: arDrive.movePublicFile({ + fileId: stubEntityID, + newParentFolderId: EID(stubEntityID.toString()) + }) + }); }); it('throws an error if the file is being moved to a different drive', async () => { @@ -730,7 +766,10 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePublicFile(stubEntityID, stubEntityID), + promiseToError: arDrive.movePublicFile({ + fileId: stubEntityID, + newParentFolderId: EID(stubEntityID.toString()) + }), errorMessage: 'Entity must stay in the same drive!' }); }); @@ -739,8 +778,11 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getPublicFile').resolves(stubPublicFile({})); stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); - const result = await arDrive.movePublicFile(stubEntityID, stubEntityIDAlt); - assertMoveFileExpectations(result, 153, 'public'); + const result = await arDrive.movePublicFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt + }); + assertMoveFileExpectations(result, W(153), 'public'); }); }); @@ -753,7 +795,11 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePrivateFile(stubEntityID, stubEntityIDAlt, await getStubDriveKey()), + promiseToError: arDrive.movePrivateFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt, + driveKey: await getStubDriveKey() + }), errorMessage: 'Supplied wallet is not the owner of this drive!' }); }); @@ -763,7 +809,11 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePrivateFile(stubEntityID, stubEntityIDAlt, await getStubDriveKey()), + promiseToError: arDrive.movePrivateFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt, + driveKey: await getStubDriveKey() + }), errorMessage: 'Entity name already exists in destination folder!' }); }); @@ -773,7 +823,11 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePrivateFile(stubEntityID, stubEntityID, await getStubDriveKey()), + promiseToError: arDrive.movePrivateFile({ + fileId: stubEntityID, + newParentFolderId: EID(stubEntityID.toString()), + driveKey: await getStubDriveKey() + }), errorMessage: `File already has parent folder with ID: ${stubEntityID}` }); }); @@ -783,7 +837,11 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); await expectAsyncErrorThrow({ - promiseToError: arDrive.movePrivateFile(stubEntityID, stubEntityID, await getStubDriveKey()), + promiseToError: arDrive.movePrivateFile({ + fileId: stubEntityID, + newParentFolderId: EID(stubEntityID.toString()), + driveKey: await getStubDriveKey() + }), errorMessage: 'Entity must stay in the same drive!' }); }); @@ -792,12 +850,12 @@ describe('ArDrive class - integrated', () => { stub(arfsDao, 'getPrivateFile').resolves(stubPrivateFile({})); stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); - const result = await arDrive.movePrivateFile( - stubEntityID, - stubEntityIDAlt, - await getStubDriveKey() - ); - assertMoveFileExpectations(result, 169, 'private'); + const result = await arDrive.movePrivateFile({ + fileId: stubEntityID, + newParentFolderId: stubEntityIDAlt, + driveKey: await getStubDriveKey() + }); + assertMoveFileExpectations(result, W(169), 'private'); }); }); }); @@ -806,8 +864,8 @@ describe('ArDrive class - integrated', () => { function assertCreateDriveExpectations( result: ArFSResult, - driveFee: number, - folderFee: number, + driveFee: Winston, + folderFee: Winston, expectedDriveKey?: string ) { // Ensure that 2 arfs entities were created @@ -835,15 +893,15 @@ function assertCreateDriveExpectations( // Ensure that the fees look healthy const feeKeys = Object.keys(result.fees); expect(feeKeys.length).to.equal(2); - expect(feeKeys[0]).to.equal(driveEntity.metadataTxId); + expect(feeKeys[0]).to.equal(driveEntity.metadataTxId.toString()); expect(feeKeys[0]).to.match(trxIdRegex); - expect(result.fees[driveEntity.metadataTxId]).to.equal(driveFee); - expect(feeKeys[1]).to.equal(rootFolderEntity.metadataTxId); + expect(`${result.fees[driveEntity.metadataTxId.toString()]}`).to.equal(`${driveFee}`); + expect(feeKeys[1]).to.equal(rootFolderEntity.metadataTxId.toString()); expect(feeKeys[1]).to.match(trxIdRegex); - expect(result.fees[rootFolderEntity.metadataTxId]).to.equal(folderFee); + expect(`${result.fees[rootFolderEntity.metadataTxId.toString()]}`).to.equal(`${folderFee}`); } -function assertCreateFolderExpectations(result: ArFSResult, folderFee: number, expectedDriveKey?: string) { +function assertCreateFolderExpectations(result: ArFSResult, folderFee: Winston, expectedDriveKey?: string) { // Ensure that 1 arfs entity was created expect(result.created.length).to.equal(1); @@ -862,15 +920,15 @@ function assertCreateFolderExpectations(result: ArFSResult, folderFee: number, e const feeKeys = Object.keys(result.fees); expect(feeKeys.length).to.equal(1); expect(feeKeys[0]).to.match(trxIdRegex); - expect(feeKeys[0]).to.equal(folderEntity.metadataTxId); - expect(result.fees[folderEntity.metadataTxId]).to.equal(folderFee); + expect(feeKeys[0]).to.equal(folderEntity.metadataTxId.toString()); + expect(`${result.fees[folderEntity.metadataTxId.toString()]}`).to.equal(`${folderFee}`); } function assertUploadFileExpectations( result: ArFSResult, - fileFee: number, - metadataFee: number, - tipFee: number, + fileFee: Winston, + metadataFee: Winston, + tipFee: Winston, expectedTip: Winston, drivePrivacy: DrivePrivacy, expectedFileId?: FileID @@ -901,7 +959,7 @@ function assertUploadFileExpectations( expect(result.tips.length).to.equal(1); const uploadTip = result.tips[0]; expect(uploadTip.txId).to.match(trxIdRegex); - expect(uploadTip.winston).to.equal(expectedTip); + expect(`${uploadTip.winston}`).to.equal(`${expectedTip}`); expect(uploadTip.recipient).to.match(trxIdRegex); // Ensure that the fees look healthy @@ -909,20 +967,21 @@ function assertUploadFileExpectations( const feeKeys = Object.keys(result.fees); expect(feeKeys[0]).to.match(trxIdRegex); - expect(feeKeys[0]).to.equal(fileEntity.dataTxId); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(result.fees[fileEntity.dataTxId!]).to.equal(fileFee); + expect(feeKeys[0]).to.equal(fileEntity.dataTxId!.toString()); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(`${result.fees[fileEntity.dataTxId!.toString()]}`).to.equal(`${fileFee}`); expect(feeKeys[1]).to.match(trxIdRegex); - expect(feeKeys[1]).to.equal(fileEntity.metadataTxId); - expect(result.fees[fileEntity.metadataTxId]).to.equal(metadataFee); + expect(feeKeys[1]).to.equal(fileEntity.metadataTxId.toString()); + expect(`${result.fees[fileEntity.metadataTxId.toString()]}`).to.equal(`${metadataFee}`); expect(feeKeys[2]).to.match(trxIdRegex); - expect(feeKeys[2]).to.equal(uploadTip.txId); - expect(result.fees[uploadTip.txId]).to.equal(tipFee); + expect(feeKeys[2]).to.equal(uploadTip.txId.toString()); + expect(`${result.fees[uploadTip.txId.toString()]}`).to.equal(`${tipFee}`); } -function assertMoveFileExpectations(result: ArFSResult, fileFee: number, drivePrivacy: DrivePrivacy) { +function assertMoveFileExpectations(result: ArFSResult, fileFee: Winston, drivePrivacy: DrivePrivacy) { // Ensure that 1 arfs entity was created expect(result.created.length).to.equal(1); @@ -947,6 +1006,6 @@ function assertMoveFileExpectations(result: ArFSResult, fileFee: number, drivePr const feeKeys = Object.keys(result.fees); expect(feeKeys.length).to.equal(1); expect(feeKeys[0]).to.match(trxIdRegex); - expect(feeKeys[0]).to.equal(fileEntity.metadataTxId); - expect(result.fees[fileEntity.metadataTxId]).to.equal(fileFee); + expect(feeKeys[0]).to.equal(fileEntity.metadataTxId.toString()); + expect(`${result.fees[fileEntity.metadataTxId.toString()]}`).to.equal(`${fileFee}`); } diff --git a/yarn.lock b/yarn.lock index 52e1f53a..cb855adb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1312,6 +1312,7 @@ __metadata: arweave-bundles: ^1.0.3 arweave-mnemonic-keys: ^0.0.9 base64-js: ^1.5.1 + bignumber.js: ^9.0.0 chai: ^4.3.4 commander: ^8.2.0 eslint: ^7.23.0 @@ -1570,7 +1571,7 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.0.1": +"bignumber.js@npm:^9.0.0, bignumber.js@npm:^9.0.1": version: 9.0.1 resolution: "bignumber.js@npm:9.0.1" checksum: 605e9639c413f344c37b23e919254f60a5017cc5ccd925e2f8fb79b36aa3d54f356df9c726f38465263236455f685d60dcf38dbe32cb0b7e4d2a32c94b035476 @@ -2034,9 +2035,9 @@ __metadata: linkType: hard "commander@npm:^8.2.0": - version: 8.2.0 - resolution: "commander@npm:8.2.0" - checksum: e41e680f2afa0a409aa21d3cad6f06a3d9d2d1086e76223ebd600181b588085e6ad0c7387cf491cb96c60de0a0a50815130368b40bfde017744210d4a8fb75a7 + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 0b818d97ca9c8ad461ff05f873082af0c1895e49984a2abf1090e4a85c3537c3c0f9921e38314a9de7e71941f00f710c204d3560e3a7bc0b1023eef92d6bbfc0 languageName: node linkType: hard From bb62f6071eba0f82ad5a1cdc2f7894b8b57612a1 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 15 Nov 2021 15:11:48 -0600 Subject: [PATCH 10/30] refactor(manifests): Use migrated core PE-477 --- .pnp.js | 10 +++++----- ...npm-0.6.0-alpha-1-90c637583d-2eebcb3cd3.zip | Bin 138885 -> 0 bytes package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 .yarn/cache/ardrive-core-js-npm-0.6.0-alpha-1-90c637583d-2eebcb3cd3.zip diff --git a/.pnp.js b/.pnp.js index 8892806a..888a4334 100755 --- a/.pnp.js +++ b/.pnp.js @@ -48,7 +48,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:0.6.0-alpha-1"], + ["ardrive-core-js", "file:../ardrive-core-js#../ardrive-core-js::hash=ec5434&locator=ardrive-cli%40workspace%3A."], ["arweave", "npm:1.10.16"], ["chai", "npm:4.3.4"], ["commander", "npm:8.3.0"], @@ -1292,7 +1292,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:0.6.0-alpha-1"], + ["ardrive-core-js", "file:../ardrive-core-js#../ardrive-core-js::hash=ec5434&locator=ardrive-cli%40workspace%3A."], ["arweave", "npm:1.10.16"], ["chai", "npm:4.3.4"], ["commander", "npm:8.3.0"], @@ -1317,10 +1317,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["ardrive-core-js", [ - ["npm:0.6.0-alpha-1", { - "packageLocation": "./.yarn/cache/ardrive-core-js-npm-0.6.0-alpha-1-90c637583d-2eebcb3cd3.zip/node_modules/ardrive-core-js/", + ["file:../ardrive-core-js#../ardrive-core-js::hash=ec5434&locator=ardrive-cli%40workspace%3A.", { + "packageLocation": "./.yarn/unplugged/ardrive-core-js-file-c63148a9da/node_modules/ardrive-core-js/", "packageDependencies": [ - ["ardrive-core-js", "npm:0.6.0-alpha-1"], + ["ardrive-core-js", "file:../ardrive-core-js#../ardrive-core-js::hash=ec5434&locator=ardrive-cli%40workspace%3A."], ["arweave", "npm:1.10.16"], ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], diff --git a/.yarn/cache/ardrive-core-js-npm-0.6.0-alpha-1-90c637583d-2eebcb3cd3.zip b/.yarn/cache/ardrive-core-js-npm-0.6.0-alpha-1-90c637583d-2eebcb3cd3.zip deleted file mode 100644 index 1b7ccf1eef2d26f026d79f7cc7fe0c2e399a3d1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138885 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

2~F&{ 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 Date: Mon, 22 Nov 2021 11:12:49 -0600 Subject: [PATCH 11/30] docs(manifest): Add manifest info to README PE-477 --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6b28999..0045aff4 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ ardrive upload-file --wallet-file /path/to/my/wallet.json --parent-folder-id "f0 1. [Uploading a Single File](#uploading-a-single-file) 2. [Uploading a Folder with Files](#bulk-upload) 3. [Fetching the Metadata of a File Entity](#fetching-the-metadata-of-a-file-entity) - 4. [Create New Drive and Upload Folder Pipeline Example](#create-upload-pipeline) + 4. [Uploading Manifests](#uploading-manifests) + 5. [Create New Drive and Upload Folder Pipeline Example](#create-upload-pipeline) 7. [Other Utility Operations](#other-utility-operations) 1. [Monitoring Transactions](#monitoring-transactions) 2. [Dealing With Network Congestion](#dealing-with-network-congestion) @@ -763,6 +764,40 @@ Example output: } ``` +### Uploading Manifests + +ArDrive supports the creation of Arweave manifests using any of your PUBLIC folders or drives. This manifest will be created at the root of specified folder or drive just as any other ArFS file entity: + +```shell +ardrive create-manifest -d bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet +``` + +This manifest is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The file itself is a .json file that holds the paths (the data transaction ids) to each file in the specified folder/drive. + +These paths are used by the gateway to create working links to each of your files: + +```shell +arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4", + "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/e.png", + "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world.txt", + "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world2.txt" +``` + +When creating this manifest, you can link up an index.html page as the first path by uploading it in to the root of the folder or drive before creating a manifest. Using this method, your index.html will even be able path to assets within the folder tree: + +```shell +my-ardrive-folder + index.html + css + styles.css + js + scripts.js + font + my-font.ttf +``` + +This is effectively hosting a web page on ArDrive. See our [example manifest web page](arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o). + ### Create New Drive and Upload Folder Pipeline Example ```shell @@ -857,6 +892,7 @@ Write ArFS create-drive create-folder upload-file +create-manifest move-file move-folder From 7e00f918d499e025c893566d416451a36654f9e1 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 22 Nov 2021 11:19:48 -0600 Subject: [PATCH 12/30] docs(manifest): Change spacing and phrasing README PE-477 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0045aff4..078b8b65 100644 --- a/README.md +++ b/README.md @@ -772,15 +772,15 @@ ArDrive supports the creation of Arweave manifests using any of your PUBLIC fold ardrive create-manifest -d bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet ``` -This manifest is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The file itself is a .json file that holds the paths (the data transaction ids) to each file in the specified folder/drive. +The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The file itself is a .json file that holds the paths (the data transaction ids) to each file in the specified folder/drive. These paths are used by the gateway to create working links to each of your files: ```shell arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4", - "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/e.png", - "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world.txt", - "arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world2.txt" +"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/e.png", +"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world.txt", +"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world2.txt" ``` When creating this manifest, you can link up an index.html page as the first path by uploading it in to the root of the folder or drive before creating a manifest. Using this method, your index.html will even be able path to assets within the folder tree: From 7d4188fa9c2694eef30d5dece96a9a1840acd773 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 22 Nov 2021 13:12:15 -0600 Subject: [PATCH 13/30] docs(manifest): Add information about updating manifests PE-477 --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 078b8b65..c33755fd 100644 --- a/README.md +++ b/README.md @@ -774,6 +774,10 @@ ardrive create-manifest -d bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wall The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The file itself is a .json file that holds the paths (the data transaction ids) to each file in the specified folder/drive. +When your drive or folder is later changed by adding files or updating them with new revisions, the original manifest will not be updated on its own. A manifest is a permanent record of your files in their current state. + +However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). + These paths are used by the gateway to create working links to each of your files: ```shell @@ -796,7 +800,7 @@ my-ardrive-folder my-font.ttf ``` -This is effectively hosting a web page on ArDrive. See our [example manifest web page](arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o). +This is effectively hosting a web page on ArDrive. See our [example manifest web page][example-manifest-webpage]. You can find out more about Arweave path manifests [here][arweave-manifest] ### Create New Drive and Upload Folder Pipeline Example @@ -953,3 +957,5 @@ ardrive --help [ardrive-discord]: https://discord.gg/w4vvrezD [arconnect]: https://arconnect.io/ [kb-wallets]: https://ardrive.atlassian.net/l/c/FpK8FuoQ +[arweave-manifests]: https://github.com/ArweaveTeam/arweave/wiki/Path-Manifests +[example-manifest-webpage]: arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o From d12105d510cce486eda79588954d505c4be872b5 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 23 Nov 2021 15:48:43 -0600 Subject: [PATCH 14/30] refactor(create manifest): Require folderId, disallow driveID PE-477 --- src/commands/create_manifest.ts | 18 ++---------------- src/parameter_declarations.ts | 6 ++++-- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index 8db8f5dd..d78a769d 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -6,7 +6,6 @@ import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes'; import { BoostParameter, FolderIdParameter, - DriveIdParameter, DryRunParameter, SeedPhraseParameter, TreeDepthParams, @@ -17,7 +16,6 @@ import { new CLICommand({ name: 'create-manifest', parameters: [ - DriveIdParameter, FolderIdParameter, DestinationManifestNameParameter, BoostParameter, @@ -37,25 +35,13 @@ new CLICommand({ dryRun: !!options.dryRun }); - // User can specify either a drive ID or a folder ID - const driveId = parameters.getParameterValue(DriveIdParameter); - const folderId = parameters.getParameterValue(FolderIdParameter); - - if (driveId && folderId) { - throw new Error('Drive ID cannot be used in conjunction with folder ID!'); - } - - if (!driveId && !folderId) { - throw new Error('Must provide either a drive ID or a folder ID to!'); - } + const folderId = parameters.getRequiredParameterValue(FolderIdParameter, EID); const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); const destManifestName = parameters.getParameterValue(DestinationManifestNameParameter); - // TODO: Private manifests 🤔 const result = await arDrive.uploadPublicManifest({ - driveId: driveId ? EID(driveId) : undefined, - folderId: folderId ? EID(folderId) : undefined, + folderId: folderId, maxDepth, destManifestName }); diff --git a/src/parameter_declarations.ts b/src/parameter_declarations.ts index dfe4fb29..16a60291 100644 --- a/src/parameter_declarations.ts +++ b/src/parameter_declarations.ts @@ -148,7 +148,8 @@ Parameter.declare({ Parameter.declare({ name: DriveIdParameter, aliases: ['-d', '--drive-id'], - description: 'the ArFS entity ID associated with the target drive' + description: 'the ArFS entity ID associated with the target drive', + required: true }); Parameter.declare({ @@ -190,7 +191,8 @@ Parameter.declare({ Parameter.declare({ name: FolderIdParameter, aliases: ['-f', '--folder-id'], - description: `the ArFS folder ID for the folder to query` + description: `the ArFS folder ID for the folder to query`, + required: true }); Parameter.declare({ From 579ae71e72ba7c75c7134253cb73777f06780c63 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 23 Nov 2021 15:51:05 -0600 Subject: [PATCH 15/30] feat(create manifest): Add conflict resolution params PE-477 --- src/commands/create_manifest.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/create_manifest.ts b/src/commands/create_manifest.ts index d78a769d..70c6c3c2 100644 --- a/src/commands/create_manifest.ts +++ b/src/commands/create_manifest.ts @@ -10,7 +10,8 @@ import { SeedPhraseParameter, TreeDepthParams, WalletFileParameter, - DestinationManifestNameParameter + DestinationManifestNameParameter, + ConflictResolutionParams } from '../parameter_declarations'; new CLICommand({ @@ -22,6 +23,7 @@ new CLICommand({ DryRunParameter, WalletFileParameter, SeedPhraseParameter, + ...ConflictResolutionParams, ...TreeDepthParams ], action: new CLIAction(async function action(options) { @@ -39,11 +41,13 @@ new CLICommand({ const maxDepth = await parameters.getMaxDepth(Number.MAX_SAFE_INTEGER); const destManifestName = parameters.getParameterValue(DestinationManifestNameParameter); + const conflictResolution = parameters.getFileNameConflictResolution(); const result = await arDrive.uploadPublicManifest({ folderId: folderId, maxDepth, - destManifestName + destManifestName, + conflictResolution }); console.log(JSON.stringify(result, null, 4)); From 16d92b3a79f8097a97843a61278868696f0272d8 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 24 Nov 2021 11:04:59 -0600 Subject: [PATCH 16/30] docs(create manifest): Adjust manifest information PE-477 --- README.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c33755fd..c379c3d3 100644 --- a/README.md +++ b/README.md @@ -766,28 +766,32 @@ Example output: ### Uploading Manifests -ArDrive supports the creation of Arweave manifests using any of your PUBLIC folders or drives. This manifest will be created at the root of specified folder or drive just as any other ArFS file entity: +[Arweave Path Manifests][arweave-manifests] are are special `.json` files that instruct Arweave Gateways to map file data associated with specific, disparate transaction IDs to customized, hosted paths relative to that of the manifest file itself. So if, for example, your manifest file had an arweave.net URL like: ```shell -ardrive create-manifest -d bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet +https://arweave.net/{manifest tx id} ``` -The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The file itself is a .json file that holds the paths (the data transaction ids) to each file in the specified folder/drive. +Then, all the mapped txs and paths in the manifest file would be addressable at URLs like: -When your drive or folder is later changed by adding files or updating them with new revisions, the original manifest will not be updated on its own. A manifest is a permanent record of your files in their current state. - -However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). +```shell +https://arweave.net/{manifest tx id}/foo.txt +https://arweave.net/{manifest tx id}/bar/baz.png +``` -These paths are used by the gateway to create working links to each of your files: +ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders or drives. This manifest will reside in the root folder of the drive they describe. ```shell -arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4", -"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/e.png", -"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world.txt", -"arweave.net/ztZAc-ahH2Im3EytKOfmO85Ae1zYfRK8k2cSzw9wHx4/a_fun_subfolder/hello_world2.txt" +ardrive create-manifest -f bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet ``` -When creating this manifest, you can link up an index.html page as the first path by uploading it in to the root of the folder or drive before creating a manifest. Using this method, your index.html will even be able path to assets within the folder tree: +The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file in the specified folder/drive. + +When your drive or folder is later changed by adding files or updating them with new revisions, the original manifest will NOT be updated on its own. A manifest is a permanent record of your files in their current state. + +However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). + +When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder or drive before creating a manifest. Using this method, your `index.html` will even be able path to assets within the folder tree: ```shell my-ardrive-folder @@ -800,7 +804,7 @@ my-ardrive-folder my-font.ttf ``` -This is effectively hosting a web page on ArDrive. See our [example manifest web page][example-manifest-webpage]. You can find out more about Arweave path manifests [here][arweave-manifest] +This is effectively hosting a web page on ArDrive. See our [example manifest web page][example-manifest-webpage]. ### Create New Drive and Upload Folder Pipeline Example From aa37049d923de1ad016c940ce8419be2d901c007 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 24 Nov 2021 11:20:11 -0600 Subject: [PATCH 17/30] docs(create manifest): Specify only folders as param in README PE-477 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c379c3d3..b5b82de7 100644 --- a/README.md +++ b/README.md @@ -779,7 +779,7 @@ https://arweave.net/{manifest tx id}/foo.txt https://arweave.net/{manifest tx id}/bar/baz.png ``` -ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders or drives. This manifest will reside in the root folder of the drive they describe. +ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders. To create a manifest of an entire public drive, specify the root folder of that drive. The generated manifest will reside in the root of the folder they describe. ```shell ardrive create-manifest -f bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet From 40509b142a8c56ef4a43de303afa55abe9700962 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 24 Nov 2021 12:41:14 -0600 Subject: [PATCH 18/30] docs(create manifest): Specify only folders as param in README PE-477 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b5b82de7..edc2a823 100644 --- a/README.md +++ b/README.md @@ -785,13 +785,13 @@ ArDrive supports the creation of these Arweave manifests using any of your PUBLI ardrive create-manifest -f bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet ``` -The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file in the specified folder/drive. +The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file in the specified folder. -When your drive or folder is later changed by adding files or updating them with new revisions, the original manifest will NOT be updated on its own. A manifest is a permanent record of your files in their current state. +When your folder is later changed by adding files or updating them with new revisions, the original manifest will NOT be updated on its own. A manifest is a permanent record of your files in their current state. However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). -When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder or drive before creating a manifest. Using this method, your `index.html` will even be able path to assets within the folder tree: +When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder before creating a manifest. Using this method, your `index.html` will even be able path to assets within the folder tree: ```shell my-ardrive-folder From 6ae3bfb9669e0debe4a746eed0bb83c1aceb6bd5 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 24 Nov 2021 14:01:52 -0600 Subject: [PATCH 19/30] docs(create manifest): README rephrasing PE-477 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index edc2a823..0171dc15 100644 --- a/README.md +++ b/README.md @@ -779,19 +779,19 @@ https://arweave.net/{manifest tx id}/foo.txt https://arweave.net/{manifest tx id}/bar/baz.png ``` -ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders. To create a manifest of an entire public drive, specify the root folder of that drive. The generated manifest will reside in the root of the folder they describe. +ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders. To create a manifest of an entire public drive, specify the root folder of that drive. The generated manifest will reside in the root of the folder it describes. ```shell ardrive create-manifest -f bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet ``` -The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file in the specified folder. +The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file within the specified folder. When your folder is later changed by adding files or updating them with new revisions, the original manifest will NOT be updated on its own. A manifest is a permanent record of your files in their current state. However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). -When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder before creating a manifest. Using this method, your `index.html` will even be able path to assets within the folder tree: +When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder before creating a manifest. Using this method, your `index.html` will even be able to path to assets within the folder tree: ```shell my-ardrive-folder @@ -804,7 +804,7 @@ my-ardrive-folder my-font.ttf ``` -This is effectively hosting a web page on ArDrive. See our [example manifest web page][example-manifest-webpage]. +This is effectively hosting a web page with ArDrive. See our [example manifest web page][example-manifest-webpage]. ### Create New Drive and Upload Folder Pipeline Example From 643801616edd0754de9ace899965e3536af7fead Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 24 Nov 2021 14:26:08 -0600 Subject: [PATCH 20/30] chore(version): Use alpha core release PE-477 --- .pnp.js | 10 +++++----- ...npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip | Bin 0 -> 141375 bytes package.json | 2 +- yarn.lock | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 .yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip diff --git a/.pnp.js b/.pnp.js index 46f8e315..5f71f273 100755 --- a/.pnp.js +++ b/.pnp.js @@ -48,7 +48,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.3"], + ["ardrive-core-js", "npm:1.0.3-alpha-1"], ["arweave", "npm:1.10.16"], ["chai", "npm:4.3.4"], ["commander", "npm:8.3.0"], @@ -1292,7 +1292,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.3"], + ["ardrive-core-js", "npm:1.0.3-alpha-1"], ["arweave", "npm:1.10.16"], ["chai", "npm:4.3.4"], ["commander", "npm:8.3.0"], @@ -1317,10 +1317,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["ardrive-core-js", [ - ["npm:1.0.3", { - "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.3-3eb958d43d-ee93abf5a7.zip/node_modules/ardrive-core-js/", + ["npm:1.0.3-alpha-1", { + "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip/node_modules/ardrive-core-js/", "packageDependencies": [ - ["ardrive-core-js", "npm:1.0.3"], + ["ardrive-core-js", "npm:1.0.3-alpha-1"], ["arweave", "npm:1.10.16"], ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], diff --git a/.yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip b/.yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip new file mode 100644 index 0000000000000000000000000000000000000000..dc8f8bcd9dbaca66c780a8cf7aa143a551d0cfe8 GIT binary patch literal 141375 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

iUr*s#5*#GBIA6Ka;z# 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

BV7gU3y@?IBI6r+xK(o9 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 Date: Mon, 29 Nov 2021 11:13:27 -0600 Subject: [PATCH 21/30] docs(create manifest): Fix broken link PE-477 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0171dc15..9259bf31 100644 --- a/README.md +++ b/README.md @@ -962,4 +962,4 @@ ardrive --help [arconnect]: https://arconnect.io/ [kb-wallets]: https://ardrive.atlassian.net/l/c/FpK8FuoQ [arweave-manifests]: https://github.com/ArweaveTeam/arweave/wiki/Path-Manifests -[example-manifest-webpage]: arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o +[example-manifest-webpage]: https://arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o From 7f741eb84554bd483abcbd7ed1e4ab764bfe2ca7 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 29 Nov 2021 16:45:34 -0600 Subject: [PATCH 22/30] chore(version): Bump dev to 1.1.5 for next release PE-733 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afe9e26d..7c0bdec3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ardrive-cli", - "version": "1.1.4", + "version": "1.1.5", "description": "The ArDrive Command Line Interface (CLI is a Node.js application for terminal-based ArDrive workflows. It also offers utility operations for securely interacting with Arweave wallets and inspecting various Arweave blockchain conditions.", "main": "./lib/index.js", "bin": { From 5155d8ba4896404ac223f3381907b4284d75522c Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 29 Nov 2021 21:36:18 -0600 Subject: [PATCH 23/30] docs(create-manifest): Add hosting a web app example PE-477 --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9259bf31..39f5ec68 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,8 @@ ardrive upload-file --wallet-file /path/to/my/wallet.json --parent-folder-id "f0 2. [Uploading a Folder with Files](#bulk-upload) 3. [Fetching the Metadata of a File Entity](#fetching-the-metadata-of-a-file-entity) 4. [Uploading Manifests](#uploading-manifests) - 5. [Create New Drive and Upload Folder Pipeline Example](#create-upload-pipeline) + 5. [Hosting a Webpage with Manifest](#hosting-a-webpage-with-manifest) + 6. [Create New Drive and Upload Folder Pipeline Example](#create-upload-pipeline) 7. [Other Utility Operations](#other-utility-operations) 1. [Monitoring Transactions](#monitoring-transactions) 2. [Dealing With Network Congestion](#dealing-with-network-congestion) @@ -772,17 +773,26 @@ Example output: https://arweave.net/{manifest tx id} ``` -Then, all the mapped txs and paths in the manifest file would be addressable at URLs like: +Then, all the mapped transactions and paths in the manifest file would be addressable at URLs like: ```shell https://arweave.net/{manifest tx id}/foo.txt https://arweave.net/{manifest tx id}/bar/baz.png ``` -ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders. To create a manifest of an entire public drive, specify the root folder of that drive. The generated manifest will reside in the root of the folder it describes. +ArDrive supports the creation of these Arweave manifests using any of your PUBLIC folders. The generated manifest paths will be links to each of the file entities within the specified folder. The manifest file entity will be created at the root of the folder. + +To create a manifest of an entire public drive, specify the root folder of that drive: + +```shell +ardrive create-manifest -f "bc9af866-6421-40f1-ac89-202bddb5c487" -w "/path/to/wallet" +``` + +You can also create a manifest of a folder's file entities at a custom depth by using the `--max-depth` option: ```shell -ardrive create-manifest -f bc9af866-6421-40f1-ac89-202bddb5c487 -w /path/to/wallet +# Create manifest of a folder's local file contents, excluding all sub-folders +ardrive create-manifest --max-depth 0 -f "867228d8-4413-4c0e-a499-e1decbf2ea38" -w "/path/to/wallet" ``` The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file within the specified folder. @@ -791,20 +801,44 @@ When your folder is later changed by adding files or updating them with new revi However, creating a subsequent manifest with the same manifest name will create a new revision of that manifest in its new current state. Manifests follow the same name conflict resolution as outlined for files above (upsert by default). -When creating this manifest, you can link up an `index.html` web page as the first path by uploading that `index.html` file into the root of the folder before creating a manifest. Using this method, your `index.html` will even be able to path to assets within the folder tree: +#### Hosting a Webpage with Manifest + +When creating a manifest, it is possible to host a webpage or web app. You can do this by creating a manifest on a folder that has an `index.html` file in its root. + +Using generated build folders from popular frameworks works as well. One requirement here to note is that the `href=` paths from your generated `index.html` file must not have leading a `/`. This means that the manifest will not resolve a path of `/dist/index.js` but it will resolve `dist/index.js` or `./dist/index.js`. + +As an example, here is a flow of creating a React app and hosting it with an ArDrive Manifest. First, generate a React app: + +```shell +yarn create react-app my-app +``` + +Next, add this field to the generated `package.json` so that the paths will resolve correctly: + +```json +"homepage": ".", +``` + +Then, create an optimized production build from within the app's directory: + +```shell +yarn build +``` + +Now, we can create and upload that produced build folder on ArDrive to any of your existing ArFS folder entities: + +```shell +ardrive upload-file -l "/build" -w "/path/to/wallet" --parent-folder-id "bc9af866-6421-40f1-ac89-202bddb5c487" +``` + +And finally, create the manifest using the generated Folder ID from the build folder creation: ```shell -my-ardrive-folder - index.html - css - styles.css - js - scripts.js - font - my-font.ttf +# Create manifest using the Folder ID of the `/build` folder +ardrive create-manifest -f "41759f05-614d-45ad-846b-63f3767504a4" -w "/path/to/wallet" ``` -This is effectively hosting a web page with ArDrive. See our [example manifest web page][example-manifest-webpage]. +This is effectively hosting a web app with ArDrive. Check out the ArDrive Price Calculator React App hosted as an [ArDrive Manifest][example-manifest-webpage]. ### Create New Drive and Upload Folder Pipeline Example @@ -962,4 +996,4 @@ ardrive --help [arconnect]: https://arconnect.io/ [kb-wallets]: https://ardrive.atlassian.net/l/c/FpK8FuoQ [arweave-manifests]: https://github.com/ArweaveTeam/arweave/wiki/Path-Manifests -[example-manifest-webpage]: https://arweave.net/V_L4J79QOrjQ_1Nbh5yAetVn8OY_KzvagIFNdCn1X_o +[example-manifest-webpage]: https://arweave.net/qozq9YIUPEHfZhoTp9DkBpJuA_KNULBnfLiMroj5pZI From 4ed1c48bbe72b567c0f9b57b4481696827de4088 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 30 Nov 2021 09:13:38 -0600 Subject: [PATCH 24/30] docs(create-manifest): Add return output example PE-477 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 609b4930..b7406f94 100644 --- a/README.md +++ b/README.md @@ -839,6 +839,17 @@ And finally, create the manifest using the generated Folder ID from the build fo ardrive create-manifest -f "41759f05-614d-45ad-846b-63f3767504a4" -w "/path/to/wallet" ``` +In the return output, the top link will be a link to the deployed web app: + +```shell + "links": [ + "https://arweave.net/0MK68J8TqGhaaOpPe713Zn0jdpczMt2NGS2CtRYiuAg", + "https://arweave.net/0MK68J8TqGhaaOpPe713Zn0jdpczMt2NGS2CtRYiuAg/asset-manifest.json", + "https://arweave.net/0MK68J8TqGhaaOpPe713Zn0jdpczMt2NGS2CtRYiuAg/favicon.ico", + "https://arweave.net/0MK68J8TqGhaaOpPe713Zn0jdpczMt2NGS2CtRYiuAg/index.html", + # ... +``` + This is effectively hosting a web app with ArDrive. Check out the ArDrive Price Calculator React App hosted as an [ArDrive Manifest][example-manifest-webpage]. ### Create New Drive and Upload Folder Pipeline Example From 163596ce41d426fd19d3fa959650ea2027980731 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 30 Nov 2021 10:22:45 -0600 Subject: [PATCH 25/30] docs(create-manifest): Change a word PE-477 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7406f94..1d214a2f 100644 --- a/README.md +++ b/README.md @@ -768,7 +768,7 @@ Example output: ### Uploading Manifests -[Arweave Path Manifests][arweave-manifests] are are special `.json` files that instruct Arweave Gateways to map file data associated with specific, disparate transaction IDs to customized, hosted paths relative to that of the manifest file itself. So if, for example, your manifest file had an arweave.net URL like: +[Arweave Path Manifests][arweave-manifests] are are special `.json` files that instruct Arweave Gateways to map file data associated with specific, unique transaction IDs to customized, hosted paths relative to that of the manifest file itself. So if, for example, your manifest file had an arweave.net URL like: ```shell https://arweave.net/{manifest tx id} From 40b213c46c15831ab3216b9d8d04555ef63d24e6 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 30 Nov 2021 10:30:51 -0600 Subject: [PATCH 26/30] docs(create-manifest): Add pipeline example for links output PE-477 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1d214a2f..273c928c 100644 --- a/README.md +++ b/README.md @@ -796,6 +796,12 @@ You can also create a manifest of a folder's file entities at a custom depth by ardrive create-manifest --max-depth 0 -f "867228d8-4413-4c0e-a499-e1decbf2ea38" -w "/path/to/wallet" ``` +Creating a `.json` file of your manifest links output can be accomplished here with some `jq` parsing and piping to a file: + +```shell +ardrive create-manifest -w /path/to/wallet -f "6c312b3e-4778-4a18-8243-f2b346f5e7cb" | jq '{links}' > links.json +``` + The manifest data transaction is tagged with a unique content-type, `application/x.arweave-manifest+json`, which tells the gateway to treat this file as a manifest. The manifest file itself is a `.json` file that holds the paths (the data transaction ids) to each file within the specified folder. When your folder is later changed by adding files or updating them with new revisions, the original manifest will NOT be updated on its own. A manifest is a permanent record of your files in their current state. From 3fa2d033cdcdc2b4b46ce37ec74fd74c9021974b Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 1 Dec 2021 14:44:47 -0600 Subject: [PATCH 27/30] chore(core version): Use alpha core build PE-743 --- .pnp.js | 10 +++++----- ...m-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip} | Bin 141375 -> 141761 bytes package.json | 2 +- yarn.lock | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) rename .yarn/cache/{ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip => ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip} (76%) diff --git a/.pnp.js b/.pnp.js index be47ebaf..e76496c7 100755 --- a/.pnp.js +++ b/.pnp.js @@ -48,7 +48,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.3-alpha-1"], + ["ardrive-core-js", "npm:1.0.5-alpha-0"], ["arweave", "npm:1.10.16"], ["axios", "npm:0.21.1"], ["chai", "npm:4.3.4"], @@ -1293,7 +1293,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.3-alpha-1"], + ["ardrive-core-js", "npm:1.0.5-alpha-0"], ["arweave", "npm:1.10.16"], ["axios", "npm:0.21.1"], ["chai", "npm:4.3.4"], @@ -1319,10 +1319,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["ardrive-core-js", [ - ["npm:1.0.3-alpha-1", { - "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip/node_modules/ardrive-core-js/", + ["npm:1.0.5-alpha-0", { + "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip/node_modules/ardrive-core-js/", "packageDependencies": [ - ["ardrive-core-js", "npm:1.0.3-alpha-1"], + ["ardrive-core-js", "npm:1.0.5-alpha-0"], ["arweave", "npm:1.10.16"], ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], diff --git a/.yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip b/.yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip similarity index 76% rename from .yarn/cache/ardrive-core-js-npm-1.0.3-alpha-1-ff9281f640-d9b8302cb4.zip rename to .yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip index dc8f8bcd9dbaca66c780a8cf7aa143a551d0cfe8..bbe20748c0e95e0da91c68dabdc3e0d74d4f86e7 100644 GIT binary patch delta 25341 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

    ag=QfCDJ4xSz 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 aFu>0&P*1P@LD)cAR)7Y>^biIe0{;iAC_fJX delta 25209 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 kc>4mmR9 Date: Wed, 1 Dec 2021 16:16:07 -0600 Subject: [PATCH 28/30] chore(core version): Use 1.0.5 production core PE-477 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d9851ba..6a090c9f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "types": "./lib/index.d.ts", "dependencies": { - "ardrive-core-js": "1.0.5-alpha-0", + "ardrive-core-js": "1.0.5", "arweave": "^1.10.16", "axios": "^0.21.1", "commander": "^8.2.0", From e3aec08fd47f73a4e1b03bec2682c164e5f0d262 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 1 Dec 2021 16:19:38 -0600 Subject: [PATCH 29/30] chore(core version): Commit cache files PE-477 --- .pnp.js | 10 +++++----- ...re-js-npm-1.0.5-6e82770d54-02490de5ea.zip} | Bin 141761 -> 141756 bytes yarn.lock | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) rename .yarn/cache/{ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip => ardrive-core-js-npm-1.0.5-6e82770d54-02490de5ea.zip} (94%) diff --git a/.pnp.js b/.pnp.js index e76496c7..8f61819a 100755 --- a/.pnp.js +++ b/.pnp.js @@ -48,7 +48,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.5-alpha-0"], + ["ardrive-core-js", "npm:1.0.5"], ["arweave", "npm:1.10.16"], ["axios", "npm:0.21.1"], ["chai", "npm:4.3.4"], @@ -1293,7 +1293,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/source-map-support", "npm:0.5.4"], ["@typescript-eslint/eslint-plugin", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], ["@typescript-eslint/parser", "virtual:6f50bb9424c73c7612c66dab5cf8914d8ec79550c84d8ca5e4888e80022682c708b4b5a1c510d282a03285cc9bb19002b477ae70d15882aa995ea1d5d6bf24ab#npm:4.20.0"], - ["ardrive-core-js", "npm:1.0.5-alpha-0"], + ["ardrive-core-js", "npm:1.0.5"], ["arweave", "npm:1.10.16"], ["axios", "npm:0.21.1"], ["chai", "npm:4.3.4"], @@ -1319,10 +1319,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["ardrive-core-js", [ - ["npm:1.0.5-alpha-0", { - "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip/node_modules/ardrive-core-js/", + ["npm:1.0.5", { + "packageLocation": "./.yarn/cache/ardrive-core-js-npm-1.0.5-6e82770d54-02490de5ea.zip/node_modules/ardrive-core-js/", "packageDependencies": [ - ["ardrive-core-js", "npm:1.0.5-alpha-0"], + ["ardrive-core-js", "npm:1.0.5"], ["arweave", "npm:1.10.16"], ["arweave-bundles", "npm:1.0.3"], ["arweave-mnemonic-keys", "npm:0.0.9"], diff --git a/.yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip b/.yarn/cache/ardrive-core-js-npm-1.0.5-6e82770d54-02490de5ea.zip similarity index 94% rename from .yarn/cache/ardrive-core-js-npm-1.0.5-alpha-0-eda2ffc549-6cb91d2870.zip rename to .yarn/cache/ardrive-core-js-npm-1.0.5-6e82770d54-02490de5ea.zip index bbe20748c0e95e0da91c68dabdc3e0d74d4f86e7..a3ae4a1045f3eed45a7662e9fa6c2fc333ccad8e 100644 GIT binary patch delta 2263 zcmY*Zc_5VeANGDR80Q#VWgIDuE96L0NtRu0Bu9-S6qObAl_N!pScIRe zVab+CD_0G*C<>Z9B1$x z9BN6C^3dHC^QD;`<*wM8eR|W^W3o|li^SDE`PWBGU52IPJvCLgR4MQ9kmb#bpDs$y z9&Lb~^_d0{?)wX;jQrgL$>98=wc(O+#7@IL?!#fxFH04Auet7#*K`gS*1Yu#r7r5^ zBH_#*!S)-m?(ceDhRii4EAE8_MO__IRQ zE;rJLsWM#*pC=iAu6`&vV4Hife4wmA!*7!C)nVfED-}1bu34=Bo`^~MgJbw*{qoSA zF>c(c_f$V%)Jx^N+T*N4Ni_yVI-#YLRWXlxbe@Zy*5~jl4u8bisP_e<954Jr#^OW?vV2-^fuQ~(Ho;w%^zf$V|8l1%1mujWGwXj2r1|SATuwK@`p5e_%U-8C z7o7`Adf4trB2!0i{N_w7@37QK83|2upXI8PZs967J-)2BXBu?&r{p>Hx6#`|E~a~! z-!#0eml9+U^OTr*K-$u*VecLHq_tM+(z&;Inxd<-_pER}rTN{>olcE4 z&Ybba1cR{^58_%UkA;zsLo6N4> zpHULE`QWx&b*I!&M0l2)1Bt|U~KFX*BL3JBhQ}pM2y<(e*ELi%JaSTOpT_8S&BZ?Un1-+tgRn7l^~g> zz9KaA(xKLCTJS!O@l`42iWt4;n@zZBVk2X>%A2@b~B4OVVLt-wkv19A)`CdTsM2PH_H3h+&^tSQ+=nq z7f#PvTs?Zbc4kMc_!c?DNBf;B5jBf!`B=6!r+(5kLrd z5WtfB`UTT~83Z5(wM2mw=nG^=io+;TpbZFTSjeC_hJ(GLKo{VHSqL>4BSCd3WE>WP zi3o7~^W2T}34sr#fhLRy$MCQ|1S7&aB2Wi<;cQqM9wMVSiWo|Hp2oK0P5$J{3&$l< zHTMvO*~11g;0;P**$N)$AdZT%ytaH40eFp8WtiV4R~NY2JoD%I@uhaQ9}Z68fFK-f$GG4mle^Zz z|R0aBgI?Y7X;D9Q+G{Ov<-Udegd0FqM#2FpJeKZ}M$7c*}_HWi}en|iT delta 2179 zcmY*ZdpMM7AD;KYtn-X!Mi`87J~n-A$oZ5SEZI*KqD8K?);c7S9fp#^MJ(G>twS2N zu-VC{bxx^4v{cI3<$U^bY%6`0e9w6IvX}R-_jf<{?|$y*eh!U2;tzVnld3V6k4%Jh zMJHR7KuDA%5cG1A)!;E7ER^fMt0A?=QPhB6Dp$~%a%hJ6KG)`1QJ#<4t~{+u4D_Bo zdDf+BaymJwTzuJF&smk@Br|+Qr2XZi50yQwjy23N*qy@b7U@6NzUh-}6kCT!-omW}DwGA>9*MFIcsJ-i`vDj@G z#MphfHFUY!z1r+>%xT{>ZkeSj>BV=0cJv&Y`H1%Z|E3RV`$P=9wwlRaxE+!#J7RxH z%Xd0YW+CczQ)?i9;e>DaaGiIYc*BThdqX@u^ZOE3)t?#qgMVC^54CQHchZQ9j|-Aq zYca1p?QcD3U*MeAST3+GRnW4kw$X`Z6yFG|ZJ!R1`$fXbSF*cnrI?#LB=PA$pFE|d z_6%h#bIH}pH}lN&;E|n*27bYJ#`lL0#2edLB#1hgRo;B8b!e<}{3~07!+)^OiLz`L zUDHCRRsN(GT&SJ*%u9Qy>);*h5n}nwNf5Q4`Wv-_HcwhC8NKNzGdT45fWSb5+wMz-j4zec{ikk!4K+Z?(*t3m(;oe~w@wfuHvz^}XLMYVZF$Hm^2`2Hr#H#yXmUPbx$qsIIv zL(7I%Qm3NaWEN+y@GQn}I0Z=ct(WV3SPWn1jt(yARNoL1f?4*y`>8AYXl&9F58i%A+ zR##0{&+ZE}e5V>gXbEiMGHZ%&+}ES)l9<2#hpAOsO!MxuV4a}OrqLZI_O>^~YRXyg z7{!kUr9P?DTr4hYStfT`zBC|?MutXa+Wfnp{h(oext;PBJVv9?Og_SU*bDaPBe+ zA$ku=i=~zkixO-OMojG^4$_;0j>a%#j|T|^Jvm~_oc(cy=d%)AeEAvn_A6}*!S6`! zE+V2-0w9PJ2m~gfrp_N5=Syt84Uk6yA%O1>?Zf~B8j``6*c12-8N`8$VK9IU%wT^Q zCJQUcfD9jpV`OQhviX%H-w($L zutpKNFjqwZyFJF$xvIbz9O#6rU!Ws!iG<0+HB}^`+$HS#68bPuVcIP$^x+dRAOV|% zNJlr`ZAb*NP*V*d<4^Hv=+7`|*hWM92%m3TY}Rx5fCxszf~o3A V-%}4oGzTN~zyV^=Ul@8C{ts8KN?QN` diff --git a/yarn.lock b/yarn.lock index abe7bc8a..443933b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -998,7 +998,7 @@ __metadata: "@types/source-map-support": ^0 "@typescript-eslint/eslint-plugin": ^4.18.0 "@typescript-eslint/parser": ^4.18.0 - ardrive-core-js: 1.0.5-alpha-0 + ardrive-core-js: 1.0.5 arweave: ^1.10.16 axios: ^0.21.1 chai: ^4.3.4 @@ -1024,9 +1024,9 @@ __metadata: languageName: unknown linkType: soft -"ardrive-core-js@npm:1.0.5-alpha-0": - version: 1.0.5-alpha-0 - resolution: "ardrive-core-js@npm:1.0.5-alpha-0" +"ardrive-core-js@npm:1.0.5": + version: 1.0.5 + resolution: "ardrive-core-js@npm:1.0.5" dependencies: arweave: ^1.10.16 arweave-bundles: ^1.0.3 @@ -1042,7 +1042,7 @@ __metadata: smartweave: ^0.4.45 utf8: ^3.0.0 uuid: ^8.3.2 - checksum: 6cb91d28707ccc1365f1eb11c6357bfd6c3fbe64e458a029f1b92ceca7a08152f88cad311b6101f5a35ff5f8e624e8b79f25a0f1dc9dc9c58c7cfa854d4bdcc8 + checksum: 02490de5ea3e806f1a793389b3dbf8dc89b6caa116870b0d32b8a0591eb3cd7c02dc16ff6c4c15c418b2d7734fcdd2379f20a640ac30292d4f6db629370b3941 languageName: node linkType: hard From 5ab465a4bdb884e1f89b1fad12dd681a8d9dacd5 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 1 Dec 2021 16:39:37 -0600 Subject: [PATCH 30/30] fix(imports): Remove duplicate command imports and alphabetize manifest PE-477 --- src/commands/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index b6202010..653a4fb3 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -2,6 +2,7 @@ import '../parameter_declarations'; import './base_reward'; import './create_drive'; import './create_folder'; +import './create_manifest'; import './create_tx'; import './drive_info'; import './file_info'; @@ -19,9 +20,6 @@ import './list_drive'; import './list_folder'; import './move_file'; import './move_folder'; -import './get_drive_key'; -import './get_file_key'; -import './create_manifest'; import './send_ar'; import './send_tx'; import './tx_status';