From 26bbf5444e1582860ef7cd6e2a30015d93e5e465 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Thu, 20 Jul 2023 10:50:32 +0200 Subject: [PATCH 01/13] feat: implement ACID batch transactions --- src/__tests__/batch.test.ts | 13 +- src/api/akord-api.ts | 4 +- src/api/api.ts | 2 +- src/core/batch.ts | 268 ++++++++++++++++++++++++++---------- src/core/membership.ts | 4 +- src/core/node.ts | 18 ++- src/core/service.ts | 41 +++--- src/core/stack.ts | 4 +- 8 files changed, 248 insertions(+), 106 deletions(-) diff --git a/src/__tests__/batch.test.ts b/src/__tests__/batch.test.ts index ce8513c4..24fd4714 100644 --- a/src/__tests__/batch.test.ts +++ b/src/__tests__/batch.test.ts @@ -15,6 +15,7 @@ describe("Testing batch actions", () => { let noteId: string; let viewerId: string; let contributorId: string; + let stackIds: string[]; beforeEach(async () => { akord = await initInstance(email, password); @@ -37,7 +38,7 @@ describe("Testing batch actions", () => { it("should revoke all items in a batch", async () => { await akord.batch.revoke([ { id: folderId, type: "Folder" }, - { id: noteId, type: "Note" }, + { id: noteId, type: "Stack" }, ]) const folder = await akord.folder.get(folderId); @@ -50,7 +51,7 @@ describe("Testing batch actions", () => { it("should restore all items in a batch", async () => { await akord.batch.restore([ { id: folderId, type: "Folder" }, - { id: noteId, type: "Note" }, + { id: noteId, type: "Stack" }, ]) const folder = await akord.folder.get(folderId); @@ -72,8 +73,10 @@ describe("Testing batch actions", () => { items.push({ file, name }); } - const response = (await akord.batch.stackCreate(vaultId, items)).data; + const { data: response, errors } = await akord.batch.stackCreate(vaultId, items); + expect(errors.length).toEqual(0); + stackIds = response.map((item) => item.stackId); for (let index in items) { const stack = await akord.stack.get(response[index].stackId); expect(stack.status).toEqual("ACTIVE"); @@ -82,6 +85,10 @@ describe("Testing batch actions", () => { expect(stack.versions[0].name).toEqual(firstFileName); } }); + + it("should revoke the previously uploaded batch", async () => { + await akord.batch.revoke(stackIds.map(stackId => ({ id: stackId, type: "Stack" }))); + }); }); describe("Batch membership actions", () => { diff --git a/src/api/akord-api.ts b/src/api/akord-api.ts index ce29af68..25b0f1e5 100644 --- a/src/api/akord-api.ts +++ b/src/api/akord-api.ts @@ -44,10 +44,10 @@ export default class AkordApi extends Api { return resources; }; - public async postContractTransaction(contractId: string, input: ContractInput, tags: Tags, metadata?: any): Promise<{ id: string, object: T }> { + public async postContractTransaction(vaultId: string, input: ContractInput, tags: Tags, metadata?: any): Promise<{ id: string, object: T }> { const { id, object } = await new ApiClient() .env(this.config) - .vaultId(contractId) + .vaultId(vaultId) .metadata(metadata) .input(input) .tags(tags) diff --git a/src/api/api.ts b/src/api/api.ts index 9ac75c55..ec2053d9 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -13,7 +13,7 @@ abstract class Api { constructor() { } - abstract postContractTransaction(contractId: string, input: ContractInput, tags: Tags, metadata?: any): Promise<{ id: string, object: T }> + abstract postContractTransaction(vaultId: string, input: ContractInput, tags: Tags, metadata?: any): Promise<{ id: string, object: T }> abstract initContractId(tags: Tags, state?: any): Promise diff --git a/src/core/batch.ts b/src/core/batch.ts index 68e6facb..b1bc3cf3 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -1,13 +1,17 @@ -import { Service, ServiceFactory } from "../core"; +import { Service } from "../core"; import { v4 as uuidv4 } from "uuid"; import { MembershipCreateOptions, MembershipService, activeStatus } from "./membership"; import { StackCreateOptions, StackService } from "./stack"; -import { NodeService } from "./node"; -import { Node, NodeType, Stack } from "../types/node"; +import { Node, NodeLike, NodeType, Stack } from "../types/node"; import { FileLike } from "../types/file"; import { BatchMembershipInviteResponse, BatchStackCreateResponse } from "../types/batch-response"; -import { RoleType } from "../types/membership"; +import { Membership, RoleType } from "../types/membership"; import { Hooks } from "./file"; +import { actionRefs, functions, objectType, protocolTags } from "../constants"; +import { ContractInput, Tag, Tags } from "../types/contract"; +import { ObjectType } from "../types/object"; +import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; +import { NodeService } from "./node"; function* chunks(arr: T[], n: number): Generator { for (let i = 0; i < arr.length; i += n) { @@ -23,75 +27,72 @@ class BatchService extends Service { * @param {{id:string,type:NoteType}[]} items * @returns Promise with corresponding transaction ids */ - public async revoke(items: { id: string, type: NodeType }[]): Promise<{ transactionId: string }[]> { - this.setGroupRef(items); - const transactionIds = [] as { transactionId: string }[]; - await Promise.all(items.map(async (item) => { - const service = new ServiceFactory(this.wallet, this.api, item.type).serviceInstance() as NodeService; - service.setGroupRef(this.groupRef); - transactionIds.push(await service.revoke(item.id)); - })); - return transactionIds; + public async revoke(items: { id: string, type: NodeType }[]) + : Promise<{ transactionId: string, object: T }[]> { + return this.batchUpdate(items.map((item) => ({ + ...item, + input: { function: functions.NODE_REVOKE }, + actionRef: item.type.toUpperCase() + "_REVOKE" + }))); } /** * @param {{id:string,type:NoteType}[]} items * @returns Promise with corresponding transaction ids */ - public async restore(items: { id: string, type: NodeType }[]): Promise<{ transactionId: string }[]> { - this.setGroupRef(items); - const transactionIds = [] as { transactionId: string }[]; - await Promise.all(items.map(async (item) => { - const service = new ServiceFactory(this.wallet, this.api, item.type).serviceInstance() as NodeService; - service.setGroupRef(this.groupRef); - transactionIds.push(await service.restore(item.id)); - })); - return transactionIds; + public async restore(items: { id: string, type: NodeType }[]) + : Promise<{ transactionId: string, object: T }[]> { + return this.batchUpdate(items.map((item) => ({ + ...item, + input: { function: functions.NODE_RESTORE }, + actionRef: item.type.toUpperCase() + "_RESTORE" + }))); } /** * @param {{id:string,type:NodeType}[]} items * @returns Promise with corresponding transaction ids */ - public async delete(items: { id: string, type: NodeType }[]): Promise<{ transactionId: string }[]> { - this.setGroupRef(items); - const transactionIds = [] as { transactionId: string }[]; - await Promise.all(items.map(async (item) => { - const service = new ServiceFactory(this.wallet, this.api, item.type).serviceInstance() as NodeService; - service.setGroupRef(this.groupRef); - transactionIds.push(await service.delete(item.id)); - })); - return transactionIds; + public async delete(items: { id: string, type: NodeType }[]) + : Promise<{ transactionId: string, object: T }[]> { + return this.batchUpdate(items.map((item) => ({ + ...item, + input: { function: functions.NODE_DELETE }, + actionRef: item.type.toUpperCase() + "_DELETE" + }))); } /** * @param {{id:string,type:NodeType}[]} items * @returns Promise with corresponding transaction ids */ - public async move(items: { id: string, type: NodeType }[], parentId?: string): Promise<{ transactionId: string }[]> { - this.setGroupRef(items); - const transactionIds = [] as { transactionId: string }[]; - await Promise.all(items.map(async (item) => { - const service = new ServiceFactory(this.wallet, this.api, item.type).serviceInstance() as NodeService; - service.setGroupRef(this.groupRef); - transactionIds.push(await service.move(item.id, parentId)); - })); - return transactionIds; + public async move(items: { id: string, type: NodeType }[], parentId?: string) + : Promise<{ transactionId: string, object: T }[]> { + return this.batchUpdate(items.map((item) => ({ + ...item, + input: { + function: functions.NODE_MOVE, + parentId: parentId + }, + actionRef: item.type.toUpperCase() + "_MOVE" + }))); } /** * @param {{id:string,role:RoleType}[]} items * @returns Promise with corresponding transaction ids */ - public async membershipChangeRole(items: { id: string, role: RoleType }[]): Promise<{ transactionId: string }[]> { - this.setGroupRef(items); - const response = [] as { transactionId: string }[]; - await Promise.all(items.map(async (item) => { - const service = new MembershipService(this.wallet, this.api); - service.setGroupRef(this.groupRef); - response.push(await service.changeRole(item.id, item.role)); - })); - return response; + public async membershipChangeRole(items: { id: string, role: RoleType }[]) + : Promise<{ transactionId: string, object: Membership }[]> { + return this.batchUpdate(items.map((item) => ({ + id: item.id, + type: objectType.MEMBERSHIP, + input: { + function: functions.MEMBERSHIP_CHANGE_ROLE, + role: item.role + }, + actionRef: actionRefs.MEMBERSHIP_CHANGE_ROLE + }))); } /** @@ -116,8 +117,8 @@ class BatchService extends Service { options.processingCountHook(processedStacksCount); } - const data = [] as { stackId: string, transactionId: string, object: Stack }[]; - const errors = [] as { name: string, message: string, error: Error }[]; + let data = [] as BatchStackCreateResponse["data"]; + const errors = [] as BatchStackCreateResponse["errors"]; if (options.progressHook) { const onProgress = options.progressHook @@ -130,30 +131,118 @@ class BatchService extends Service { options.progressHook = stackProgressHook; } + const vault = await this.api.getVault(vaultId); + this.setVault(vault); + this.setVaultId(vaultId); + this.setIsPublic(vault.public); + await this.setMembershipKeys(vault); + + const stackCreateOptions = { + ...options, + cacheOnly: this.vault.cacheOnly + } + for (const chunk of [...chunks(items, BatchService.BATCH_CHUNK_SIZE)]) { + const transactions = [] as { + vaultId: string, + input: ContractInput, + tags: Tags, + item: { file: FileLike, name: string, options?: StackCreateOptions } + }[]; + + // upload file data & metadata await Promise.all(chunk.map(async (item) => { - try { - const service = new StackService(this.wallet, this.api); - service.setGroupRef(this.groupRef); - - const stackCreateOptions = { - ...options, - ...(item.options || {}) - } - const stackResponse = await service.create(vaultId, item.file, item.name, stackCreateOptions); - if (options.cancelHook?.signal.aborted) { - return { data, errors, cancelled: items.length - processedStacksCount }; - } - data.push(stackResponse); - processedStacksCount += 1; - options.processingCountHook(processedStacksCount); - if (options.onStackCreated) { - await options.onStackCreated(stackResponse.object); - } - } catch (error) { - errors.push({ name: item.name, message: error.toString(), error }) + const service = new StackService(this.wallet, this.api); + service.setVault(vault); + service.setVaultId(vaultId); + service.setIsPublic(vault.public); + await service.setMembershipKeys(vault); + service.setVaultContextForFile(); + service.setActionRef(actionRefs.STACK_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setGroupRef(this.groupRef); + + const nodeId = uuidv4(); + service.setObjectId(nodeId); + + const createOptions = { + ...stackCreateOptions, + ...(item.options || {}) + } + service.setAkordTags((service.isPublic ? [item.name] : []).concat(createOptions.tags)); + service.arweaveTags = await service.getTxTags(); + service.arweaveTags.push(new Tag( + protocolTags.PARENT_ID, + createOptions.parentId ? createOptions.parentId : "root" + )); + + const state = { + name: await service.processWriteString(item.name ? item.name : item.file.name), + versions: [await service.uploadNewFileVersion(item.file, createOptions)], + tags: service.tags }; - })) + const id = await service.uploadState(state); + const input = { + function: service.function, + data: id, + parentId: createOptions.parentId + } + // queue the stack transaction for posting + transactions.push({ + vaultId: service.vaultId, + input: input, + tags: service.arweaveTags, + item + }); + } + )); + + let currentTx: { + vaultId: string, + input: ContractInput, + tags: Tags, + item: { file: FileLike, name: string, options?: StackCreateOptions } + }; + while (processedStacksCount < items.length) { + if (options.cancelHook?.signal.aborted) { + return { data, errors, cancelled: items.length - processedStacksCount }; + } + if (transactions.length === 0) { + // wait for a while if the queue is empty before checking again + await new Promise((resolve) => setTimeout(resolve, 100)); + } else { + try { + currentTx = transactions.shift(); + // process the dequeued stack transaction + const { id, object } = await this.api.postContractTransaction( + currentTx.vaultId, + currentTx.input, + currentTx.tags + ); + processedStacksCount += 1; + if (options.processingCountHook) { + options.processingCountHook(processedStacksCount); + } + if (options.onStackCreated) { + await options.onStackCreated(object); + } + const stack = new Stack(object, this.keys); + if (!this.isPublic) { + try { + await stack.decrypt(); + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + data.push({ transactionId: id, object: stack, stackId: object.id }); + if (options.cancelHook?.signal.aborted) { + return { data, errors, cancelled: items.length - processedStacksCount }; + } + } catch (error) { + errors.push({ name: currentTx.item.name, message: error.toString(), error }); + }; + } + } if (options.cancelHook?.signal.aborted) { return { data, errors, cancelled: items.length - processedStacksCount }; } @@ -197,6 +286,41 @@ class BatchService extends Service { return { data: data, errors: errors }; } + private async batchUpdate(items: { id: string, type: ObjectType, input: ContractInput, actionRef: string }[]) + : Promise<{ transactionId: string, object: T }[]> { + this.setGroupRef(items); + const result = [] as { transactionId: string, object: T }[]; + for (const [itemIndex, item] of items.entries()) { + const node = item.type === objectType.MEMBERSHIP + ? await this.api.getMembership(item.id) + : await this.api.getNode(item.id, item.type); + + const service = item.type === objectType.MEMBERSHIP + ? new MembershipService(this.wallet, this.api) + : new NodeService(this.wallet, this.api); + if (itemIndex === 0 || this.vaultId !== node.vaultId) { + this.setVaultId(node.vaultId); + this.setIsPublic(node.__public__); + await this.setMembershipKeys(node); + } + service.setVaultId(this.vaultId); + service.setIsPublic(this.isPublic); + await service.setMembershipKeys(node); + service.setFunction(item.input.function); + service.setActionRef(item.actionRef); + service.setObject(node); + service.setObjectId(item.id); + service.setObjectType(item.type); + service.arweaveTags = await service.getTxTags(); + const { id, object } = await this.api.postContractTransaction(this.vaultId, item.input, service.arweaveTags); + const processedObject = item.type === objectType.MEMBERSHIP + ? await (service).processMembership(object as Membership, !this.isPublic, this.keys) + : await (>service).processNode(object as any, !this.isPublic, this.keys) as any; + result.push({ transactionId: id, object: processedObject }); + } + return result; + } + public setGroupRef(items: any) { this.groupRef = items && items.length > 1 ? uuidv4() : null; } diff --git a/src/core/membership.ts b/src/core/membership.ts index 84b7f773..9361b586 100644 --- a/src/core/membership.ts +++ b/src/core/membership.ts @@ -417,12 +417,12 @@ class MembershipService extends Service { this.setObjectType(this.objectType); } - protected async getTxTags(): Promise { + async getTxTags(): Promise { const tags = await super.getTxTags(); return tags.concat(new Tag(protocolTags.MEMBERSHIP_ID, this.objectId)); } - protected async processMembership(object: Membership, shouldDecrypt: boolean, keys?: EncryptedKeys[]): Promise { + async processMembership(object: Membership, shouldDecrypt: boolean, keys?: EncryptedKeys[]): Promise { const membership = new Membership(object, keys); if (shouldDecrypt) { try { diff --git a/src/core/node.ts b/src/core/node.ts index a73ce0ea..4875a954 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -1,12 +1,13 @@ import { Service } from './service'; import { functions, protocolTags, status } from "../constants"; -import { NodeLike, NodeType } from '../types/node'; +import { Folder, Memo, NodeLike, NodeType, Stack } from '../types/node'; import { EncryptedKeys } from '@akord/crypto'; import { GetOptions, ListOptions } from '../types/query-options'; import { ContractInput, Tag, Tags } from '../types/contract'; import { Paginated } from '../types/paginated'; import { v4 as uuidv4 } from "uuid"; import { IncorrectEncryptionKey } from '../errors/incorrect-encryption-key'; +import { BadRequest } from '../errors/bad-request'; class NodeService extends Service { objectType: NodeType; @@ -207,12 +208,12 @@ class NodeService extends Service { this.setObjectType(type); } - protected async getTxTags(): Promise { + async getTxTags(): Promise { const tags = await super.getTxTags(); return tags.concat(new Tag(protocolTags.NODE_ID, this.objectId)); } - protected async processNode(object: NodeLike, shouldDecrypt: boolean, keys?: EncryptedKeys[]): Promise { + async processNode(object: NodeLike, shouldDecrypt: boolean, keys?: EncryptedKeys[]): Promise { const node = this.nodeInstance(object, keys); if (shouldDecrypt) { try { @@ -227,7 +228,16 @@ class NodeService extends Service { protected NodeType: new (arg0: any, arg1: EncryptedKeys[]) => NodeLike private nodeInstance(nodeProto: any, keys: Array): NodeLike { - return new this.NodeType(nodeProto, keys); + // TODO: use a generic NodeLike constructor + if (this.objectType === "Folder") { + return new Folder(nodeProto, keys); + } else if (this.objectType === "Stack") { + return new Stack(nodeProto, keys); + } else if (this.objectType === "Memo") { + return new Memo(nodeProto, keys); + } else { + throw new BadRequest("Given type is not supported: " + this.objectType); + } } } diff --git a/src/core/service.ts b/src/core/service.ts index 2f7fe560..a2af1d2e 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -82,6 +82,22 @@ class Service { this.groupRef = groupRef; } + setActionRef(actionRef: string) { + this.actionRef = actionRef; + } + + setObjectType(objectType: ObjectType) { + this.objectType = objectType; + } + + setFunction(functionName: functions) { + this.function = functionName; + } + + setObject(object: NodeLike | Membership | Vault) { + this.object = object; + } + setIsPublic(isPublic: boolean) { this.isPublic = isPublic; } @@ -115,7 +131,7 @@ class Service { } } - protected async setVaultContext(vaultId: string) { + async setVaultContext(vaultId: string) { const vault = await this.api.getVault(vaultId); this.setVault(vault); this.setVaultId(vaultId); @@ -123,7 +139,7 @@ class Service { await this.setMembershipKeys(vault); } - protected async setMembershipKeys(object: Object) { + async setMembershipKeys(object: Object) { if (!this.isPublic) { const keys = object.__keys__.map(((keyPair: any) => { return { @@ -146,23 +162,8 @@ class Service { } } - protected setObjectType(objectType: ObjectType) { - this.objectType = objectType; - } - - protected setFunction(functionName: functions) { - this.function = functionName; - } - - protected setActionRef(actionRef: string) { - this.actionRef = actionRef; - } - - protected setObject(object: NodeLike | Membership | Vault) { - this.object = object; - } - protected async getProfileDetails(): Promise { + return {}; const user = await this.api.getUser(); if (user) { const profileEncrypter = new Encrypter(this.wallet, null, null); @@ -229,7 +230,7 @@ class Service { }; } - protected async processWriteString(data: string): Promise { + async processWriteString(data: string): Promise { if (this.isPublic) return data; let encryptedPayload: string; try { @@ -344,7 +345,7 @@ class Service { return newState; } - protected async getTxTags(): Promise { + async getTxTags(): Promise { const tags = [ new Tag(protocolTags.FUNCTION_NAME, this.function), new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), diff --git a/src/core/stack.ts b/src/core/stack.ts index 16ce17b8..2b49aff2 100644 --- a/src/core/stack.ts +++ b/src/core/stack.ts @@ -117,7 +117,7 @@ class StackService extends NodeService { return stack.getUri(type, index); } - private async uploadNewFileVersion(file: FileLike, options: FileUploadOptions): Promise { + public async uploadNewFileVersion(file: FileLike, options: FileUploadOptions): Promise { const { resourceTx, resourceUrl, @@ -138,7 +138,7 @@ class StackService extends NodeService { return version; } - protected setVaultContextForFile(): void { + setVaultContextForFile(): void { this.fileService.setKeys(this.keys); this.fileService.setRawDataEncryptionPublicKey(this.dataEncrypter.publicKey); this.fileService.setVaultId(this.vaultId); From dee89e47140afc656dcc5fdd49571246b342dfb5 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Tue, 25 Jul 2023 16:29:01 +0200 Subject: [PATCH 02/13] fix: profile details --- src/core/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/service.ts b/src/core/service.ts index a2af1d2e..8f8ef8a3 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -163,7 +163,6 @@ class Service { } protected async getProfileDetails(): Promise { - return {}; const user = await this.api.getUser(); if (user) { const profileEncrypter = new Encrypter(this.wallet, null, null); From b893d878664fdaf8907df958fc8407f5a300d20c Mon Sep 17 00:00:00 2001 From: Weronika K Date: Tue, 25 Jul 2023 17:05:01 +0200 Subject: [PATCH 03/13] feat: ACID batch membership invite --- src/core/batch.ts | 89 +++++++++++++++++++++++++++++++++---- src/core/membership.ts | 2 +- src/types/batch-response.ts | 3 +- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/core/batch.ts b/src/core/batch.ts index b1bc3cf3..a2242e83 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -12,6 +12,7 @@ import { ContractInput, Tag, Tags } from "../types/contract"; import { ObjectType } from "../types/object"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; import { NodeService } from "./node"; +import { BadRequest } from "../errors/bad-request"; function* chunks(arr: T[], n: number): Generator { for (let i = 0; i < arr.length; i += n) { @@ -256,33 +257,103 @@ class BatchService extends Service { * @param {MembershipCreateOptions} [options] invitation email message, etc. * @returns Promise with new membership ids & their corresponding transaction ids */ - public async membershipInvite(vaultId: string, items: { email: string, role: RoleType }[], options: MembershipCreateOptions = {}): Promise { + public async membershipInvite(vaultId: string, items: { email: string, role: RoleType }[], options: MembershipCreateOptions = {}) + : Promise { this.setGroupRef(items); const members = await this.api.getMembers(vaultId); - const data = [] as { membershipId: string, transactionId: string }[]; + const data = [] as BatchMembershipInviteResponse["data"]; const errors = []; - await Promise.all(items.map(async (item) => { + const transactions = [] as { + input: ContractInput, + tags: Tags, + item: { email: string, role: RoleType } + }[]; + + const vault = await this.api.getVault(vaultId); + this.setVault(vault); + this.setVaultId(vaultId); + this.setIsPublic(vault.public); + await this.setMembershipKeys(vault); + + // upload metadata + await Promise.all(items.map(async (item: { email: string, role: RoleType }) => { const email = item.email.toLowerCase(); const role = item.role; const member = members.find(item => item.email?.toLowerCase() === email); if (member && activeStatus.includes(member.status)) { - errors.push({ email: email, message: "Membership already exists for this user." }); + const message = "Membership already exists for this user."; + errors.push({ email: email, message, error: new BadRequest(message) }); } else { const userHasAccount = await this.api.existsUser(email); const service = new MembershipService(this.wallet, this.api); service.setGroupRef(this.groupRef); if (userHasAccount) { - data.push(await service.invite(vaultId, email, role, options)); + service.setVault(vault); + service.setVaultId(vaultId); + service.setIsPublic(vault.public); + await service.setMembershipKeys(this.vault); + service.setActionRef(actionRefs.MEMBERSHIP_INVITE); + service.setFunction(functions.MEMBERSHIP_INVITE); + const membershipId = uuidv4(); + service.setObjectId(membershipId); + + const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(email); + const state = { + keys: await service.prepareMemberKeys(publicKey), + encPublicSigningKey: await service.processWriteString(publicSigningKey) + }; + + service.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] + .concat(await service.getTxTags()); + + const dataTxId = await service.uploadState(state); + + const input = { + function: service.function, + address, + role, + data: dataTxId + } + transactions.push({ + input: input, + tags: service.arweaveTags, + item: item + }); } else { - data.push({ - ...(await service.inviteNewUser(vaultId, email, role, options)), - transactionId: null - }) + try { + const { id } = await this.api.inviteNewUser(vaultId, email, role, options.message); + data.push({ + membershipId: id, + transactionId: null + }) + } catch (error: any) { + errors.push({ + email: email, + error: error, + message: error.message, + item: item + }) + } } } } )); + + for (let tx of transactions) { + try { + const { id, object } = await this.api.postContractTransaction(vaultId, tx.input, tx.tags); + const membership = await new MembershipService(this.wallet, this.api).processMembership(object as Membership, !this.isPublic, this.keys); + data.push({ membershipId: membership.id, transactionId: id, object: membership }); + } catch (error: any) { + errors.push({ + email: tx.item.email, + error: error, + message: error.message, + item: tx.item + }) + } + } return { data: data, errors: errors }; } diff --git a/src/core/membership.ts b/src/core/membership.ts index 9361b586..5a4fd6b5 100644 --- a/src/core/membership.ts +++ b/src/core/membership.ts @@ -434,7 +434,7 @@ class MembershipService extends Service { return membership; } - private async prepareMemberKeys(publicKey: string): Promise { + async prepareMemberKeys(publicKey: string): Promise { if (!this.isPublic) { const keysEncrypter = new Encrypter(this.wallet, this.dataEncrypter.keys, base64ToArray(publicKey)); try { diff --git a/src/types/batch-response.ts b/src/types/batch-response.ts index ea7bd4fb..d333e250 100644 --- a/src/types/batch-response.ts +++ b/src/types/batch-response.ts @@ -1,3 +1,4 @@ +import { Membership } from "./membership" import { Stack } from "./node" export interface BatchStackCreateResponse { @@ -7,6 +8,6 @@ export interface BatchStackCreateResponse { } export interface BatchMembershipInviteResponse { - data: Array<{ membershipId: string, transactionId: string }> + data: Array<{ membershipId: string, transactionId: string, object?: Membership }> errors: Array<{ email: string, message: string, error: Error }> } From 89a9e7b6dd44729ca407557d61d7edd93c52e782 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Thu, 27 Jul 2023 13:31:29 +0200 Subject: [PATCH 04/13] refactor: setting service context --- src/core/batch.ts | 125 +++++++++++++++++++------------------------- src/core/index.ts | 1 - src/core/node.ts | 19 +++++-- src/core/service.ts | 26 ++++++--- src/core/types.ts | 50 ------------------ 5 files changed, 88 insertions(+), 133 deletions(-) delete mode 100644 src/core/types.ts diff --git a/src/core/batch.ts b/src/core/batch.ts index a2242e83..7712ce59 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -10,7 +10,6 @@ import { Hooks } from "./file"; import { actionRefs, functions, objectType, protocolTags } from "../constants"; import { ContractInput, Tag, Tags } from "../types/contract"; import { ObjectType } from "../types/object"; -import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; import { NodeService } from "./node"; import { BadRequest } from "../errors/bad-request"; @@ -104,7 +103,7 @@ class BatchService extends Service { */ public async stackCreate( vaultId: string, - items: { file: FileLike, name: string, options?: StackCreateOptions }[], + items: StackCreateItem[], options: BatchStackCreateOptions = {} ): Promise { const size = items.reduce((sum, stack) => { @@ -113,7 +112,6 @@ class BatchService extends Service { let progress = 0; let processedStacksCount = 0; const perFileProgress = new Map(); - this.setGroupRef(items); if (options.processingCountHook) { options.processingCountHook(processedStacksCount); } @@ -132,11 +130,15 @@ class BatchService extends Service { options.progressHook = stackProgressHook; } + // set service context const vault = await this.api.getVault(vaultId); this.setVault(vault); this.setVaultId(vaultId); this.setIsPublic(vault.public); await this.setMembershipKeys(vault); + this.setGroupRef(items); + this.setActionRef(actionRefs.STACK_CREATE); + this.setFunction(functions.NODE_CREATE); const stackCreateOptions = { ...options, @@ -144,24 +146,12 @@ class BatchService extends Service { } for (const chunk of [...chunks(items, BatchService.BATCH_CHUNK_SIZE)]) { - const transactions = [] as { - vaultId: string, - input: ContractInput, - tags: Tags, - item: { file: FileLike, name: string, options?: StackCreateOptions } - }[]; + const transactions = [] as StackCreateTransaction[]; // upload file data & metadata await Promise.all(chunk.map(async (item) => { - const service = new StackService(this.wallet, this.api); - service.setVault(vault); - service.setVaultId(vaultId); - service.setIsPublic(vault.public); - await service.setMembershipKeys(vault); + const service = new StackService(this.wallet, this.api, this); service.setVaultContextForFile(); - service.setActionRef(actionRefs.STACK_CREATE); - service.setFunction(functions.NODE_CREATE); - service.setGroupRef(this.groupRef); const nodeId = uuidv4(); service.setObjectId(nodeId); @@ -171,11 +161,8 @@ class BatchService extends Service { ...(item.options || {}) } service.setAkordTags((service.isPublic ? [item.name] : []).concat(createOptions.tags)); + service.setParentId(createOptions.parentId); service.arweaveTags = await service.getTxTags(); - service.arweaveTags.push(new Tag( - protocolTags.PARENT_ID, - createOptions.parentId ? createOptions.parentId : "root" - )); const state = { name: await service.processWriteString(item.name ? item.name : item.file.name), @@ -183,27 +170,18 @@ class BatchService extends Service { tags: service.tags }; const id = await service.uploadState(state); - const input = { - function: service.function, - data: id, - parentId: createOptions.parentId - } + // queue the stack transaction for posting transactions.push({ vaultId: service.vaultId, - input: input, + input: { function: service.function, data: id, parentId: createOptions.parentId }, tags: service.arweaveTags, item }); } )); - let currentTx: { - vaultId: string, - input: ContractInput, - tags: Tags, - item: { file: FileLike, name: string, options?: StackCreateOptions } - }; + let currentTx: StackCreateTransaction; while (processedStacksCount < items.length) { if (options.cancelHook?.signal.aborted) { return { data, errors, cancelled: items.length - processedStacksCount }; @@ -227,14 +205,9 @@ class BatchService extends Service { if (options.onStackCreated) { await options.onStackCreated(object); } - const stack = new Stack(object, this.keys); - if (!this.isPublic) { - try { - await stack.decrypt(); - } catch (error) { - throw new IncorrectEncryptionKey(error); - } - } + + const stack = await new StackService(this.wallet, this.api, this) + .processNode(object, !this.isPublic, this.keys); data.push({ transactionId: id, object: stack, stackId: object.id }); if (options.cancelHook?.signal.aborted) { return { data, errors, cancelled: items.length - processedStacksCount }; @@ -257,27 +230,26 @@ class BatchService extends Service { * @param {MembershipCreateOptions} [options] invitation email message, etc. * @returns Promise with new membership ids & their corresponding transaction ids */ - public async membershipInvite(vaultId: string, items: { email: string, role: RoleType }[], options: MembershipCreateOptions = {}) + public async membershipInvite(vaultId: string, items: MembershipInviteItem[], options: MembershipCreateOptions = {}) : Promise { - this.setGroupRef(items); const members = await this.api.getMembers(vaultId); const data = [] as BatchMembershipInviteResponse["data"]; const errors = []; - const transactions = [] as { - input: ContractInput, - tags: Tags, - item: { email: string, role: RoleType } - }[]; + const transactions = [] as MembershipInviteTransaction[]; + // set service context + this.setGroupRef(items); const vault = await this.api.getVault(vaultId); this.setVault(vault); this.setVaultId(vaultId); this.setIsPublic(vault.public); await this.setMembershipKeys(vault); + this.setActionRef(actionRefs.MEMBERSHIP_INVITE); + this.setFunction(functions.MEMBERSHIP_INVITE); // upload metadata - await Promise.all(items.map(async (item: { email: string, role: RoleType }) => { + await Promise.all(items.map(async (item: MembershipInviteItem) => { const email = item.email.toLowerCase(); const role = item.role; const member = members.find(item => item.email?.toLowerCase() === email); @@ -286,15 +258,8 @@ class BatchService extends Service { errors.push({ email: email, message, error: new BadRequest(message) }); } else { const userHasAccount = await this.api.existsUser(email); - const service = new MembershipService(this.wallet, this.api); - service.setGroupRef(this.groupRef); + const service = new MembershipService(this.wallet, this.api, this); if (userHasAccount) { - service.setVault(vault); - service.setVaultId(vaultId); - service.setIsPublic(vault.public); - await service.setMembershipKeys(this.vault); - service.setActionRef(actionRefs.MEMBERSHIP_INVITE); - service.setFunction(functions.MEMBERSHIP_INVITE); const membershipId = uuidv4(); service.setObjectId(membershipId); @@ -309,14 +274,9 @@ class BatchService extends Service { const dataTxId = await service.uploadState(state); - const input = { - function: service.function, - address, - role, - data: dataTxId - } transactions.push({ - input: input, + vaultId, + input: { function: service.function, address, role, data: dataTxId }, tags: service.arweaveTags, item: item }); @@ -343,7 +303,7 @@ class BatchService extends Service { for (let tx of transactions) { try { const { id, object } = await this.api.postContractTransaction(vaultId, tx.input, tx.tags); - const membership = await new MembershipService(this.wallet, this.api).processMembership(object as Membership, !this.isPublic, this.keys); + const membership = await new MembershipService(this.wallet, this.api, this).processMembership(object as Membership, !this.isPublic, this.keys); data.push({ membershipId: membership.id, transactionId: id, object: membership }); } catch (error: any) { errors.push({ @@ -366,17 +326,15 @@ class BatchService extends Service { ? await this.api.getMembership(item.id) : await this.api.getNode(item.id, item.type); - const service = item.type === objectType.MEMBERSHIP - ? new MembershipService(this.wallet, this.api) - : new NodeService(this.wallet, this.api); if (itemIndex === 0 || this.vaultId !== node.vaultId) { this.setVaultId(node.vaultId); this.setIsPublic(node.__public__); await this.setMembershipKeys(node); } - service.setVaultId(this.vaultId); - service.setIsPublic(this.isPublic); - await service.setMembershipKeys(node); + const service = item.type === objectType.MEMBERSHIP + ? new MembershipService(this.wallet, this.api, this) + : new NodeService(this.wallet, this.api, this); + service.setFunction(item.input.function); service.setActionRef(item.actionRef); service.setObject(node); @@ -402,6 +360,31 @@ export type BatchStackCreateOptions = Hooks & { onStackCreated?: (item: Stack) => Promise }; +export type TransactionPayload = { + vaultId: string, + input: ContractInput, + tags: Tags +} + +export type StackCreateTransaction = TransactionPayload & { + item: StackCreateItem +} + +export type MembershipInviteTransaction = TransactionPayload & { + item: MembershipInviteItem +} + +export type StackCreateItem = { + file: FileLike, + name: string, + options?: StackCreateOptions +} + +export type MembershipInviteItem = { + email: string, + role: RoleType +} + export { BatchService } diff --git a/src/core/index.ts b/src/core/index.ts index fcc81deb..6261f896 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,2 +1 @@ -export * from "./types"; export * from "./service"; diff --git a/src/core/node.ts b/src/core/node.ts index 4875a954..58e2768e 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -12,6 +12,8 @@ import { BadRequest } from '../errors/bad-request'; class NodeService extends Service { objectType: NodeType; + parentId?: string; + defaultListOptions = { shouldDecrypt: true, parentId: undefined, @@ -150,10 +152,11 @@ class NodeService extends Service { const nodeId = uuidv4(); this.setObjectId(nodeId); this.setFunction(functions.NODE_CREATE); + this.setFunction(functions.NODE_CREATE); + this.setParentId(clientInput.parentId); this.arweaveTags = await this.getTxTags(); clientTags?.map((tag: Tag) => this.arweaveTags.push(tag)); - this.arweaveTags.push(new Tag(protocolTags.PARENT_ID, clientInput.parentId ? clientInput.parentId : "root")); const input = { function: this.function, @@ -180,10 +183,8 @@ class NodeService extends Service { ...clientInput } as ContractInput; + this.setParentId(clientInput?.parentId); this.arweaveTags = await this.getTxTags(); - if (clientInput && this.function === functions.NODE_MOVE) { - this.arweaveTags.push(new Tag(protocolTags.PARENT_ID, clientInput.parentId ? clientInput.parentId : "root")); - } if (stateUpdates) { const id = await this.mergeAndUploadState(stateUpdates); @@ -198,6 +199,10 @@ class NodeService extends Service { return { transactionId: id, object: node }; } + async setParentId(parentId?: string) { + this.parentId = parentId; + } + protected async setVaultContextFromNodeId(nodeId: string, type: NodeType, vaultId?: string) { const object = await this.api.getNode(nodeId, type, vaultId); this.setVaultId(object.vaultId); @@ -210,7 +215,11 @@ class NodeService extends Service { async getTxTags(): Promise { const tags = await super.getTxTags(); - return tags.concat(new Tag(protocolTags.NODE_ID, this.objectId)); + tags.push(new Tag(protocolTags.NODE_ID, this.objectId)) + if (this.function === functions.NODE_CREATE || this.function === functions.NODE_MOVE) { + tags.push(new Tag(protocolTags.PARENT_ID, this.parentId ? this.parentId : "root")); + } + return tags; } async processNode(object: NodeLike, shouldDecrypt: boolean, keys?: EncryptedKeys[]): Promise { diff --git a/src/core/service.ts b/src/core/service.ts index 8f8ef8a3..de8f335d 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -54,7 +54,7 @@ class Service { tags: string[] // akord tags for easier search arweaveTags: Tags // arweave tx tags - constructor(wallet: Wallet, api: Api, encryptionKeys?: EncryptionKeys) { + constructor(wallet: Wallet, api: Api, service?: Service, encryptionKeys?: EncryptionKeys) { this.wallet = wallet this.api = api // for the data encryption @@ -63,6 +63,20 @@ class Service { encryptionKeys?.keys, encryptionKeys?.getPublicKey() ) + // set context from another service + if (service) { + this.setVault(service.vault); + this.setVaultId(service.vaultId); + this.setIsPublic(service.isPublic); + this.setKeys(service.keys); + this.setRawDataEncryptionPublicKey(service.dataEncrypter.publicKey); + this.setFunction(service.function); + this.setActionRef(service.actionRef); + this.setObjectId(service.objectId); + this.setObject(service.object); + this.setGroupRef(service.groupRef); + this.setAkordTags(service.tags); + } } setKeys(keys: EncryptedKeys[]) { @@ -360,11 +374,11 @@ class Service { tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); } this.tags - ?.filter(tag => tag) - ?.map((tag: string) => - tag.split(" ").map((value: string) => - tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) - ); + ?.filter(tag => tag) + ?.map((tag: string) => + tag?.split(" ").map((value: string) => + tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) + ); // remove duplicates return [...new Map(tags.map(item => [item.value, item])).values()]; } diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 1c872958..00000000 --- a/src/core/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { EncryptionKeys, Wallet } from "@akord/crypto"; -import { Api } from "../api/api"; -import { VaultService } from "./vault"; -import { StackService } from "./stack"; -import { MemoService } from "./memo"; -import { MembershipService } from "./membership"; -import { FolderService } from "./folder"; -import { NoteService } from "./note" -import { ProfileService } from "./profile"; -import { NodeService } from "./node"; -import { Service } from "./service"; -import { ObjectType } from "../types/object"; - -export class ServiceFactory { - - service: Service - - constructor(wallet: Wallet, api: Api, objectType: ObjectType, encryptionKeys?: EncryptionKeys) { - switch (objectType) { - case "Vault": - this.service = new VaultService(wallet, api, encryptionKeys); - break - case "Membership": - this.service = new MembershipService(wallet, api, encryptionKeys); - break - case "Stack": - this.service = new StackService(wallet, api, encryptionKeys); - break - case "Folder": - this.service = new FolderService(wallet, api, encryptionKeys); - break - case "Memo": - this.service = new MemoService(wallet, api, encryptionKeys); - break - case "Note": - this.service = new NoteService(wallet, api, encryptionKeys); - break - case "Profile": - this.service = new ProfileService(wallet, api, encryptionKeys); - break - default: - this.service = new NodeService(wallet, api, encryptionKeys); - break - } - } - - serviceInstance() { - return this.service - } -} From b09b643733a8954890c6a71a242bcaeb6b97e5bb Mon Sep 17 00:00:00 2001 From: Weronika K Date: Thu, 27 Jul 2023 14:53:10 +0200 Subject: [PATCH 05/13] fix: stack transaction queue --- src/core/batch.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/batch.ts b/src/core/batch.ts index 7712ce59..e4335ff5 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -22,6 +22,7 @@ function* chunks(arr: T[], n: number): Generator { class BatchService extends Service { public static BATCH_CHUNK_SIZE = 50; + public static TRANSACTION_QUEUE_WAIT_TIME = 1; /** * @param {{id:string,type:NoteType}[]} items @@ -149,7 +150,7 @@ class BatchService extends Service { const transactions = [] as StackCreateTransaction[]; // upload file data & metadata - await Promise.all(chunk.map(async (item) => { + Promise.all(chunk.map(async (item) => { const service = new StackService(this.wallet, this.api, this); service.setVaultContextForFile(); @@ -181,6 +182,7 @@ class BatchService extends Service { } )); + // post queued stack transactions let currentTx: StackCreateTransaction; while (processedStacksCount < items.length) { if (options.cancelHook?.signal.aborted) { @@ -188,7 +190,7 @@ class BatchService extends Service { } if (transactions.length === 0) { // wait for a while if the queue is empty before checking again - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, BatchService.TRANSACTION_QUEUE_WAIT_TIME)); } else { try { currentTx = transactions.shift(); From 70d0539c3062d31b5b1c05e6d8430ff41870851c Mon Sep 17 00:00:00 2001 From: Weronika K Date: Wed, 2 Aug 2023 17:22:15 +0200 Subject: [PATCH 06/13] fix: setting node tags --- src/core/folder.ts | 2 +- src/core/memo.ts | 2 +- src/core/stack.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/folder.ts b/src/core/folder.ts index d35059a9..041c8109 100644 --- a/src/core/folder.ts +++ b/src/core/folder.ts @@ -19,7 +19,7 @@ class FolderService extends NodeService { this.setAkordTags((this.isPublic ? [name] : []).concat(options.tags)); const state = { name: await this.processWriteString(name), - tags: this.tags + tags: options.tags } const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { folderId: nodeId, transactionId, object }; diff --git a/src/core/memo.ts b/src/core/memo.ts index 1d602420..e45314df 100644 --- a/src/core/memo.ts +++ b/src/core/memo.ts @@ -31,7 +31,7 @@ class MemoService extends NodeService { this.setAkordTags(options.tags); const state = { versions: [await this.memoVersion(message)], - tags: this.tags + tags: options.tags }; const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { memoId: nodeId, transactionId, object }; diff --git a/src/core/stack.ts b/src/core/stack.ts index 16ce17b8..5951e3e3 100644 --- a/src/core/stack.ts +++ b/src/core/stack.ts @@ -33,7 +33,7 @@ class StackService extends NodeService { const state = { name: await this.processWriteString(name ? name : file.name), versions: [await this.uploadNewFileVersion(file, createOptions)], - tags: this.tags + tags: createOptions.tags }; const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; From e890a9218c62476988878d22228863579926c201 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Wed, 2 Aug 2023 17:22:44 +0200 Subject: [PATCH 07/13] feat: split tags also by dot & comma --- src/core/service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/service.ts b/src/core/service.ts index 2f7fe560..5b52c9ed 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -360,11 +360,11 @@ class Service { tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); } this.tags - ?.filter(tag => tag) - ?.map((tag: string) => - tag.split(" ").map((value: string) => - tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) - ); + ?.filter(tag => tag) + ?.map((tag: string) => + tag?.split(" ").join(",").split(".").join(",").split(",").map((value: string) => + tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) + ); // remove duplicates return [...new Map(tags.map(item => [item.value, item])).values()]; } From aef04fb87fc5bd31a8b1e9785b4b6f0caa5f7028 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Wed, 2 Aug 2023 17:24:58 +0200 Subject: [PATCH 08/13] fix: set default tags value --- src/core/folder.ts | 2 +- src/core/memo.ts | 2 +- src/core/stack.ts | 2 +- src/core/vault.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/folder.ts b/src/core/folder.ts index 041c8109..14bd0d5f 100644 --- a/src/core/folder.ts +++ b/src/core/folder.ts @@ -19,7 +19,7 @@ class FolderService extends NodeService { this.setAkordTags((this.isPublic ? [name] : []).concat(options.tags)); const state = { name: await this.processWriteString(name), - tags: options.tags + tags: options.tags || [] } const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { folderId: nodeId, transactionId, object }; diff --git a/src/core/memo.ts b/src/core/memo.ts index e45314df..48fdb74c 100644 --- a/src/core/memo.ts +++ b/src/core/memo.ts @@ -31,7 +31,7 @@ class MemoService extends NodeService { this.setAkordTags(options.tags); const state = { versions: [await this.memoVersion(message)], - tags: options.tags + tags: options.tags || [] }; const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { memoId: nodeId, transactionId, object }; diff --git a/src/core/stack.ts b/src/core/stack.ts index 5951e3e3..21e93b03 100644 --- a/src/core/stack.ts +++ b/src/core/stack.ts @@ -33,7 +33,7 @@ class StackService extends NodeService { const state = { name: await this.processWriteString(name ? name : file.name), versions: [await this.uploadNewFileVersion(file, createOptions)], - tags: createOptions.tags + tags: createOptions.tags || [] }; const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; diff --git a/src/core/vault.ts b/src/core/vault.ts index 5871c1cc..094889f9 100644 --- a/src/core/vault.ts +++ b/src/core/vault.ts @@ -127,7 +127,7 @@ class VaultService extends Service { name: await this.processWriteString(name), termsOfAccess: createOptions.termsOfAccess, description: createOptions.description ? await this.processWriteString(createOptions.description) : undefined, - tags: createOptions.tags + tags: createOptions.tags || [] } const vaultStateTx = await this.uploadState(vaultState, createOptions.cacheOnly); From 90e361c1a9beda4f584dff03124884ff8e46b4a6 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Wed, 2 Aug 2023 15:27:51 +0000 Subject: [PATCH 09/13] ci: version bump to v4.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9c904ec..42cf09d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@akord/akord-js", - "version": "4.16.1", + "version": "4.17.0", "description": "A set of core js functions to interact with Akord", "main": "lib/index.js", "repository": "git@github.com:Akord-com/akord-js.git", From c0ca0436c93daa1978f83e78598998d248067189 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Thu, 3 Aug 2023 08:36:48 +0000 Subject: [PATCH 10/13] ci: version bump to v4.18.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42cf09d7..e48ac624 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@akord/akord-js", - "version": "4.17.0", + "version": "4.18.0", "description": "A set of core js functions to interact with Akord", "main": "lib/index.js", "repository": "git@github.com:Akord-com/akord-js.git", From 24c10da10201f0c369de18cc436f0a65c91bb628 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Wed, 9 Aug 2023 15:31:37 +0200 Subject: [PATCH 11/13] fix: use fresh service instance for akord calls --- src/core/batch.ts | 9 +- src/core/contract.ts | 1 - src/core/file.ts | 44 ++++++--- src/core/folder.ts | 13 +-- src/core/membership.ts | 203 +++++++++++++++++++++-------------------- src/core/memo.ts | 57 ++++++------ src/core/node.ts | 47 +++++----- src/core/stack.ts | 101 +++++++++----------- src/core/vault.ts | 171 +++++++++++++++++----------------- 9 files changed, 336 insertions(+), 310 deletions(-) diff --git a/src/core/batch.ts b/src/core/batch.ts index e4335ff5..3102ac3b 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -6,7 +6,7 @@ import { Node, NodeLike, NodeType, Stack } from "../types/node"; import { FileLike } from "../types/file"; import { BatchMembershipInviteResponse, BatchStackCreateResponse } from "../types/batch-response"; import { Membership, RoleType } from "../types/membership"; -import { Hooks } from "./file"; +import { FileService, Hooks } from "./file"; import { actionRefs, functions, objectType, protocolTags } from "../constants"; import { ContractInput, Tag, Tags } from "../types/contract"; import { ObjectType } from "../types/object"; @@ -152,7 +152,6 @@ class BatchService extends Service { // upload file data & metadata Promise.all(chunk.map(async (item) => { const service = new StackService(this.wallet, this.api, this); - service.setVaultContextForFile(); const nodeId = uuidv4(); service.setObjectId(nodeId); @@ -165,9 +164,13 @@ class BatchService extends Service { service.setParentId(createOptions.parentId); service.arweaveTags = await service.getTxTags(); + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(item.file, createOptions); + const version = await fileService.newVersion(item.file, fileUploadResult); + const state = { name: await service.processWriteString(item.name ? item.name : item.file.name), - versions: [await service.uploadNewFileVersion(item.file, createOptions)], + versions: [version], tags: service.tags }; const id = await service.uploadState(state); diff --git a/src/core/contract.ts b/src/core/contract.ts index c773f037..aa0047f4 100644 --- a/src/core/contract.ts +++ b/src/core/contract.ts @@ -11,7 +11,6 @@ class ContractService extends Service { */ public async getState(id: string): Promise { const contract = await this.api.getContractState(id); - this.setIsPublic(contract.public); if (contract.public) { return contract; } else { diff --git a/src/core/file.ts b/src/core/file.ts index cb0216c2..6ede2ae0 100644 --- a/src/core/file.ts +++ b/src/core/file.ts @@ -11,6 +11,7 @@ import { BinaryLike } from "crypto"; import { getTxData, getTxMetadata } from "../arweave"; import * as mime from "mime-types"; import { CONTENT_TYPE as MANIFEST_CONTENT_TYPE, FILE_TYPE as MANIFEST_FILE_TYPE } from "./manifest"; +import { FileVersion } from "../types/node"; const DEFAULT_FILE_TYPE = "text/plain"; @@ -28,27 +29,27 @@ class FileService extends Service { * @returns Promise with file buffer */ public async get(id: string, vaultId: string, options: DownloadOptions = {}): Promise { - await this.setVaultContext(vaultId); + const service = new FileService(this.wallet, this.api); + await service.setVaultContext(vaultId); const downloadOptions = options as FileDownloadOptions; - downloadOptions.public = this.isPublic; + downloadOptions.public = service.isPublic; let fileBinary: ArrayBuffer; if (options.isChunked) { let currentChunk = 0; while (currentChunk < options.numberOfChunks) { const url = `${id}_${currentChunk}`; downloadOptions.loadedSize = currentChunk * this.chunkSize; - const chunkBinary = await this.getBinary(url, downloadOptions); - fileBinary = this.appendBuffer(fileBinary, chunkBinary); + const chunkBinary = await service.getBinary(url, downloadOptions); + fileBinary = service.appendBuffer(fileBinary, chunkBinary); currentChunk++; } } else { const { fileData, metadata } = await this.api.downloadFile(id, downloadOptions); - fileBinary = await this.processReadRaw(fileData, metadata) + fileBinary = await service.processReadRaw(fileData, metadata) } return fileBinary; } - /** * Downloads the file keeping memory consumed (RAM) under defiend level: this#chunkSize. * In browser, streaming of the binary requires self hosting of mitm.html and sw.js @@ -59,17 +60,18 @@ class FileService extends Service { * @returns Promise with file buffer */ public async download(id: string, vaultId: string, options: DownloadOptions = {}): Promise { - await this.setVaultContext(vaultId); + const service = new FileService(this.wallet, this.api); + await service.setVaultContext(vaultId); const downloadOptions = options as FileDownloadOptions; - downloadOptions.public = this.isPublic; - const writer = await this.stream(options.name, options.resourceSize); + downloadOptions.public = service.isPublic; + const writer = await service.stream(options.name, options.resourceSize); if (options.isChunked) { let currentChunk = 0; try { while (currentChunk < options.numberOfChunks) { const url = `${id}_${currentChunk}`; - downloadOptions.loadedSize = currentChunk * this.chunkSize; - const fileBinary = await this.getBinary(url, downloadOptions); + downloadOptions.loadedSize = currentChunk * service.chunkSize; + const fileBinary = await service.getBinary(url, downloadOptions); if (writer instanceof WritableStreamDefaultWriter) { await writer.ready } @@ -85,7 +87,7 @@ class FileService extends Service { await writer.close(); } } else { - const fileBinary = await this.getBinary(id, downloadOptions); + const fileBinary = await service.getBinary(id, downloadOptions); await writer.write(new Uint8Array(fileBinary)); await writer.close(); } @@ -157,6 +159,24 @@ class FileService extends Service { } } + public async newVersion(file: FileLike, uploadResult: FileUploadResult): Promise { + const version = new FileVersion({ + owner: await this.wallet.getAddress(), + createdAt: JSON.stringify(Date.now()), + name: await this.processWriteString(file.name), + type: file.type, + size: file.size, + resourceUri: [ + `arweave:${uploadResult.resourceTx}`, + `hash:${uploadResult.resourceHash}`, + `s3:${uploadResult.resourceUrl}` + ], + numberOfChunks: uploadResult.numberOfChunks, + chunkSize: uploadResult.chunkSize, + }); + return version; + } + private retrieveFileMetadata(fileTxId: string, tags: Tags = []) : { name: string, type: string } { const type = this.retrieveFileType(tags); diff --git a/src/core/folder.ts b/src/core/folder.ts index 14bd0d5f..2541d77b 100644 --- a/src/core/folder.ts +++ b/src/core/folder.ts @@ -13,15 +13,16 @@ class FolderService extends NodeService { * @returns Promise with new folder id & corresponding transaction id */ public async create(vaultId: string, name: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.FOLDER_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags((this.isPublic ? [name] : []).concat(options.tags)); + const service = new FolderService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.FOLDER_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags((service.isPublic ? [name] : []).concat(options.tags)); const state = { - name: await this.processWriteString(name), + name: await service.processWriteString(name), tags: options.tags || [] } - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { folderId: nodeId, transactionId, object }; } }; diff --git a/src/core/membership.ts b/src/core/membership.ts index 5a4fd6b5..46cd827c 100644 --- a/src/core/membership.ts +++ b/src/core/membership.ts @@ -8,7 +8,6 @@ import { MembershipInput, Tag, Tags } from "../types/contract"; import { Paginated } from "../types/paginated"; import { BadRequest } from "../errors/bad-request"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; -import { UserPublicInfo } from "../types"; export const activeStatus = [status.ACCEPTED, status.PENDING, status.INVITED] as StatusType[]; @@ -90,37 +89,38 @@ class MembershipService extends Service { * @returns Promise with new membership id & corresponding transaction id */ public async invite(vaultId: string, email: string, role: RoleType, options: MembershipCreateOptions = {}): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.MEMBERSHIP_INVITE); - this.setFunction(functions.MEMBERSHIP_INVITE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.MEMBERSHIP_INVITE); + service.setFunction(functions.MEMBERSHIP_INVITE); const membershipId = uuidv4(); - this.setObjectId(membershipId); + service.setObjectId(membershipId); const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(email); const state = { - keys: await this.prepareMemberKeys(publicKey), - encPublicSigningKey: await this.processWriteString(publicSigningKey) + keys: await service.prepareMemberKeys(publicKey), + encPublicSigningKey: await service.processWriteString(publicSigningKey) }; - this.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] - .concat(await this.getTxTags()); + service.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] + .concat(await service.getTxTags()); - const dataTxId = await this.uploadState(state); + const dataTxId = await service.uploadState(state); const input = { - function: this.function, + function: service.function, address, role, data: dataTxId } const { id, object } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags, + service.arweaveTags, { message: options.message } ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { membershipId, transactionId: id, object: membership }; } @@ -137,28 +137,29 @@ class MembershipService extends Service { transactionId: string, members: Array<{ id: string, address: string }> }> { - await this.setVaultContext(vaultId); - this.setActionRef("MEMBERSHIP_AIRDROP"); - this.setFunction(functions.MEMBERSHIP_ADD); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef("MEMBERSHIP_AIRDROP"); + service.setFunction(functions.MEMBERSHIP_ADD); const memberArray = [] as MembershipInput[]; const membersMetadata = []; const dataArray = [] as { id: string, data: string }[]; const memberTags = [] as Tags; for (const member of members) { const membershipId = uuidv4(); - this.setObjectId(membershipId); + service.setObjectId(membershipId); const memberAddress = await deriveAddress(base64ToArray(member.publicSigningKey)); const state = { id: membershipId, address: memberAddress, - keys: await this.prepareMemberKeys(member.publicKey), - encPublicSigningKey: await this.processWriteString(member.publicSigningKey), - memberDetails: await this.processMemberDetails({ name: member.options?.name }, this.vault.cacheOnly), + keys: await service.prepareMemberKeys(member.publicKey), + encPublicSigningKey: await service.processWriteString(member.publicSigningKey), + memberDetails: await service.processMemberDetails({ name: member.options?.name }, service.vault.cacheOnly), }; - const data = await this.uploadState(state); + const data = await service.uploadState(state); dataArray.push({ id: membershipId, data @@ -174,17 +175,17 @@ class MembershipService extends Service { memberTags.push(new Tag(protocolTags.MEMBERSHIP_ID, membershipId)); } - this.arweaveTags = memberTags.concat(await super.getTxTags()); + service.arweaveTags = memberTags.concat(await super.getTxTags()); const input = { - function: this.function, + function: service.function, members: memberArray }; const { id } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags, + service.arweaveTags, { members: membersMetadata } ); return { members: input.members, transactionId: id }; @@ -196,21 +197,22 @@ class MembershipService extends Service { */ public async accept(membershipId: string): Promise { const memberDetails = await this.getProfileDetails(); - await this.setVaultContextFromMembershipId(membershipId); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); const state = { - memberDetails: await this.processMemberDetails(memberDetails, this.object.__cacheOnly__), - encPublicSigningKey: await this.processWriteString(this.wallet.signingPublicKey()) + memberDetails: await service.processMemberDetails(memberDetails, service.object.__cacheOnly__), + encPublicSigningKey: await service.processWriteString(this.wallet.signingPublicKey()) } - this.setActionRef(actionRefs.MEMBERSHIP_ACCEPT); - this.setFunction(functions.MEMBERSHIP_ACCEPT); + service.setActionRef(actionRefs.MEMBERSHIP_ACCEPT); + service.setFunction(functions.MEMBERSHIP_ACCEPT); - const data = await this.mergeAndUploadState(state); + const data = await service.mergeAndUploadState(state); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - await this.getTxTags() + service.vaultId, + { function: service.function, data }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -219,34 +221,35 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async confirm(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_CONFIRM); - this.setFunction(functions.MEMBERSHIP_INVITE); - const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(this.object.email); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_CONFIRM); + service.setFunction(functions.MEMBERSHIP_INVITE); + const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(service.object.email); const state = { - keys: await this.prepareMemberKeys(publicKey), - encPublicSigningKey: await this.processWriteString(publicSigningKey) + keys: await service.prepareMemberKeys(publicKey), + encPublicSigningKey: await service.processWriteString(publicSigningKey) }; - this.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] - .concat(await this.getTxTags()); + service.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] + .concat(await service.getTxTags()); - const dataTxId = await this.uploadState(state); + const dataTxId = await service.uploadState(state); const input = { - function: this.function, + function: service.function, address, data: dataTxId, - role: this.object.role + role: service.object.role } const { id, object } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags + service.arweaveTags ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -255,16 +258,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async reject(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_REJECT); - this.setFunction(functions.MEMBERSHIP_REJECT); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_REJECT); + service.setFunction(functions.MEMBERSHIP_REJECT); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -273,16 +277,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async leave(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_LEAVE); - this.setFunction(functions.MEMBERSHIP_REJECT); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_LEAVE); + service.setFunction(functions.MEMBERSHIP_REJECT); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -291,18 +296,19 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async revoke(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_REVOKE); - this.setFunction(functions.MEMBERSHIP_REVOKE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_REVOKE); + service.setFunction(functions.MEMBERSHIP_REVOKE); - this.arweaveTags = await this.getTxTags(); + service.arweaveTags = await service.getTxTags(); let data: { id: string, value: string }[]; - if (!this.isPublic) { - const memberships = await this.listAll(this.vaultId, { shouldDecrypt: false }); + if (!service.isPublic) { + const memberships = await this.listAll(service.vaultId, { shouldDecrypt: false }); const activeMembers = memberships.filter((member: Membership) => - member.id !== this.objectId + member.id !== service.objectId && (member.status === status.ACCEPTED || member.status === status.PENDING)); // rotate keys for all active members @@ -311,13 +317,13 @@ class MembershipService extends Service { const { publicKey } = await this.api.getUserPublicData(member.email); memberPublicKeys.set(member.id, publicKey); })); - const { memberKeys } = await this.rotateMemberKeys(memberPublicKeys); + const { memberKeys } = await service.rotateMemberKeys(memberPublicKeys); // upload new state for all active members data = []; await Promise.all(activeMembers.map(async (member: Membership) => { const memberService = new MembershipService(this.wallet, this.api); - memberService.setVaultId(this.vaultId); + memberService.setVaultId(service.vaultId); memberService.setObjectId(member.id); memberService.setObject(member); const dataTx = await memberService.mergeAndUploadState({ keys: memberKeys.get(member.id) }); @@ -326,11 +332,11 @@ class MembershipService extends Service { } const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags + service.vaultId, + { function: service.function, data }, + service.arweaveTags ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -340,16 +346,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async changeRole(membershipId: string, role: RoleType): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_CHANGE_ROLE); - this.setFunction(functions.MEMBERSHIP_CHANGE_ROLE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_CHANGE_ROLE); + service.setFunction(functions.MEMBERSHIP_CHANGE_ROLE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, role }, - await this.getTxTags() + service.vaultId, + { function: service.function, role }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -382,28 +389,28 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async inviteResend(membershipId: string): Promise { - const object = await this.api.getMembership(membershipId, this.vaultId); - this.setActionRef(actionRefs.MEMBERSHIP_INVITE_RESEND); - if (object.status !== status.PENDING && object.status !== status.INVITED) { + const membership = await this.api.getMembership(membershipId); + if (membership.status !== status.PENDING && membership.status !== status.INVITED) { throw new BadRequest("Cannot resend the invitation for member: " + membershipId + - ". Found invalid status: " + object.status); + ". Found invalid status: " + membership.status); } - await this.api.inviteResend(object.vaultId, membershipId); + await this.api.inviteResend(membership.vaultId, membershipId); } async profileUpdate(membershipId: string, name: string, avatar: ArrayBuffer): Promise { - await this.setVaultContextFromMembershipId(membershipId); - const memberDetails = await this.processMemberDetails({ name, avatar }, this.object.__cacheOnly__); - this.setActionRef(actionRefs.MEMBERSHIP_PROFILE_UPDATE); - this.setFunction(functions.MEMBERSHIP_UPDATE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + const memberDetails = await service.processMemberDetails({ name, avatar }, service.object.__cacheOnly__); + service.setActionRef(actionRefs.MEMBERSHIP_PROFILE_UPDATE); + service.setFunction(functions.MEMBERSHIP_UPDATE); - const data = await this.mergeAndUploadState({ memberDetails }); + const data = await service.mergeAndUploadState({ memberDetails }); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - await this.getTxTags() + service.vaultId, + { function: service.function, data }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } diff --git a/src/core/memo.ts b/src/core/memo.ts index 48fdb74c..bee3b563 100644 --- a/src/core/memo.ts +++ b/src/core/memo.ts @@ -25,15 +25,16 @@ class MemoService extends NodeService { * @returns Promise with new node id & corresponding transaction id */ public async create(vaultId: string, message: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.MEMO_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags(options.tags); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.MEMO_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags(options.tags); const state = { - versions: [await this.memoVersion(message)], + versions: [await service.memoVersion(message)], tags: options.tags || [] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { memoId: nodeId, transactionId, object }; } @@ -43,22 +44,23 @@ class MemoService extends NodeService { * @returns Promise with corresponding transaction id */ public async addReaction(memoId: string, reaction: reactionEmoji): Promise { - await this.setVaultContextFromNodeId(memoId, this.objectType); - this.setActionRef(actionRefs.MEMO_ADD_REACTION); - this.setFunction(functions.NODE_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContextFromNodeId(memoId, this.objectType); + service.setActionRef(actionRefs.MEMO_ADD_REACTION); + service.setFunction(functions.NODE_UPDATE); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); - newState.versions[newState.versions.length - 1].reactions.push(await this.memoReaction(reaction)); - const dataTxId = await this.uploadState(newState); + newState.versions[newState.versions.length - 1].reactions.push(await service.memoReaction(reaction)); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const memo = await this.processMemo(object, !this.isPublic, this.keys); + const memo = await this.processMemo(object, !service.isPublic, service.keys); return { transactionId: id, object: memo }; } @@ -68,20 +70,21 @@ class MemoService extends NodeService { * @returns Promise with corresponding transaction id */ public async removeReaction(memoId: string, reaction: reactionEmoji): Promise { - await this.setVaultContextFromNodeId(memoId, this.objectType); - this.setActionRef(actionRefs.MEMO_REMOVE_REACTION); - this.setFunction(functions.NODE_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContextFromNodeId(memoId, this.objectType); + service.setActionRef(actionRefs.MEMO_REMOVE_REACTION); + service.setFunction(functions.NODE_UPDATE); + service.arweaveTags = await service.getTxTags(); - const state = await this.deleteReaction(reaction); - const dataTxId = await this.uploadState(state); + const state = await service.deleteReaction(reaction); + const dataTxId = await service.uploadState(state); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const memo = await this.processMemo(object, !this.isPublic, this.keys); + const memo = await this.processMemo(object, !service.isPublic, service.keys); return { transactionId: id, object: memo }; } diff --git a/src/core/node.ts b/src/core/node.ts index 58e2768e..1ffc0646 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -90,13 +90,14 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async rename(nodeId: string, name: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType); - this.setActionRef(this.objectType.toUpperCase() + "_RENAME"); - this.setFunction(functions.NODE_UPDATE); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType); + service.setActionRef(this.objectType.toUpperCase() + "_RENAME"); + service.setFunction(functions.NODE_UPDATE); const state = { - name: await this.processWriteString(name) + name: await service.processWriteString(name) }; - return this.nodeUpdate(state); + return service.nodeUpdate(state) as Promise; } /** @@ -105,10 +106,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async move(nodeId: string, parentId?: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_MOVE"); - this.setFunction(functions.NODE_MOVE); - return this.nodeUpdate(null, { parentId }); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_MOVE"); + service.setFunction(functions.NODE_MOVE); + return service.nodeUpdate(null, { parentId }); } /** @@ -116,10 +118,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async revoke(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_REVOKE"); - this.setFunction(functions.NODE_REVOKE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_REVOKE"); + service.setFunction(functions.NODE_REVOKE); + return service.nodeUpdate() as Promise; } /** @@ -127,10 +130,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async restore(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_RESTORE"); - this.setFunction(functions.NODE_RESTORE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_RESTORE"); + service.setFunction(functions.NODE_RESTORE); + return service.nodeUpdate() as Promise; } /** @@ -138,10 +142,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async delete(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_DELETE"); - this.setFunction(functions.NODE_DELETE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_DELETE"); + service.setFunction(functions.NODE_DELETE); + return service.nodeUpdate() as Promise; } protected async nodeCreate(state?: any, clientInput?: { parentId?: string }, clientTags?: Tags): Promise<{ diff --git a/src/core/stack.ts b/src/core/stack.ts index 0effd0c9..b425ed76 100644 --- a/src/core/stack.ts +++ b/src/core/stack.ts @@ -1,6 +1,6 @@ import { NodeCreateOptions, NodeService } from "./node"; import { actionRefs, functions, objectType } from "../constants"; -import { FileService, FileUploadResult, FileUploadOptions } from "./file"; +import { FileService, FileUploadOptions } from "./file"; import { FileLike } from "../types/file"; import { FileVersion, Stack, StorageType, nodeType } from "../types/node"; @@ -22,20 +22,24 @@ class StackService extends NodeService { ...this.defaultCreateOptions, ...options } - await this.setVaultContext(vaultId); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags((this.isPublic ? [name] : []).concat(createOptions.tags)); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.STACK_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags((service.isPublic ? [name] : []).concat(createOptions.tags)); - createOptions.cacheOnly = this.vault.cacheOnly; + createOptions.cacheOnly = service.vault.cacheOnly; + + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(file, createOptions); + const version = await fileService.newVersion(file, fileUploadResult); const state = { - name: await this.processWriteString(name ? name : file.name), - versions: [await this.uploadNewFileVersion(file, createOptions)], + name: await service.processWriteString(name ? name : file.name), + versions: [version], tags: createOptions.tags || [] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; } @@ -46,25 +50,26 @@ class StackService extends NodeService { * @returns Promise with new stack id & corresponding transaction id */ public async import(vaultId: string, fileTxId: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_CREATE); - this.setFunction(functions.NODE_CREATE); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.STACK_CREATE); + service.setFunction(functions.NODE_CREATE); - const { file, resourceHash, resourceUrl } = await this.fileService.import(fileTxId); + const fileService = new FileService(this.wallet, this.api, service); + const { file, resourceHash, resourceUrl } = await fileService.import(fileTxId); const version = new FileVersion({ owner: await this.wallet.getAddress(), createdAt: JSON.stringify(Date.now()), - name: await this.processWriteString(file.name), + name: await service.processWriteString(file.name), type: file.type, size: file.size, resourceUri: [`arweave:${fileTxId}`, `hash:${resourceHash}`, `s3:${resourceUrl}`], }); const state = { - name: await this.processWriteString(file.name), + name: await service.processWriteString(file.name), versions: [version] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; } @@ -75,17 +80,21 @@ class StackService extends NodeService { * @returns Promise with corresponding transaction id */ public async uploadRevision(stackId: string, file: FileLike, options: FileUploadOptions = {}): Promise { - await this.setVaultContextFromNodeId(stackId, this.objectType); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_UPLOAD_REVISION); + const service = new StackService(this.wallet, this.api); + await service.setVaultContextFromNodeId(stackId, this.objectType); + service.setActionRef(actionRefs.STACK_UPLOAD_REVISION); + service.setFunction(functions.NODE_UPDATE); + + options.cacheOnly = service.object.__cacheOnly__; - options.cacheOnly = this.object.__cacheOnly__; + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(file, options); + const version = await fileService.newVersion(file, fileUploadResult); const state = { - versions: [await this.uploadNewFileVersion(file, options)] + versions: [version] }; - this.setFunction(functions.NODE_UPDATE); - return this.nodeUpdate(state); + return service.nodeUpdate(state); } /** @@ -95,13 +104,13 @@ class StackService extends NodeService { * @returns Promise with version name & data buffer */ public async getVersion(stackId: string, index?: number): Promise<{ name: string, data: ArrayBuffer }> { - const stack = new Stack(await this.api.getNode(stackId, objectType.STACK, this.vaultId), null); + const stack = new Stack(await this.api.getNode(stackId, objectType.STACK), null); const version = stack.getVersion(index); - await this.setVaultContext(stack.vaultId); - this.setVaultContextForFile(); - const { fileData, metadata } = await this.api.downloadFile(version.getUri(StorageType.S3), { public: this.isPublic }); - const data = await this.processReadRaw(fileData, metadata); - const name = await this.processReadString(version.name); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(stack.vaultId); + const { fileData, metadata } = await this.api.downloadFile(version.getUri(StorageType.S3), { public: service.isPublic }); + const data = await service.processReadRaw(fileData, metadata); + const name = await service.processReadString(version.name); return { name, data }; } @@ -113,37 +122,9 @@ class StackService extends NodeService { * @returns Promise with stack file uri */ public async getUri(stackId: string, type: StorageType = StorageType.ARWEAVE, index?: number): Promise { - const stack = new Stack(await this.api.getNode(stackId, objectType.STACK, this.vaultId), null); + const stack = new Stack(await this.api.getNode(stackId, objectType.STACK), null); return stack.getUri(type, index); } - - public async uploadNewFileVersion(file: FileLike, options: FileUploadOptions): Promise { - const { - resourceTx, - resourceUrl, - resourceHash, - numberOfChunks, - chunkSize - } = await this.fileService.create(file, options); - const version = new FileVersion({ - owner: await this.wallet.getAddress(), - createdAt: JSON.stringify(Date.now()), - name: await this.processWriteString(file.name), - type: file.type, - size: file.size, - resourceUri: [`arweave:${resourceTx}`, `hash:${resourceHash}`, `s3:${resourceUrl}`], - numberOfChunks, - chunkSize, - }); - return version; - } - - setVaultContextForFile(): void { - this.fileService.setKeys(this.keys); - this.fileService.setRawDataEncryptionPublicKey(this.dataEncrypter.publicKey); - this.fileService.setVaultId(this.vaultId); - this.fileService.setIsPublic(this.isPublic); - } }; export type StackCreateOptions = NodeCreateOptions & FileUploadOptions; diff --git a/src/core/vault.ts b/src/core/vault.ts index 094889f9..7ab417cb 100644 --- a/src/core/vault.ts +++ b/src/core/vault.ts @@ -99,57 +99,58 @@ class VaultService extends Service { } const memberDetails = await this.getProfileDetails(); - this.setActionRef(actionRefs.VAULT_CREATE); - this.setIsPublic(createOptions.public); - this.setFunction(functions.VAULT_CREATE); - this.setVaultId(vaultId); - this.setObjectId(vaultId); - this.setAkordTags((this.isPublic ? [name] : []).concat(createOptions.tags)); + const service = new VaultService(this.wallet, this.api); + service.setActionRef(actionRefs.VAULT_CREATE); + service.setIsPublic(createOptions.public); + service.setFunction(functions.VAULT_CREATE); + service.setVaultId(vaultId); + service.setObjectId(vaultId); + service.setAkordTags((service.isPublic ? [name] : []).concat(createOptions.tags)); const address = await this.wallet.getAddress(); const membershipId = uuidv4(); - this.arweaveTags = [ + service.arweaveTags = [ new Tag(protocolTags.MEMBER_ADDRESS, address), new Tag(protocolTags.MEMBERSHIP_ID, membershipId), - ].concat(await this.getTxTags()); - createOptions.arweaveTags?.map((tag: Tag) => this.arweaveTags.push(tag)); + ].concat(await service.getTxTags()); + createOptions.arweaveTags?.map((tag: Tag) => service.arweaveTags.push(tag)); let keys: EncryptedKeys[]; - if (!this.isPublic) { - const { memberKeys, keyPair } = await this.rotateMemberKeys(new Map([[membershipId, this.wallet.publicKey()]])); + if (!service.isPublic) { + const { memberKeys, keyPair } = await service.rotateMemberKeys(new Map([[membershipId, this.wallet.publicKey()]])); keys = memberKeys.get(membershipId); - this.setRawDataEncryptionPublicKey(keyPair.publicKey); - this.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); + service.setRawDataEncryptionPublicKey(keyPair.publicKey); + service.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); } const vaultState = { - name: await this.processWriteString(name), + name: await service.processWriteString(name), termsOfAccess: createOptions.termsOfAccess, - description: createOptions.description ? await this.processWriteString(createOptions.description) : undefined, + description: createOptions.description ? await service.processWriteString(createOptions.description) : undefined, tags: createOptions.tags || [] } - const vaultStateTx = await this.uploadState(vaultState, createOptions.cacheOnly); + const vaultStateTx = await service.uploadState(vaultState, createOptions.cacheOnly); const memberState = { keys, - encPublicSigningKey: await this.processWriteString(this.wallet.signingPublicKey()), - memberDetails: await this.processMemberDetails(memberDetails, createOptions.cacheOnly) + encPublicSigningKey: await service.processWriteString(this.wallet.signingPublicKey()), + memberDetails: await service.processMemberDetails(memberDetails, createOptions.cacheOnly) } const memberService = new MembershipService(this.wallet, this.api); - memberService.setVaultId(this.vaultId); + memberService.setVaultId(service.vaultId); memberService.setObjectId(membershipId); const memberStateTx = await memberService.uploadState(memberState, createOptions.cacheOnly); const data = { vault: vaultStateTx, membership: memberStateTx }; const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags, + service.vaultId, + { function: service.function, data }, + service.arweaveTags, { cacheOnly: createOptions.cacheOnly } ); - const vault = await this.processVault(object, true, this.keys); + const vault = await service.processVault(object, true, service.keys); return { vaultId, membershipId, transactionId: id, object: vault }; } @@ -162,32 +163,33 @@ class VaultService extends Service { if (!options.name && !options.tags && !options.description) { throw new BadRequest("Nothing to update"); } - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_UPDATE_METADATA); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_UPDATE_METADATA); + service.setFunction(functions.VAULT_UPDATE); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (options.name) { - newState.name = await this.processWriteString(options.name); + newState.name = await service.processWriteString(options.name); } if (options.tags) { newState.tags = options.tags; } if (options.description) { - newState.description = await this.processWriteString(options.description); + newState.description = await service.processWriteString(options.description); } - this.setAkordTags((options.name && this.isPublic ? [options.name] : []).concat(options.tags)); - this.arweaveTags = await this.getTxTags(); + service.setAkordTags((options.name && service.isPublic ? [options.name] : []).concat(options.tags)); + service.arweaveTags = await service.getTxTags(); - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -197,22 +199,23 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async rename(vaultId: string, name: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_RENAME); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_RENAME); + service.setFunction(functions.VAULT_UPDATE); const state = { - name: await this.processWriteString(name) + name: await service.processWriteString(name) }; - const data = await this.mergeAndUploadState(state); - this.setAkordTags(this.isPublic ? [name] : []); - this.arweaveTags = await this.getTxTags(); + const data = await service.mergeAndUploadState(state); + service.setAkordTags(service.isPublic ? [name] : []); + service.arweaveTags = await service.getTxTags(); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags + service.vaultId, + { function: service.function, data }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -222,14 +225,15 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async addTags(vaultId: string, tags: string[]): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_ADD_TAGS); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_ADD_TAGS); + service.setFunction(functions.VAULT_UPDATE); - this.setAkordTags(tags); - this.arweaveTags = await this.getTxTags(); + service.setAkordTags(tags); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (!newState.tags) { newState.tags = []; @@ -239,14 +243,14 @@ class VaultService extends Service { newState.tags.push(tag); } } - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -256,12 +260,13 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async removeTags(vaultId: string, tags: string[]): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_REMOVE_TAGS); - this.setFunction(functions.VAULT_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_REMOVE_TAGS); + service.setFunction(functions.VAULT_UPDATE); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (!newState.tags || newState.tags.length === 0) { throw new BadRequest("Tags cannot be removed, vault does not have any"); @@ -270,14 +275,14 @@ class VaultService extends Service { const index = this.getTagIndex(newState.tags, tag); newState.tags.splice(index, 1); } - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -286,16 +291,17 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async archive(vaultId: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_ARCHIVE); - this.setFunction(functions.VAULT_ARCHIVE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_ARCHIVE); + service.setFunction(functions.VAULT_ARCHIVE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -304,16 +310,17 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async restore(vaultId: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_RESTORE); - this.setFunction(functions.VAULT_RESTORE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_RESTORE); + service.setFunction(functions.VAULT_RESTORE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } From 454cd8f20b5620fcee8fd496e8b4cfbd6c412ce4 Mon Sep 17 00:00:00 2001 From: Weronika K Date: Wed, 9 Aug 2023 20:20:38 +0200 Subject: [PATCH 12/13] refactor: service cleanup --- src/core/common.ts | 59 +++++++++ src/core/membership.ts | 54 +++++++- src/core/node.ts | 5 +- src/core/profile.ts | 65 +++++++++- src/core/service.ts | 283 ++++++++++------------------------------- src/core/vault.ts | 28 ++-- 6 files changed, 257 insertions(+), 237 deletions(-) create mode 100644 src/core/common.ts diff --git a/src/core/common.ts b/src/core/common.ts new file mode 100644 index 00000000..eaa866d9 --- /dev/null +++ b/src/core/common.ts @@ -0,0 +1,59 @@ +import { ListOptions } from "../types/query-options"; +import lodash from "lodash"; +import { EncryptionMetadata } from "./service"; +import { EncryptedPayload } from "@akord/crypto/lib/types"; +import { base64ToArray } from "@akord/crypto"; + +export const handleListErrors = async (originalItems: Array, promises: Array>) + : Promise<{ items: Array, errors: Array<{ id: string, error: Error }> }> => { + const results = await Promise.all(promises.map(p => p.catch(e => e))); + const items = results.filter(result => !(result instanceof Error)); + const errors = results + .map((result, index) => ({ result, index })) + .filter((mapped) => mapped.result instanceof Error) + .map((filtered) => ({ id: (originalItems[filtered.index]).id, error: filtered.result })); + return { items, errors }; +} + +export const paginate = async (apiCall: any, listOptions: ListOptions & { vaultId?: string }): Promise> => { + let token = undefined; + let results = [] as T[]; + do { + const { items, nextToken } = await apiCall(listOptions); + results = results.concat(items); + token = nextToken; + listOptions.nextToken = nextToken; + if (nextToken === "null") { + token = undefined; + } + } while (token); + return results; +} + +export const mergeState = (currentState: any, stateUpdates: any): any => { + let newState = lodash.cloneDeepWith(currentState); + lodash.mergeWith( + newState, + stateUpdates, + function concatArrays(objValue, srcValue) { + if (lodash.isArray(objValue)) { + return objValue.concat(srcValue); + } + }); + return newState; +} + +export const getEncryptedPayload = (data: ArrayBuffer | string, metadata: EncryptionMetadata) + : EncryptedPayload => { + const { encryptedKey, iv } = metadata; + if (encryptedKey && iv) { + return { + encryptedKey, + encryptedData: { + iv: base64ToArray(iv), + ciphertext: data as ArrayBuffer + } + } + } + return null; +} diff --git a/src/core/membership.ts b/src/core/membership.ts index 46cd827c..3d11c5d5 100644 --- a/src/core/membership.ts +++ b/src/core/membership.ts @@ -1,6 +1,6 @@ import { actionRefs, objectType, status, functions, protocolTags } from "../constants"; import { v4 as uuidv4 } from "uuid"; -import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray } from "@akord/crypto"; +import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray, generateKeyPair, Keys } from "@akord/crypto"; import { Service } from "./service"; import { Membership, RoleType, StatusType } from "../types/membership"; import { GetOptions, ListOptions } from "../types/query-options"; @@ -8,6 +8,9 @@ import { MembershipInput, Tag, Tags } from "../types/contract"; import { Paginated } from "../types/paginated"; import { BadRequest } from "../errors/bad-request"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; +import { handleListErrors, paginate } from "./common"; +import { ProfileService } from "./profile"; +import { ProfileDetails } from "../types/profile-details"; export const activeStatus = [status.ACCEPTED, status.PENDING, status.INVITED] as StatusType[]; @@ -60,7 +63,7 @@ class MembershipService extends Service { .map(async (membershipProto: Membership) => { return await this.processMembership(membershipProto, !membershipProto.__public__ && listOptions.shouldDecrypt, membershipProto.__keys__); }) as Promise[]; - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -77,7 +80,7 @@ class MembershipService extends Service { const list = async (options: ListOptions & { vaultId: string }) => { return await this.list(options.vaultId, options); } - return await this.paginate(list, { ...options, vaultId }); + return await paginate(list, { ...options, vaultId }); } /** @@ -196,7 +199,8 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async accept(membershipId: string): Promise { - const memberDetails = await this.getProfileDetails(); + const profileService = new ProfileService(this.wallet, this.api); + const memberDetails = await profileService.get(); const service = new MembershipService(this.wallet, this.api); await service.setVaultContextFromMembershipId(membershipId); const state = { @@ -457,6 +461,48 @@ class MembershipService extends Service { return null; } } + + async rotateMemberKeys(publicKeys: Map): Promise<{ + memberKeys: Map, + keyPair: Keys + }> { + const memberKeys = new Map(); + // generate a new vault key pair + const keyPair = await generateKeyPair(); + + for (let [memberId, publicKey] of publicKeys) { + const memberKeysEncrypter = new Encrypter( + this.wallet, + this.dataEncrypter.keys, + base64ToArray(publicKey) + ); + try { + memberKeys.set(memberId, [await memberKeysEncrypter.encryptMemberKey(keyPair)]); + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + return { memberKeys, keyPair }; + } + + async processMemberDetails(memberDetails: { name?: string, avatar?: ArrayBuffer }, cacheOnly?: boolean) { + const processedMemberDetails = {} as ProfileDetails; + if (!this.isPublic) { + if (memberDetails.name) { + processedMemberDetails.name = await this.processWriteString(memberDetails.name); + } + if (memberDetails.avatar) { + const { resourceUrl, resourceTx } = await this.processAvatar(memberDetails.avatar, cacheOnly); + processedMemberDetails.avatarUri = [`arweave:${resourceTx}`, `s3:${resourceUrl}`]; + } + } + return new ProfileDetails(processedMemberDetails); + } + + private async processAvatar(avatar: ArrayBuffer, cacheOnly?: boolean): Promise<{ resourceTx: string, resourceUrl: string }> { + const { processedData, encryptionTags } = await this.processWriteRaw(avatar); + return this.api.uploadFile(processedData, encryptionTags, { cacheOnly, public: false }); + } }; export type MembershipCreateOptions = { diff --git a/src/core/node.ts b/src/core/node.ts index 1ffc0646..44a304be 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -8,6 +8,7 @@ import { Paginated } from '../types/paginated'; import { v4 as uuidv4 } from "uuid"; import { IncorrectEncryptionKey } from '../errors/incorrect-encryption-key'; import { BadRequest } from '../errors/bad-request'; +import { handleListErrors, paginate } from './common'; class NodeService extends Service { objectType: NodeType; @@ -64,7 +65,7 @@ class NodeService extends Service { .map(async (nodeProto: any) => { return await this.processNode(nodeProto, !nodeProto.__public__ && listOptions.shouldDecrypt, nodeProto.__keys__); }); - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -81,7 +82,7 @@ class NodeService extends Service { const list = async (options: ListOptions & { vaultId: string }) => { return await this.list(options.vaultId, options); } - return await this.paginate(list, { ...options, vaultId }); + return await paginate(list, { ...options, vaultId }); } /** diff --git a/src/core/profile.ts b/src/core/profile.ts index cc52a609..cd48a123 100644 --- a/src/core/profile.ts +++ b/src/core/profile.ts @@ -6,6 +6,9 @@ import { Service } from "./service"; import { objectType } from "../constants"; import { Membership } from "../types/membership"; import { ListOptions } from "../types/query-options"; +import { getEncryptedPayload, handleListErrors, paginate } from "./common"; +import { Encrypter, arrayToString } from "@akord/crypto"; +import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; class ProfileService extends Service { objectType = objectType.PROFILE; @@ -20,7 +23,42 @@ class ProfileService extends Service { shouldCacheDecider: () => CacheBusters.cache }) public async get(): Promise { - return await this.getProfileDetails(); + const user = await this.api.getUser(); + if (user) { + const profileEncrypter = new Encrypter(this.wallet, null, null); + profileEncrypter.decryptedKeys = [ + { + publicKey: this.wallet.publicKeyRaw(), + privateKey: this.wallet.privateKeyRaw() + } + ] + let avatar = null; + const resourceUri = getAvatarUri(new ProfileDetails(user)); + if (resourceUri) { + const { fileData, metadata } = await this.api.downloadFile(resourceUri); + const encryptedPayload = getEncryptedPayload(fileData, metadata); + try { + if (encryptedPayload) { + avatar = await profileEncrypter.decryptRaw(encryptedPayload, false); + } else { + const dataString = arrayToString(new Uint8Array(fileData)); + avatar = await profileEncrypter.decryptRaw(dataString, true); + } + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + try { + const decryptedProfile = await profileEncrypter.decryptObject( + user, + ['name'], + ); + return { ...decryptedProfile, avatar } + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + return {}; } /** @@ -38,11 +76,12 @@ class ProfileService extends Service { }> { // update profile const user = await this.api.getUser(); - this.setObject(user); - this.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw()); - this.setIsPublic(false); - const profileDetails = await this.processMemberDetails({ name, avatar }, true); + const service = new MembershipService(this.wallet, this.api); + + service.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw()); + service.setIsPublic(false); + const profileDetails = await service.processMemberDetails({ name, avatar }, true); const newProfileDetails = new ProfileDetails({ ...user, @@ -61,7 +100,7 @@ class ProfileService extends Service { transactions.push({ id: membership.id, transactionId: transactionId }); return membership; }) - const { errors } = await this.handleListErrors(memberships, membershipPromises); + const { errors } = await handleListErrors(memberships, membershipPromises); return { transactions, errors }; } @@ -69,10 +108,22 @@ class ProfileService extends Service { const list = async (listOptions: ListOptions) => { return await this.api.getMemberships(listOptions); } - return await this.paginate(list, {}); + return await paginate(list, {}); } }; +const getAvatarUri = (profileDetails: ProfileDetails) => { + if (profileDetails.avatarUri && profileDetails.avatarUri.length) { + const avatarUri = [...profileDetails.avatarUri] + .reverse() + .find(resourceUri => resourceUri.startsWith("s3:")) + ?.replace("s3:", ""); + return avatarUri !== "null" && avatarUri; + } else { + return null; + } +} + export { ProfileService } diff --git a/src/core/service.ts b/src/core/service.ts index 34bdddee..c17600e0 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -11,12 +11,9 @@ import { arrayToBase64, base64ToJson, deriveAddress, - EncryptedKeys, - generateKeyPair, - Keys + EncryptedKeys } from "@akord/crypto"; import { objectType, protocolTags, functions, dataTags, encryptionTags, smartweaveTags, AKORD_TAG } from '../constants'; -import lodash from "lodash"; import { Vault } from "../types/vault"; import { Tag, Tags } from "../types/contract"; import { NodeLike } from "../types/node"; @@ -24,8 +21,7 @@ import { Membership } from "../types/membership"; import { Object, ObjectType } from "../types/object"; import { EncryptedPayload } from "@akord/crypto/lib/types"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; -import { ProfileDetails } from "../types/profile-details"; -import { ListOptions } from "../types/query-options"; +import { getEncryptedPayload, mergeState } from "./common"; export type EncryptionMetadata = { encryptedKey?: string, @@ -128,21 +124,18 @@ class Service { this.tags = tags; } - async processReadRaw(data: ArrayBuffer | string, metadata: EncryptionMetadata, shouldDecrypt = true): Promise { - if (this.isPublic || !shouldDecrypt) { - return data as ArrayBuffer; - } - - const encryptedPayload = this.getEncryptedPayload(data, metadata); + async processWriteString(data: string): Promise { + if (this.isPublic) return data; + let encryptedPayload: string; try { - if (encryptedPayload) { - return this.dataEncrypter.decryptRaw(encryptedPayload, false); - } else { - return this.dataEncrypter.decryptRaw(data as string); - } + encryptedPayload = await this.dataEncrypter.encryptRaw(stringToArray(data)) as string; } catch (error) { throw new IncorrectEncryptionKey(error); } + const decodedPayload = base64ToJson(encryptedPayload) as any; + decodedPayload.publicAddress = (await this.getActiveKey()).address; + delete decodedPayload.publicKey; + return jsonToBase64(decodedPayload); } async setVaultContext(vaultId: string) { @@ -176,43 +169,48 @@ class Service { } } - protected async getProfileDetails(): Promise { - const user = await this.api.getUser(); - if (user) { - const profileEncrypter = new Encrypter(this.wallet, null, null); - profileEncrypter.decryptedKeys = [ - { - publicKey: this.wallet.publicKeyRaw(), - privateKey: this.wallet.privateKeyRaw() - } - ] - let avatar = null; - const resourceUri = this.getAvatarUri(new ProfileDetails(user)); - if (resourceUri) { - const { fileData, metadata } = await this.api.downloadFile(resourceUri); - const encryptedPayload = this.getEncryptedPayload(fileData, metadata); - try { - if (encryptedPayload) { - avatar = await profileEncrypter.decryptRaw(encryptedPayload, false); - } else { - const dataString = arrayToString(new Uint8Array(fileData)); - avatar = await profileEncrypter.decryptRaw(dataString, true); - } - } catch (error) { - throw new IncorrectEncryptionKey(error); - } - } - try { - const decryptedProfile = await profileEncrypter.decryptObject( - user, - ['name'], - ); - return { ...decryptedProfile, avatar } - } catch (error) { - throw new IncorrectEncryptionKey(error); - } + async uploadState(state: any, cacheOnly = false): Promise { + const signature = await this.signData(state); + const tags = [ + new Tag(dataTags.DATA_TYPE, "State"), + new Tag(smartweaveTags.CONTENT_TYPE, STATE_CONTENT_TYPE), + new Tag(protocolTags.SIGNATURE, signature), + new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), + new Tag(protocolTags.VAULT_ID, this.vaultId), + new Tag(protocolTags.NODE_TYPE, this.objectType), + ] + if (this.objectType === objectType.MEMBERSHIP) { + tags.push(new Tag(protocolTags.MEMBERSHIP_ID, this.objectId)) + } else if (this.objectType !== objectType.VAULT) { + tags.push(new Tag(protocolTags.NODE_ID, this.objectId)) } - return {}; + const ids = await this.api.uploadData([{ data: state, tags }], { cacheOnly }); + return ids[0]; + } + + async getTxTags(): Promise { + const tags = [ + new Tag(protocolTags.FUNCTION_NAME, this.function), + new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), + new Tag(protocolTags.VAULT_ID, this.vaultId), + new Tag(protocolTags.TIMESTAMP, JSON.stringify(Date.now())), + new Tag(protocolTags.NODE_TYPE, this.objectType), + new Tag(protocolTags.PUBLIC, this.isPublic ? "true" : "false"), + ] + if (this.groupRef) { + tags.push(new Tag(protocolTags.GROUP_REF, this.groupRef)); + } + if (this.actionRef) { + tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); + } + this.tags + ?.filter(tag => tag) + ?.map((tag: string) => + tag?.split(" ").join(",").split(".").join(",").split(",").map((value: string) => + tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) + ); + // remove duplicates + return [...new Map(tags.map(item => [item.value, item])).values()]; } protected async processWriteRaw(data: ArrayBuffer, encryptedKey?: string) { @@ -236,53 +234,21 @@ class Service { return { processedData, encryptionTags: tags } } - protected async getActiveKey() { - return { - address: await deriveAddress(this.dataEncrypter.publicKey), - publicKey: arrayToBase64(this.dataEncrypter.publicKey) - }; - } + protected async processReadRaw(data: ArrayBuffer | string, metadata: EncryptionMetadata, shouldDecrypt = true): Promise { + if (this.isPublic || !shouldDecrypt) { + return data as ArrayBuffer; + } - async processWriteString(data: string): Promise { - if (this.isPublic) return data; - let encryptedPayload: string; + const encryptedPayload = getEncryptedPayload(data, metadata); try { - encryptedPayload = await this.dataEncrypter.encryptRaw(stringToArray(data)) as string; + if (encryptedPayload) { + return this.dataEncrypter.decryptRaw(encryptedPayload, false); + } else { + return this.dataEncrypter.decryptRaw(data as string); + } } catch (error) { throw new IncorrectEncryptionKey(error); } - const decodedPayload = base64ToJson(encryptedPayload) as any; - decodedPayload.publicAddress = (await this.getActiveKey()).address; - delete decodedPayload.publicKey; - return jsonToBase64(decodedPayload); - } - - protected getAvatarUri(profileDetails: ProfileDetails) { - if (profileDetails.avatarUri && profileDetails.avatarUri.length) { - const avatarUri = [...profileDetails.avatarUri].reverse().find(resourceUri => resourceUri.startsWith("s3:"))?.replace("s3:", ""); - return avatarUri !== "null" && avatarUri; - } else { - return null; - } - } - - protected async processAvatar(avatar: ArrayBuffer, cacheOnly?: boolean): Promise<{ resourceTx: string, resourceUrl: string }> { - const { processedData, encryptionTags } = await this.processWriteRaw(avatar); - return this.api.uploadFile(processedData, encryptionTags, { cacheOnly, public: false }); - } - - protected async processMemberDetails(memberDetails: { name?: string, avatar?: ArrayBuffer }, cacheOnly?: boolean) { - const processedMemberDetails = {} as ProfileDetails; - if (!this.isPublic) { - if (memberDetails.name) { - processedMemberDetails.name = await this.processWriteString(memberDetails.name); - } - if (memberDetails.avatar) { - const { resourceUrl, resourceTx } = await this.processAvatar(memberDetails.avatar, cacheOnly); - processedMemberDetails.avatarUri = [`arweave:${resourceTx}`, `s3:${resourceUrl}`]; - } - } - return new ProfileDetails(processedMemberDetails); } protected async processReadString(data: string, shouldDecrypt = true): Promise { @@ -291,18 +257,11 @@ class Service { return arrayToString(decryptedDataRaw); } - protected getEncryptedPayload(data: ArrayBuffer | string, metadata: EncryptionMetadata): EncryptedPayload { - const { encryptedKey, iv } = metadata; - if (encryptedKey && iv) { - return { - encryptedKey, - encryptedData: { - iv: base64ToArray(iv), - ciphertext: data as ArrayBuffer - } - } - } - return null; + protected async getActiveKey() { + return { + address: await deriveAddress(this.dataEncrypter.publicKey), + publicKey: arrayToBase64(this.dataEncrypter.publicKey) + }; } protected async getCurrentState(): Promise { @@ -313,11 +272,11 @@ class Service { protected async mergeAndUploadState(stateUpdates: any): Promise { const currentState = await this.getCurrentState(); - const mergedState = await this.mergeState(currentState, stateUpdates); + const mergedState = mergeState(currentState, stateUpdates); return this.uploadState(mergedState); } - protected async signData(data: any): Promise { + private async signData(data: any): Promise { const privateKeyRaw = this.wallet.signingPrivateKeyRaw(); const signature = await signString( jsonToBase64(data), @@ -325,112 +284,6 @@ class Service { ); return signature; } - - public async uploadState(state: any, cacheOnly = false): Promise { - const signature = await this.signData(state); - const tags = [ - new Tag(dataTags.DATA_TYPE, "State"), - new Tag(smartweaveTags.CONTENT_TYPE, STATE_CONTENT_TYPE), - new Tag(protocolTags.SIGNATURE, signature), - new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), - new Tag(protocolTags.VAULT_ID, this.vaultId), - new Tag(protocolTags.NODE_TYPE, this.objectType), - ] - if (this.objectType === objectType.MEMBERSHIP) { - tags.push(new Tag(protocolTags.MEMBERSHIP_ID, this.objectId)) - } else if (this.objectType !== objectType.VAULT) { - tags.push(new Tag(protocolTags.NODE_ID, this.objectId)) - } - const ids = await this.api.uploadData([{ data: state, tags }], { cacheOnly }); - return ids[0]; - } - - protected async mergeState(currentState: any, stateUpdates: any) { - let newState = lodash.cloneDeepWith(currentState); - lodash.mergeWith( - newState, - stateUpdates, - function concatArrays(objValue, srcValue) { - if (lodash.isArray(objValue)) { - return objValue.concat(srcValue); - } - }); - return newState; - } - - async getTxTags(): Promise { - const tags = [ - new Tag(protocolTags.FUNCTION_NAME, this.function), - new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), - new Tag(protocolTags.VAULT_ID, this.vaultId), - new Tag(protocolTags.TIMESTAMP, JSON.stringify(Date.now())), - new Tag(protocolTags.NODE_TYPE, this.objectType), - new Tag(protocolTags.PUBLIC, this.isPublic ? "true" : "false"), - ] - if (this.groupRef) { - tags.push(new Tag(protocolTags.GROUP_REF, this.groupRef)); - } - if (this.actionRef) { - tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); - } - this.tags - ?.filter(tag => tag) - ?.map((tag: string) => - tag?.split(" ").join(",").split(".").join(",").split(",").map((value: string) => - tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) - ); - // remove duplicates - return [...new Map(tags.map(item => [item.value, item])).values()]; - } - - protected async handleListErrors(originalItems: Array, promises: Array>) - : Promise<{ items: Array, errors: Array<{ id: string, error: Error }> }> { - const results = await Promise.all(promises.map(p => p.catch(e => e))); - const items = results.filter(result => !(result instanceof Error)); - const errors = results - .map((result, index) => ({ result, index })) - .filter((mapped) => mapped.result instanceof Error) - .map((filtered) => ({ id: (originalItems[filtered.index]).id, error: filtered.result })); - return { items, errors }; - } - - protected async paginate(apiCall: any, listOptions: ListOptions & { vaultId?: string }): Promise> { - let token = undefined; - let results = [] as T[]; - do { - const { items, nextToken } = await apiCall(listOptions); - results = results.concat(items); - token = nextToken; - listOptions.nextToken = nextToken; - if (nextToken === "null") { - token = undefined; - } - } while (token); - return results; - } - - protected async rotateMemberKeys(publicKeys: Map): Promise<{ - memberKeys: Map, - keyPair: Keys - }> { - const memberKeys = new Map(); - // generate a new vault key pair - const keyPair = await generateKeyPair(); - - for (let [memberId, publicKey] of publicKeys) { - const memberKeysEncrypter = new Encrypter( - this.wallet, - this.dataEncrypter.keys, - base64ToArray(publicKey) - ); - try { - memberKeys.set(memberId, [await memberKeysEncrypter.encryptMemberKey(keyPair)]); - } catch (error) { - throw new IncorrectEncryptionKey(error); - } - } - return { memberKeys, keyPair }; - } } export { diff --git a/src/core/vault.ts b/src/core/vault.ts index 7ab417cb..af25ba14 100644 --- a/src/core/vault.ts +++ b/src/core/vault.ts @@ -11,6 +11,8 @@ import { MembershipService } from "./membership"; import lodash from "lodash"; import { NotFound } from "../errors/not-found"; import { BadRequest } from "../errors/bad-request"; +import { handleListErrors, paginate } from "./common"; +import { ProfileService } from "./profile"; class VaultService extends Service { objectType = objectType.VAULT; @@ -61,7 +63,7 @@ class VaultService extends Service { .map(async (vaultProto: Vault) => { return await this.processVault(vaultProto, listOptions.shouldDecrypt, vaultProto.keys); }) as Promise[]; - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -77,7 +79,7 @@ class VaultService extends Service { const list = async (listOptions: ListOptions) => { return await this.list(listOptions); } - return await this.paginate(list, options); + return await paginate(list, options); } /** @@ -98,7 +100,9 @@ class VaultService extends Service { vaultId = await this.api.initContractId([new Tag(protocolTags.NODE_TYPE, objectType.VAULT)]); } - const memberDetails = await this.getProfileDetails(); + const profileService = new ProfileService(this.wallet, this.api); + const memberDetails = await profileService.get(); + const service = new VaultService(this.wallet, this.api); service.setActionRef(actionRefs.VAULT_CREATE); service.setIsPublic(createOptions.public); @@ -116,12 +120,20 @@ class VaultService extends Service { ].concat(await service.getTxTags()); createOptions.arweaveTags?.map((tag: Tag) => service.arweaveTags.push(tag)); + const memberService = new MembershipService(this.wallet, this.api, service); + memberService.setVaultId(service.vaultId); + memberService.setObjectId(membershipId); + let keys: EncryptedKeys[]; if (!service.isPublic) { - const { memberKeys, keyPair } = await service.rotateMemberKeys(new Map([[membershipId, this.wallet.publicKey()]])); + const { memberKeys, keyPair } = await memberService.rotateMemberKeys( + new Map([[membershipId, this.wallet.publicKey()]]) + ); keys = memberKeys.get(membershipId); service.setRawDataEncryptionPublicKey(keyPair.publicKey); service.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); + memberService.setRawDataEncryptionPublicKey(keyPair.publicKey); + memberService.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); } const vaultState = { @@ -134,12 +146,10 @@ class VaultService extends Service { const memberState = { keys, - encPublicSigningKey: await service.processWriteString(this.wallet.signingPublicKey()), - memberDetails: await service.processMemberDetails(memberDetails, createOptions.cacheOnly) + encPublicSigningKey: await memberService.processWriteString(this.wallet.signingPublicKey()), + memberDetails: await memberService.processMemberDetails(memberDetails, createOptions.cacheOnly) } - const memberService = new MembershipService(this.wallet, this.api); - memberService.setVaultId(service.vaultId); - memberService.setObjectId(membershipId); + const memberStateTx = await memberService.uploadState(memberState, createOptions.cacheOnly); const data = { vault: vaultStateTx, membership: memberStateTx }; From 97050d43672054e15868c1924da2c67c7963c880 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Tue, 22 Aug 2023 11:49:47 +0000 Subject: [PATCH 13/13] ci: version bump to v4.18.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e48ac624..d98b0cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@akord/akord-js", - "version": "4.18.0", + "version": "4.18.1", "description": "A set of core js functions to interact with Akord", "main": "lib/index.js", "repository": "git@github.com:Akord-com/akord-js.git",