diff --git a/README.md b/README.md index 0c0ab353..0c8933b4 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,24 @@ const fdpCache = new FdpStorage('https://localhost:1633', batchId, { fdpCache.cache.object = JSON.parse(cache) ``` +There are available function for interacting with DataHub contract. For example to list all available subscriptions: + +```js +const subs = await fdp.personalStorage.getAllSubscriptions() +``` + +To get user's subscriptions: + +```js +const subItems = await fdp.personalStorage.getAllSubItems() +``` + +And to get pod information of a subItem: + +```js +const podShareInfo = await fdp.personalStorage.openSubscribedPod(subItems[0].subHash, subItems[0].unlockKeyLocation) +``` + ## Data migration Starting from the version `0.18.0`, pods and directories are stored in different format than the older versions. For all new accounts this doesn't have any impact. But to access pods and folders from existing accounts, migration is required. diff --git a/package-lock.json b/package-lock.json index bd37e573..5b6bbbd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ethersphere/bee-js": "^6.2.0", "@fairdatasociety/fdp-contracts-js": "^3.10.0", "crypto-js": "^4.2.0", + "elliptic": "^6.5.4", "ethers": "^5.5.2", "js-sha3": "^0.9.2", "pako": "^2.1.0" diff --git a/package.json b/package.json index dc248528..b73dde0b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@ethersphere/bee-js": "^6.2.0", "@fairdatasociety/fdp-contracts-js": "^3.10.0", "crypto-js": "^4.2.0", + "elliptic": "^6.5.4", "ethers": "^5.5.2", "js-sha3": "^0.9.2", "pako": "^2.1.0" diff --git a/src/fdp-storage.ts b/src/fdp-storage.ts index 63eedca7..31b490ce 100644 --- a/src/fdp-storage.ts +++ b/src/fdp-storage.ts @@ -5,7 +5,7 @@ import { Connection } from './connection/connection' import { Options } from './types' import { Directory } from './directory/directory' import { File } from './file/file' -import { ENS } from '@fairdatasociety/fdp-contracts-js' +import { ENS, DataHub } from '@fairdatasociety/fdp-contracts-js' import { CacheInfo, DEFAULT_CACHE_OPTIONS } from './cache/types' export class FdpStorage { @@ -15,6 +15,7 @@ export class FdpStorage { public readonly directory: Directory public readonly file: File public readonly ens: ENS + public readonly dataHub: DataHub public readonly cache: CacheInfo constructor(beeUrl: string, postageBatchId: BatchId, options?: Options) { @@ -24,8 +25,9 @@ export class FdpStorage { } this.connection = new Connection(new Bee(beeUrl), postageBatchId, this.cache, options) this.ens = new ENS(options?.ensOptions, null, options?.ensDomain) + this.dataHub = new DataHub(options?.dataHubOptions, null, options?.ensDomain) this.account = new AccountData(this.connection, this.ens) - this.personalStorage = new PersonalStorage(this.account) + this.personalStorage = new PersonalStorage(this.account, this.ens, this.dataHub) this.directory = new Directory(this.account) this.file = new File(this.account) } diff --git a/src/pod/personal-storage.ts b/src/pod/personal-storage.ts index f125e787..fd1990f7 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -11,6 +11,7 @@ import { getSharedPodInfo, podListToBytes, podListToJSON, + assertPodShareInfo, podPreparedToPod, podsListPreparedToPodsList, sharedPodPreparedToSharedPod, @@ -18,16 +19,32 @@ import { } from './utils' import { getExtendedPodsList } from './api' import { uploadBytes } from '../file/utils' -import { stringToBytes } from '../utils/bytes' +import { bytesToString, stringToBytes } from '../utils/bytes' import { Reference, Utils } from '@ethersphere/bee-js' -import { assertEncryptedReference, EncryptedReference } from '../utils/hex' +import { assertEncryptedReference, EncryptedReference, HexString } from '../utils/hex' import { prepareEthAddress } from '../utils/wallet' import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' import { getNextEpoch } from '../feed/lookup/utils' +import { + ActiveBid, + DataHub, + ENS, + ENS_DOMAIN, + SubItem, + Subscription, + SubscriptionRequest, +} from '@fairdatasociety/fdp-contracts-js' +import { decryptWithBytes, deriveSecretFromKeys } from '../utils/encryption' +import { namehash } from 'ethers/lib/utils' +import { BigNumber } from 'ethers' export class PersonalStorage { - constructor(private accountData: AccountData) {} + constructor( + private accountData: AccountData, + private ens: ENS, + private dataHub: DataHub, + ) {} /** * Gets the list of pods for the active account @@ -201,4 +218,80 @@ export class PersonalStorage { return sharedPodPreparedToSharedPod(pod) } + + async getSubscriptions(address: string): Promise { + return this.dataHub.getUsersSubscriptions(address) + } + + async getAllSubItems(address: string): Promise { + return this.dataHub.getAllSubItems(address) + } + + async getAllSubItemsForNameHash(name: string): Promise { + return this.dataHub.getAllSubItemsForNameHash(namehash(`${name}.${ENS_DOMAIN}`)) + } + + async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise { + const sub = await this.dataHub.getSubBy(subHash) + + const publicKey = await this.ens.getPublicKeyByUsernameHash(sub.fdpSellerNameHash) + + const encryptedData = await this.accountData.connection.bee.downloadData(swarmLocation.substring(2)) + + const secret = deriveSecretFromKeys(this.accountData.wallet!.privateKey, publicKey) + + const data = JSON.parse(bytesToString(decryptWithBytes(secret, encryptedData))) + + assertPodShareInfo(data) + + return data + } + + async getActiveBids(): Promise { + return this.dataHub.getActiveBids(this.accountData.wallet!.address) + } + + async getListedSubs(address: string): Promise { + return this.dataHub.getListedSubs(address) + } + + async getSubRequests(address: string): Promise { + return this.dataHub.getSubRequests(address) + } + + async bidSub(subHash: string, buyerUsername: string, value: BigNumber): Promise { + return this.dataHub.requestSubscription(subHash, buyerUsername, value) + } + + async createSubscription( + sellerUsername: string, + swarmLocation: string, + price: BigNumber, + categoryHash: string, + podAddress: string, + daysValid: number, + value?: BigNumber, + ): Promise { + return this.dataHub.createSubscription( + sellerUsername, + swarmLocation, + price, + categoryHash, + podAddress, + daysValid, + value, + ) + } + + async getAllSubscriptions(): Promise { + return this.dataHub.getSubs() + } + + async getSubscriptionsByCategory(categoryHash: string) { + const subs = await this.getAllSubscriptions() + + // TODO temporary until the category gets added to fdp-contracts + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return subs.filter(sub => (sub as any).category === categoryHash) + } } diff --git a/src/types.ts b/src/types.ts index 5cb25cc5..b59602e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { BeeRequestOptions } from '@ethersphere/bee-js' -import { EnsEnvironment } from '@fairdatasociety/fdp-contracts-js' +import { DataHubEnvironment, EnsEnvironment } from '@fairdatasociety/fdp-contracts-js' import { CacheOptions } from './cache/types' export { DirectoryItem, FileItem } from './content-items/types' @@ -42,6 +42,10 @@ export interface Options { * FDP-contracts options */ ensOptions?: EnsEnvironment + /** + * FDP-contracts options + */ + dataHubOptions?: DataHubEnvironment /** * ENS domain for usernames */ diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index 00128273..be1701b2 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -1,5 +1,7 @@ import CryptoJS from 'crypto-js' import { PrivateKeyBytes, Utils } from '@ethersphere/bee-js' +import { ec as EC } from 'elliptic' +import { utils } from 'ethers' import { bytesToHex } from './hex' import { bytesToString, bytesToWordArray, wordArrayToBytes } from './bytes' import { isArrayBufferView, isString } from './type' @@ -18,7 +20,10 @@ export declare type PodPasswordBytes = Utils.Bytes<32> * @param password string to decrypt bytes * @param data WordsArray to be decrypted */ -export function decrypt(password: string, data: CryptoJS.lib.WordArray): CryptoJS.lib.WordArray { +export function decrypt( + password: string | CryptoJS.lib.WordArray, + data: CryptoJS.lib.WordArray, +): CryptoJS.lib.WordArray { const wordSize = 4 const key = CryptoJS.SHA256(password) const iv = CryptoJS.lib.WordArray.create(data.words.slice(0, IV_LENGTH), IV_LENGTH) @@ -78,6 +83,13 @@ export function decryptBytes(password: string, data: Uint8Array): Uint8Array { return wordArrayToBytes(decrypt(password, bytesToWordArray(data))) } +/** + * Decrypt bytes with bytes password + */ +export function decryptWithBytes(password: Uint8Array, data: Uint8Array): Uint8Array { + return wordArrayToBytes(decrypt(bytesToWordArray(password), bytesToWordArray(data))) +} + /** * Decrypt data and converts it from JSON string to object * @@ -97,3 +109,20 @@ export function decryptJson(password: string | Uint8Array, data: Uint8Array): un return jsonParse(bytesToString(decryptBytes(passwordString, data)), 'decrypted json') } + +/** + * Derives shared secret using private and public keys from different pairs + * @param privateKey Private key as a hex string + * @param publicKey Public key as a hex string + * @returns secret + */ +export function deriveSecretFromKeys(privateKey: string, publicKey: string): Uint8Array { + const ec = new EC('secp256k1') + + const privateKeyPair = ec.keyFromPrivate(utils.arrayify(privateKey), 'bytes') + const publicKeyPair = ec.keyFromPublic(publicKey.substring(2), 'hex') + + const derivedHex = '0x' + privateKeyPair.derive(publicKeyPair.getPublic()).toString(16) + + return wordArrayToBytes(CryptoJS.SHA256(bytesToWordArray(utils.arrayify(derivedHex)))) +} diff --git a/test/integration/node/pod/pods-limitation-check.spec.ts b/test/integration/node/pod/pods-limitation-check.spec.ts index 62d6d584..3bea30a3 100644 --- a/test/integration/node/pod/pods-limitation-check.spec.ts +++ b/test/integration/node/pod/pods-limitation-check.spec.ts @@ -1,4 +1,4 @@ -import { createFdp, generateRandomHexString, generateUser } from '../../../utils' +import { createFdp, generateRandomHexString, generateUser, sleep } from '../../../utils' import { MAX_POD_NAME_LENGTH } from '../../../../src' import { HIGHEST_LEVEL } from '../../../../src/feed/lookup/epoch' @@ -11,5 +11,6 @@ it('Pods limitation check', async () => { for (let i = 0; i < HIGHEST_LEVEL; i++) { const longPodName = generateRandomHexString(MAX_POD_NAME_LENGTH) await fdp.personalStorage.create(longPodName) + await sleep(100) } })