Skip to content

Commit

Permalink
feat: pod subscriptions (#295)
Browse files Browse the repository at this point in the history
* feat: pod subscriptions

* fix: pod subscription

* fix: pod subscription

* feat: additional datahub functions

* test: test fix
  • Loading branch information
tomicvladan authored Feb 23, 2024
1 parent fffe4af commit 0df06d8
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 8 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions src/fdp-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down
99 changes: 96 additions & 3 deletions src/pod/personal-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,40 @@ import {
getSharedPodInfo,
podListToBytes,
podListToJSON,
assertPodShareInfo,
podPreparedToPod,
podsListPreparedToPodsList,
sharedPodPreparedToSharedPod,
uploadPodDataV2,
} 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
Expand Down Expand Up @@ -201,4 +218,80 @@ export class PersonalStorage {

return sharedPodPreparedToSharedPod(pod)
}

async getSubscriptions(address: string): Promise<Subscription[]> {
return this.dataHub.getUsersSubscriptions(address)
}

async getAllSubItems(address: string): Promise<SubItem[]> {
return this.dataHub.getAllSubItems(address)
}

async getAllSubItemsForNameHash(name: string): Promise<SubItem[]> {
return this.dataHub.getAllSubItemsForNameHash(namehash(`${name}.${ENS_DOMAIN}`))
}

async openSubscribedPod(subHash: HexString, swarmLocation: HexString): Promise<PodShareInfo> {
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<ActiveBid[]> {
return this.dataHub.getActiveBids(this.accountData.wallet!.address)
}

async getListedSubs(address: string): Promise<string[]> {
return this.dataHub.getListedSubs(address)
}

async getSubRequests(address: string): Promise<SubscriptionRequest[]> {
return this.dataHub.getSubRequests(address)
}

async bidSub(subHash: string, buyerUsername: string, value: BigNumber): Promise<void> {
return this.dataHub.requestSubscription(subHash, buyerUsername, value)
}

async createSubscription(
sellerUsername: string,
swarmLocation: string,
price: BigNumber,
categoryHash: string,
podAddress: string,
daysValid: number,
value?: BigNumber,
): Promise<void> {
return this.dataHub.createSubscription(
sellerUsername,
swarmLocation,
price,
categoryHash,
podAddress,
daysValid,
value,
)
}

async getAllSubscriptions(): Promise<Subscription[]> {
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)
}
}
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -42,6 +42,10 @@ export interface Options {
* FDP-contracts options
*/
ensOptions?: EnsEnvironment
/**
* FDP-contracts options
*/
dataHubOptions?: DataHubEnvironment
/**
* ENS domain for usernames
*/
Expand Down
31 changes: 30 additions & 1 deletion src/utils/encryption.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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
*
Expand All @@ -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))))
}
3 changes: 2 additions & 1 deletion test/integration/node/pod/pods-limitation-check.spec.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
}
})

0 comments on commit 0df06d8

Please sign in to comment.