From 4173d5fe9b30e62a7c209b474f03917956c3ebb9 Mon Sep 17 00:00:00 2001 From: Vladan Date: Wed, 3 Jan 2024 16:15:38 +0100 Subject: [PATCH 1/5] feat: pod subscriptions --- package-lock.json | 1 + package.json | 1 + src/fdp-storage.ts | 6 +++-- src/pod/personal-storage.ts | 52 ++++++++++++++++++++++++++++++++++--- src/types.ts | 6 ++++- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f1eb2c1..a607c9b4 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" }, diff --git a/package.json b/package.json index 251ff745..3e4d2ea1 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,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" }, 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 95655690..4b13f756 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -1,3 +1,4 @@ +import { ec as EC } from 'elliptic' import { SharedPod, PodReceiveOptions, PodShareInfo, PodsList, Pod, PodsListPrepared } from './types' import { assertAccount } from '../account/utils' import { writeFeedData } from '../feed/api' @@ -15,21 +16,29 @@ import { podPreparedToPod, sharedPodPreparedToSharedPod, podListToJSON, + assertPodShareInfo, } 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, preparePrivateKey } from '../utils/wallet' import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' import { getNextEpoch } from '../feed/lookup/utils' +import { DataHub, ENS, ENS_DOMAIN, Subscription } from '@fairdatasociety/fdp-contracts-js' +import { decryptBytes } from '../utils/encryption' +import { namehash } from 'ethers/lib/utils' export const POD_TOPIC = 'Pods' 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 @@ -198,4 +207,41 @@ export class PersonalStorage { return sharedPodPreparedToSharedPod(pod) } + + async getSubscriptions(ens: string): Promise { + const subItems = await this.dataHub.getAllSubItemsForNameHash(namehash(`${ens}.${ENS_DOMAIN}`)) + + const subscriptions: Subscription[] = [] + + for (let i = 0; i < subItems.length; i++) { + const sub = await this.dataHub.getSubBy(subItems[i].subHash) + + subscriptions.push(sub) + } + + return subscriptions + } + + async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise { + const sub = await this.dataHub.getSubBy(subHash) + + const publicKeyHex = await this.ens.getPublicKeyByUsernameHash(sub.fdpSellerNameHash) + + const wallet = this.accountData.wallet! + + const ec = new EC('secp256k1') + + const privateKey = ec.keyFromPrivate(wallet.privateKey) + const publicKey = ec.keyFromPublic(publicKeyHex.substring(2), 'hex') + + const secret = privateKey.derive(publicKey.getPublic()).toString(16) + + const encryptedData = await this.accountData.connection.bee.downloadData(swarmLocation) + + const data = JSON.parse(bytesToString(decryptBytes(secret, encryptedData))) + + assertPodShareInfo(data) + + return data + } } 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 */ From 2ed73627906c79c34d3c2bc8af23263873858653 Mon Sep 17 00:00:00 2001 From: Vladan Date: Tue, 9 Jan 2024 15:36:52 +0100 Subject: [PATCH 2/5] fix: pod subscription --- src/pod/personal-storage.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/pod/personal-storage.ts b/src/pod/personal-storage.ts index 4b13f756..1ad8e06d 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -27,7 +27,7 @@ import { prepareEthAddress, preparePrivateKey } from '../utils/wallet' import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' import { getNextEpoch } from '../feed/lookup/utils' -import { DataHub, ENS, ENS_DOMAIN, Subscription } from '@fairdatasociety/fdp-contracts-js' +import { DataHub, ENS, ENS_DOMAIN, SubItem, Subscription } from '@fairdatasociety/fdp-contracts-js' import { decryptBytes } from '../utils/encryption' import { namehash } from 'ethers/lib/utils' @@ -208,18 +208,16 @@ export class PersonalStorage { return sharedPodPreparedToSharedPod(pod) } - async getSubscriptions(ens: string): Promise { - const subItems = await this.dataHub.getAllSubItemsForNameHash(namehash(`${ens}.${ENS_DOMAIN}`)) - - const subscriptions: Subscription[] = [] - - for (let i = 0; i < subItems.length; i++) { - const sub = await this.dataHub.getSubBy(subItems[i].subHash) + async getSubscriptions(address: string): Promise { + return this.dataHub.getUsersSubscriptions(address) + } - subscriptions.push(sub) - } + async getAllSubItems(address: string): Promise { + return this.dataHub.getAllSubItems(address) + } - return subscriptions + async getAllSubItemsForNameHash(name: string): Promise { + return this.dataHub.getAllSubItemsForNameHash(namehash(`${name}.${ENS_DOMAIN}`)) } async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise { @@ -236,9 +234,9 @@ export class PersonalStorage { const secret = privateKey.derive(publicKey.getPublic()).toString(16) - const encryptedData = await this.accountData.connection.bee.downloadData(swarmLocation) + const encryptedData = await this.accountData.connection.bee.downloadFile(swarmLocation.substring(2)) - const data = JSON.parse(bytesToString(decryptBytes(secret, encryptedData))) + const data = JSON.parse(bytesToString(decryptBytes(secret, encryptedData.data))) assertPodShareInfo(data) From 615aeff506b3fd327e6aafecba033e15fd9f0366 Mon Sep 17 00:00:00 2001 From: Vladan Date: Wed, 17 Jan 2024 15:00:50 +0100 Subject: [PATCH 3/5] fix: pod subscription --- src/pod/personal-storage.ts | 20 ++++++-------------- src/utils/encryption.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/pod/personal-storage.ts b/src/pod/personal-storage.ts index 1ad8e06d..486d0859 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -1,4 +1,3 @@ -import { ec as EC } from 'elliptic' import { SharedPod, PodReceiveOptions, PodShareInfo, PodsList, Pod, PodsListPrepared } from './types' import { assertAccount } from '../account/utils' import { writeFeedData } from '../feed/api' @@ -28,7 +27,7 @@ import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' import { getNextEpoch } from '../feed/lookup/utils' import { DataHub, ENS, ENS_DOMAIN, SubItem, Subscription } from '@fairdatasociety/fdp-contracts-js' -import { decryptBytes } from '../utils/encryption' +import { decryptWithBytes, deriveSecretFromKeys } from '../utils/encryption' import { namehash } from 'ethers/lib/utils' export const POD_TOPIC = 'Pods' @@ -220,23 +219,16 @@ export class PersonalStorage { return this.dataHub.getAllSubItemsForNameHash(namehash(`${name}.${ENS_DOMAIN}`)) } - async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise { + async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise { const sub = await this.dataHub.getSubBy(subHash) - const publicKeyHex = await this.ens.getPublicKeyByUsernameHash(sub.fdpSellerNameHash) + const publicKey = await this.ens.getPublicKeyByUsernameHash(sub.fdpSellerNameHash) - const wallet = this.accountData.wallet! - - const ec = new EC('secp256k1') - - const privateKey = ec.keyFromPrivate(wallet.privateKey) - const publicKey = ec.keyFromPublic(publicKeyHex.substring(2), 'hex') - - const secret = privateKey.derive(publicKey.getPublic()).toString(16) + const encryptedData = await this.accountData.connection.bee.downloadData(swarmLocation.substring(2)) - const encryptedData = await this.accountData.connection.bee.downloadFile(swarmLocation.substring(2)) + const secret = deriveSecretFromKeys(this.accountData.wallet!.privateKey, publicKey) - const data = JSON.parse(bytesToString(decryptBytes(secret, encryptedData.data))) + const data = JSON.parse(bytesToString(decryptWithBytes(secret, encryptedData))) assertPodShareInfo(data) 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)))) +} From c85255c4b1442f3042a64d5ace8cbd3f7a7d9eb1 Mon Sep 17 00:00:00 2001 From: Vladan Date: Thu, 18 Jan 2024 14:47:04 +0100 Subject: [PATCH 4/5] feat: additional datahub functions --- README.md | 18 +++++++++++ src/pod/personal-storage.ts | 59 ++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 687995cf..84d056e7 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) +``` + ## Documentation You can generate API docs locally with: diff --git a/src/pod/personal-storage.ts b/src/pod/personal-storage.ts index 486d0859..c702b838 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -26,9 +26,18 @@ import { prepareEthAddress, preparePrivateKey } from '../utils/wallet' import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' import { getNextEpoch } from '../feed/lookup/utils' -import { DataHub, ENS, ENS_DOMAIN, SubItem, Subscription } from '@fairdatasociety/fdp-contracts-js' +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 const POD_TOPIC = 'Pods' @@ -234,4 +243,52 @@ export class PersonalStorage { 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) + } } From 74bdc591dca5b24fafec84aa4266be59463c15b3 Mon Sep 17 00:00:00 2001 From: Vladan Date: Fri, 23 Feb 2024 13:41:56 +0100 Subject: [PATCH 5/5] test: test fix --- test/integration/node/pod/pods-limitation-check.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) } })