From f0130723b6e2b0d654edadb8bf73619eea8ffab2 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 17 Jan 2022 16:57:19 +0800 Subject: [PATCH 01/18] feat: create web and mobile db methods --- .../mask/background/database/persona/app.ts | 192 +++++ .../database/persona/consistency.ts | 2 +- .../mask/background/database/persona/db.ts | 816 +----------------- .../mask/background/database/persona/type.ts | 135 +++ .../mask/background/database/persona/web.ts | 684 +++++++++++++++ .../background-script/IdentityService.ts | 6 +- 6 files changed, 1047 insertions(+), 788 deletions(-) create mode 100644 packages/mask/background/database/persona/app.ts create mode 100644 packages/mask/background/database/persona/type.ts create mode 100644 packages/mask/background/database/persona/web.ts diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts new file mode 100644 index 000000000000..e7190ae7740d --- /dev/null +++ b/packages/mask/background/database/persona/app.ts @@ -0,0 +1,192 @@ +import type { + PersonaRecord, + FullPersonaDBTransaction, + PersonasTransaction, + ProfileRecordDB, + PersonaRecordWithPrivateKey, + ProfileRecord, + ProfileTransaction, + RelationRecord, + RelationTransaction, +} from './type' +import type { ProfileIdentifier, PersonaIdentifier } from '@masknet/shared-base' + +export async function createPersonaDB(record: PersonaRecord): Promise { + return +} + +export async function createPersonaByProfileDB( + query: ProfileIdentifier, + t?: FullPersonaDBTransaction<'readonly'>, +): Promise { + return null +} + +export async function queryPersonaDB( + query: PersonaIdentifier, + t?: PersonasTransaction<'readonly'>, + isIncludeLogout?: boolean, +): Promise { + return null +} + +export async function queryPersonasWithPrivateKey(): Promise { + return [] +} + +/** + * Update an existing Persona record. + * @param nextRecord The partial record to be merged + * @param howToMerge How to merge linkedProfiles and `field: undefined` + * @param t transaction + */ + +export async function updatePersonaDB( // Do a copy here. We need to delete keys from it. + { ...nextRecord }: Readonly & Pick>, + howToMerge: { + linkedProfiles: 'replace' | 'merge' + explicitUndefinedField: 'ignore' | 'delete field' + }, + t: PersonasTransaction<'readwrite'>, +): Promise { + return +} + +export async function createOrUpdatePersonaDB( + record: Partial & Pick, + howToMerge: Parameters[1] & { protectPrivateKey?: boolean }, + t: PersonasTransaction<'readwrite'>, +): Promise {} + +/** + * Delete a Persona + */ +export async function deletePersonaDB( + id: PersonaIdentifier, + confirm: 'delete even with private' | "don't delete if have private key", + t: PersonasTransaction<'readwrite'>, +): Promise {} + +/** + * Delete a Persona + * @returns a boolean. true: the record no longer exists; false: the record is kept. + */ +export async function safeDeletePersonaDB( + id: PersonaIdentifier, + t?: FullPersonaDBTransaction<'readwrite'>, +): Promise {} + +/** + * Create a new profile. + */ +export async function createProfileDB(record: ProfileRecordDB, t: ProfileTransaction<'readwrite'>): Promise {} + +/** + * Query a profile. + */ +export async function queryProfileDB( + id: ProfileIdentifier, + t?: ProfileTransaction<'readonly'>, +): Promise { + return null +} + +/** + * Query many profiles. + */ +export async function queryProfilesDB( + network: string | ((record: ProfileRecord) => boolean), + t?: ProfileTransaction<'readonly'>, +): Promise { + return [] +} + +/** + * query profiles with paged + * @param options + * @param count + */ +export async function queryProfilesPagedDB( + options: { + after?: ProfileIdentifier + query?: string + }, + count: number, +): Promise { + return [] +} + +/** + * Update a profile. + */ +export async function updateProfileDB( + updating: Partial & Pick, + t: ProfileTransaction<'readwrite'>, +): Promise { + return +} + +export async function createOrUpdateProfileDB(rec: ProfileRecord, t: ProfileTransaction<'readwrite'>): Promise {} + +/** + * Detach a profile from it's linking persona. + * @param identifier The profile want to detach + * @param t A living transaction + */ +export async function detachProfileDB( + identifier: ProfileIdentifier, + t?: FullPersonaDBTransaction<'readwrite'>, +): Promise {} + +/** + * attach a profile. + */ +export async function attachProfileDB(): Promise { + return +} + +/** + * Delete a profile + */ +export async function deleteProfileDB(id: ProfileIdentifier, t: ProfileTransaction<'readwrite'>): Promise {} + +/** + * Create a new Relation + */ +export async function createRelationDB( + record: Omit, + t: RelationTransaction<'readwrite'>, + silent = false, +): Promise { + return +} + +export async function queryRelations( + query: (record: RelationRecord) => boolean, + t?: RelationTransaction<'readonly'>, +): Promise { + return [] +} + +export async function queryRelationsPagedDB( + linked: PersonaIdentifier, + options: { + network: string + after?: RelationRecord + }, + count: number, +): Promise { + return [] +} + +/** + * Update a relation + * @param updating + * @param t + * @param silent + */ +export async function updateRelationDB( + updating: Omit, + t: RelationTransaction<'readwrite'>, + silent = false, +): Promise {} diff --git a/packages/mask/background/database/persona/consistency.ts b/packages/mask/background/database/persona/consistency.ts index e23525a8bfed..0f0200bcad10 100644 --- a/packages/mask/background/database/persona/consistency.ts +++ b/packages/mask/background/database/persona/consistency.ts @@ -7,7 +7,7 @@ import { ECKeyIdentifier, } from '@masknet/shared-base' import { restorePrototype } from '../../../utils-pure' -import type { FullPersonaDBTransaction } from './db' +import type { FullPersonaDBTransaction } from './type' type ReadwriteFullPersonaDBTransaction = FullPersonaDBTransaction<'readwrite'> diff --git a/packages/mask/background/database/persona/db.ts b/packages/mask/background/database/persona/db.ts index 7a3c8c3b2db8..e78b3e0ce28a 100644 --- a/packages/mask/background/database/persona/db.ts +++ b/packages/mask/background/database/persona/db.ts @@ -1,789 +1,37 @@ -import Fuse from 'fuse.js' -import { DBSchema, openDB } from 'idb/with-async-ittr' -import { CryptoKeyToJsonWebKey, PrototypeLess, restorePrototype } from '../../../utils-pure' -import { createDBAccessWithAsyncUpgrade, createTransaction, IDBPSafeTransaction } from '../utils/openDB' -import { assertPersonaDBConsistency } from './consistency' -import { - AESJsonWebKey, - ECKeyIdentifier, - EC_Private_JsonWebKey, - EC_Public_JsonWebKey, - Identifier, - IdentifierMap, - PersonaIdentifier, - ProfileIdentifier, - RelationFavor, -} from '@masknet/shared-base' -import { MaskMessages } from '../../../shared' - -/** - * Database structure: - * - * # ObjectStore `persona`: - * @description Store Personas. - * @type {PersonaRecordDB} - * @keys inline, {@link PersonaRecordDb.identifier} - * - * # ObjectStore `profiles`: - * @description Store profiles. - * @type {ProfileRecord} - * A persona links to 0 or more profiles. - * Each profile links to 0 or 1 persona. - * @keys inline, {@link ProfileRecord.identifier} - * - * # ObjectStore `relations`: - * @description Store relations. - * @type {RelationRecord} - * Save the relationship between persona and profile. - * @keys inline {@link RelationRecord.linked @link RelationRecord.profile} - */ - -const db = createDBAccessWithAsyncUpgrade( - 1, - 4, - (currentOpenVersion, knowledge) => { - return openDB('maskbook-persona', currentOpenVersion, { - upgrade(db, oldVersion, newVersion, transaction) { - function v0_v1() { - db.createObjectStore('personas', { keyPath: 'identifier' }) - db.createObjectStore('profiles', { keyPath: 'identifier' }) - transaction.objectStore('profiles').createIndex('network', 'network', { unique: false }) - transaction.objectStore('personas').createIndex('hasPrivateKey', 'hasPrivateKey', { unique: false }) - } - async function v1_v2() { - const persona = transaction.objectStore('personas') - const profile = transaction.objectStore('profiles') - await update(persona) - await update(profile) - async function update(q: typeof persona | typeof profile) { - for await (const rec of persona) { - if (!rec.value.localKey) continue - const jwk = knowledge?.data.get(rec.value.identifier) - if (!jwk) { - // !!! This should not happen - // !!! Remove it will implicitly drop user's localKey - delete rec.value.localKey - // !!! Keep it will leave a bug, broken data in the DB - // continue - // !!! DON'T throw cause it will break the database upgrade - } - rec.value.localKey = jwk - await rec.update(rec.value) - } - } - } - async function v2_v3() { - try { - db.createObjectStore('relations', { keyPath: ['linked', 'profile'] }) - transaction - .objectStore('relations') - .createIndex('linked, profile, favor', ['linked', 'profile', 'favor'], { unique: true }) - } catch {} - } - async function v3_v4() { - try { - transaction.objectStore('relations').deleteIndex('linked, profile, favor') - transaction - .objectStore('relations') - .createIndex('favor, profile, linked', ['favor', 'profile', 'linked'], { unique: true }) - const relation = transaction.objectStore('relations') - - await update(relation) - async function update(q: typeof relation) { - for await (const rec of relation) { - rec.value.favor = - rec.value.favor === RelationFavor.DEPRECATED - ? RelationFavor.UNCOLLECTED - : RelationFavor.COLLECTED - - await rec.update(rec.value) - } - } - } catch {} - } - if (oldVersion < 1) v0_v1() - if (oldVersion < 2) v1_v2() - if (oldVersion < 3) v2_v3() - if (oldVersion < 4) v3_v4() - }, - }) - }, - async (db) => { - if (db.version === 1) { - const map: V1To2 = { version: 2, data: new Map() } - const t = createTransaction(db, 'readonly')('personas', 'profiles') - const a = await t.objectStore('personas').getAll() - const b = await t.objectStore('profiles').getAll() - for (const rec of [...a, ...b]) { - if (!rec.localKey) continue - map.data.set(rec.identifier, (await CryptoKeyToJsonWebKey(rec.localKey as any)) as any) +export * from './type' + +export const { + queryProfilesDB, + queryProfileDB, + queryPersonaDB, + queryPersonasDB, + detachProfileDB, + deletePersonaDB, + safeDeletePersonaDB, + queryPersonaByProfileDB, + createPersonaDB, + attachProfileDB, + consistentPersonaDBWriteAccess, + updatePersonaDB, + createOrUpdatePersonaDB, + queryProfilesPagedDB, + createOrUpdateProfileDB, + createProfileDB, + createRelationDB, + createRelationsTransaction, + deleteProfileDB, + queryRelationsPagedDB, + updateRelationDB, + queryPersonasWithPrivateKey, + queryRelations, +} = new Proxy({} as any as typeof import('./web'), { + get(_, key) { + return async function () { + if (process.env.arch === 'app') { + return import('./app').then((x) => (x as any)[key]) } - return map - } - return undefined - }, -) -type V1To2 = { version: 2; data: Map } -type Knowledge = V1To2 -export type FullPersonaDBTransaction = IDBPSafeTransaction< - PersonaDB, - ['personas', 'profiles', 'relations'], - Mode -> -export type ProfileTransaction = IDBPSafeTransaction< - PersonaDB, - ['profiles'], - Mode -> -export type PersonasTransaction = IDBPSafeTransaction< - PersonaDB, - ['personas'], - Mode -> - -export type RelationTransaction = IDBPSafeTransaction< - PersonaDB, - ['relations'], - Mode -> - -export async function createRelationsTransaction() { - const database = await db() - return createTransaction(database, 'readwrite')('relations') -} - -// @deprecated Please create a transaction directly -export async function consistentPersonaDBWriteAccess( - action: (t: FullPersonaDBTransaction<'readwrite'>) => Promise, - tryToAutoFix = true, -) { - // TODO: collect all changes on this transaction then only perform consistency check on those records. - const database = await db() - let t = createTransaction(database, 'readwrite')('profiles', 'personas', 'relations') - let finished = false - const finish = () => (finished = true) - t.addEventListener('abort', finish) - t.addEventListener('complete', finish) - t.addEventListener('error', finish) - // Pause those events when patching write access - const resumeProfile = MaskMessages.events.profilesChanged.pause() - const resumePersona = MaskMessages.events.ownPersonaChanged.pause() - const resumeRelation = MaskMessages.events.relationsChanged.pause() - try { - await action(t) - } finally { - if (finished) { - console.warn('The transaction ends too early! There MUST be a bug in the program!') - console.trace() - // start a new transaction to check consistency - t = createTransaction(database, 'readwrite')('profiles', 'personas', 'relations') + return import('./web').then((x) => (x as any)[key]) } - try { - await assertPersonaDBConsistency(tryToAutoFix ? 'fix' : 'throw', 'full check', t) - resumeProfile((data) => [data.flat()]) - resumePersona((data) => (data.length ? [undefined] : [])) - resumeRelation((data) => [data.flat()]) - } finally { - // If the consistency check throws, we drop all pending events - resumeProfile(() => []) - resumePersona(() => []) - resumeRelation(() => []) - } - } -} - -export async function createReadonlyPersonaTransaction() { - return createTransaction(await db(), 'readonly') -} - -//#region Plain methods -/** Create a new Persona. */ -export async function createPersonaDB(record: PersonaRecord, t: PersonasTransaction<'readwrite'>): Promise { - await t.objectStore('personas').add(personaRecordToDB(record)) - record.privateKey && MaskMessages.events.ownPersonaChanged.sendToAll(undefined) -} - -export async function queryPersonaByProfileDB( - query: ProfileIdentifier, - t?: FullPersonaDBTransaction<'readonly'>, -): Promise { - t = t || createTransaction(await db(), 'readonly')('personas', 'profiles', 'relations') - const x = await t.objectStore('profiles').get(query.toText()) - if (!x?.linkedPersona) return null - return queryPersonaDB(restorePrototype(x.linkedPersona, ECKeyIdentifier.prototype), t) -} - -/** - * Query a Persona. - */ -export async function queryPersonaDB( - query: PersonaIdentifier, - t?: PersonasTransaction<'readonly'>, - isIncludeLogout?: boolean, -): Promise { - t = t || createTransaction(await db(), 'readonly')('personas') - const x = await t.objectStore('personas').get(query.toText()) - if (x && (isIncludeLogout || !x.hasLogout)) return personaRecordOutDB(x) - return null -} - -/** - * Query many Personas. - */ -export async function queryPersonasDB( - query: (record: PersonaRecord) => boolean, - t?: PersonasTransaction<'readonly'>, - isIncludeLogout?: boolean, -): Promise { - t = t || createTransaction(await db(), 'readonly')('personas') - const records: PersonaRecord[] = [] - for await (const each of t.objectStore('personas')) { - const out = personaRecordOutDB(each.value) - if (query(out) && (isIncludeLogout || !out.hasLogout)) records.push(out) - } - return records -} - -export type PersonaRecordWithPrivateKey = PersonaRecord & Required> -/** - * Query many Personas. - */ -export async function queryPersonasWithPrivateKey( - t?: FullPersonaDBTransaction<'readonly'>, -): Promise { - t = t || createTransaction(await db(), 'readonly')('personas', 'profiles', 'relations') - const records: PersonaRecord[] = [] - records.push( - ...(await t.objectStore('personas').index('hasPrivateKey').getAll(IDBKeyRange.only('yes'))).map( - personaRecordOutDB, - ), - ) - return records as PersonaRecordWithPrivateKey[] -} - -/** - * Update an existing Persona record. - * @param nextRecord The partial record to be merged - * @param howToMerge How to merge linkedProfiles and `field: undefined` - * @param t transaction - */ -export async function updatePersonaDB( - // Do a copy here. We need to delete keys from it. - { ...nextRecord }: Readonly & Pick>, - howToMerge: { - linkedProfiles: 'replace' | 'merge' - explicitUndefinedField: 'ignore' | 'delete field' }, - t: PersonasTransaction<'readwrite'>, -): Promise { - const _old = await t.objectStore('personas').get(nextRecord.identifier.toText()) - if (!_old) throw new TypeError('Update an non-exist data') - const old = personaRecordOutDB(_old) - let nextLinkedProfiles = old.linkedProfiles - if (nextRecord.linkedProfiles) { - if (howToMerge.linkedProfiles === 'merge') - nextLinkedProfiles = new IdentifierMap( - new Map([...nextLinkedProfiles.__raw_map__, ...nextRecord.linkedProfiles.__raw_map__]), - ) - else nextLinkedProfiles = nextRecord.linkedProfiles - } - if (howToMerge.explicitUndefinedField === 'ignore') { - for (const _key in nextRecord) { - const key = _key as keyof typeof nextRecord - if (nextRecord[key] === undefined) { - delete nextRecord[key as keyof typeof nextRecord] - } - } - } - const next: PersonaRecordDB = personaRecordToDB({ - ...old, - ...nextRecord, - linkedProfiles: nextLinkedProfiles, - updatedAt: nextRecord.updatedAt ?? new Date(), - }) - await t.objectStore('personas').put(next) - ;(next.privateKey || old.privateKey) && MaskMessages.events.ownPersonaChanged.sendToAll(undefined) -} - -export async function createOrUpdatePersonaDB( - record: Partial & Pick, - howToMerge: Parameters[1] & { protectPrivateKey?: boolean }, - t: PersonasTransaction<'readwrite'>, -) { - const personaInDB = await t.objectStore('personas').get(record.identifier.toText()) - - if (howToMerge.protectPrivateKey && !!personaInDB?.privateKey && !record.privateKey) return - - if (howToMerge.protectPrivateKey && !!personaInDB?.privateKey) { - const nextRecord = personaRecordOutDB(personaInDB) - nextRecord.hasLogout = false - - return updatePersonaDB(nextRecord, howToMerge, t) - } - - if (personaInDB) return updatePersonaDB(record, howToMerge, t) - else - return createPersonaDB( - { - ...record, - createdAt: record.createdAt ?? new Date(), - updatedAt: record.updatedAt ?? new Date(), - linkedProfiles: new IdentifierMap(new Map()), - }, - t, - ) -} - -/** - * Delete a Persona - */ -export async function deletePersonaDB( - id: PersonaIdentifier, - confirm: 'delete even with private' | "don't delete if have private key", - t: PersonasTransaction<'readwrite'>, -): Promise { - const r = await t.objectStore('personas').get(id.toText()) - if (!r) return - if (confirm !== 'delete even with private' && r.privateKey) - throw new TypeError('Cannot delete a persona with a private key') - await t.objectStore('personas').delete(id.toText()) - r.privateKey && MaskMessages.events.ownPersonaChanged.sendToAll() -} -/** - * Delete a Persona - * @returns a boolean. true: the record no longer exists; false: the record is kept. - */ -export async function safeDeletePersonaDB( - id: PersonaIdentifier, - t?: FullPersonaDBTransaction<'readwrite'>, -): Promise { - t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') - const r = await queryPersonaDB(id, t) - if (!r) return true - if (r.linkedProfiles.size !== 0) return false - if (r.privateKey) return false - await deletePersonaDB(id, "don't delete if have private key", t) - return true -} - -/** - * Create a new profile. - */ -export async function createProfileDB(record: ProfileRecord, t: ProfileTransaction<'readwrite'>): Promise { - await t.objectStore('profiles').add(profileToDB(record)) - MaskMessages.events.profilesChanged.sendToAll([{ of: record.identifier, reason: 'update' }]) -} - -/** - * Query a profile. - */ -export async function queryProfileDB( - id: ProfileIdentifier, - t?: ProfileTransaction<'readonly'>, -): Promise { - t = t || createTransaction(await db(), 'readonly')('profiles') - const result = await t.objectStore('profiles').get(id.toText()) - if (result) return profileOutDB(result) - return null -} - -/** - * Query many profiles. - */ -export async function queryProfilesDB( - network: string | ((record: ProfileRecord) => boolean), - t?: ProfileTransaction<'readonly'>, -): Promise { - t = t || createTransaction(await db(), 'readonly')('profiles') - const result: ProfileRecord[] = [] - if (typeof network === 'string') { - result.push( - ...(await t.objectStore('profiles').index('network').getAll(IDBKeyRange.only(network))).map(profileOutDB), - ) - } else { - for await (const each of t.objectStore('profiles').iterate()) { - const out = profileOutDB(each.value) - if (network(out)) result.push(out) - } - } - return result -} - -const fuse = new Fuse([] as ProfileRecord[], { - shouldSort: true, - threshold: 0.45, - minMatchCharLength: 1, - keys: [ - { name: 'nickname', weight: 0.8 }, - { name: 'identifier.network', weight: 0.2 }, - ], }) - -export async function queryProfilesPagedDB( - options: { - after?: ProfileIdentifier - query?: string - }, - count: number, -): Promise { - const t = createTransaction(await db(), 'readonly')('profiles') - const breakPoint = options.after?.toText() - let firstRecord = true - const data: ProfileRecord[] = [] - for await (const rec of t.objectStore('profiles').iterate()) { - if (firstRecord && breakPoint && rec.key !== breakPoint) { - rec.continue(breakPoint) - firstRecord = false - continue - } - firstRecord = false - // after this record - if (rec.key === breakPoint) continue - if (count <= 0) break - const outData = profileOutDB(rec.value) - if (typeof options.query === 'string') { - fuse.setCollection([outData]) - if (!fuse.search(options.query).length) continue - } - count -= 1 - data.push(outData) - } - return data -} - -/** - * Update a profile. - */ -export async function updateProfileDB( - updating: Partial & Pick, - t: ProfileTransaction<'readwrite'>, -): Promise { - const old = await t.objectStore('profiles').get(updating.identifier.toText()) - if (!old) throw new Error('Updating a non exists record') - - const nextRecord: ProfileRecordDB = profileToDB({ - ...profileOutDB(old), - ...updating, - }) - await t.objectStore('profiles').put(nextRecord) - MaskMessages.events.profilesChanged.sendToAll([{ reason: 'update', of: updating.identifier }]) -} -export async function createOrUpdateProfileDB(rec: ProfileRecord, t: ProfileTransaction<'readwrite'>) { - if (await queryProfileDB(rec.identifier, t)) return updateProfileDB(rec, t) - else return createProfileDB(rec, t) -} - -/** - * Detach a profile from it's linking persona. - * @param identifier The profile want to detach - * @param t A living transaction - */ -export async function detachProfileDB( - identifier: ProfileIdentifier, - t?: FullPersonaDBTransaction<'readwrite'>, -): Promise { - t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') - const profile = await queryProfileDB(identifier, t) - if (!profile?.linkedPersona) return - - const linkedPersona = profile.linkedPersona - const persona = await queryPersonaDB(linkedPersona, t) - persona?.linkedProfiles.delete(identifier) - - if (persona) { - await updatePersonaDB(persona, { linkedProfiles: 'replace', explicitUndefinedField: 'delete field' }, t) - if (persona.privateKey) MaskMessages.events.ownPersonaChanged.sendToAll(undefined) - } - profile.linkedPersona = undefined - await updateProfileDB(profile, t) -} - -/** - * attach a profile. - */ -export async function attachProfileDB( - identifier: ProfileIdentifier, - attachTo: PersonaIdentifier, - data: LinkedProfileDetails, - t?: FullPersonaDBTransaction<'readwrite'>, -): Promise { - t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') - const profile = - (await queryProfileDB(identifier, t)) || - (await createProfileDB({ identifier, createdAt: new Date(), updatedAt: new Date() }, t)) || - (await queryProfileDB(identifier, t)) - const persona = await queryPersonaDB(attachTo, t) - if (!persona || !profile) return - - if (profile.linkedPersona !== undefined && !profile.linkedPersona.equals(attachTo)) { - await detachProfileDB(identifier, t) - } - - profile.linkedPersona = attachTo - persona.linkedProfiles.set(identifier, data) - - await updatePersonaDB(persona, { linkedProfiles: 'merge', explicitUndefinedField: 'ignore' }, t) - await updateProfileDB(profile, t) - - if (persona.privateKey) MaskMessages.events.ownPersonaChanged.sendToAll(undefined) -} - -/** - * Delete a profile - */ -export async function deleteProfileDB(id: ProfileIdentifier, t: ProfileTransaction<'readwrite'>): Promise { - await t.objectStore('profiles').delete(id.toText()) - MaskMessages.events.profilesChanged.sendToAll([{ reason: 'delete', of: id }]) -} - -/** - * Create a new Relation - */ -export async function createRelationDB( - record: Omit, - t: RelationTransaction<'readwrite'>, - silent = false, -): Promise { - await t.objectStore('relations').add(relationRecordToDB(record)) - if (!silent) - MaskMessages.events.relationsChanged.sendToAll([{ of: record.profile, reason: 'update', favor: record.favor }]) -} - -export async function queryRelations(query: (record: RelationRecord) => boolean, t?: RelationTransaction<'readonly'>) { - t = t || createTransaction(await db(), 'readonly')('relations') - const records: RelationRecord[] = [] - - for await (const each of t.objectStore('relations')) { - const out = relationRecordOutDB(each.value) - if (query(out)) records.push(out) - } - - return records -} - -/** - * Query relations by paged - */ -export async function queryRelationsPagedDB( - linked: PersonaIdentifier, - options: { - network: string - after?: RelationRecord - }, - count: number, -) { - const t = createTransaction(await db(), 'readonly')('relations') - let firstRecord = true - - const data: RelationRecord[] = [] - - for await (const cursor of t.objectStore('relations').index('favor, profile, linked').iterate()) { - if (cursor.value.linked !== linked.toText()) continue - if (cursor.value.network !== options.network) continue - - if (firstRecord && options.after && options.after.profile.toText() !== cursor?.value.profile) { - cursor.continue([options.after.favor, options.after.profile.toText(), options.after.linked.toText()]) - firstRecord = false - continue - } - - firstRecord = false - - //after this record - if ( - options.after?.linked.toText() === cursor?.value.linked && - options.after?.profile.toText() === cursor?.value.profile - ) - continue - - if (count <= 0) break - const outData = relationRecordOutDB(cursor.value) - count -= 1 - data.push(outData) - } - return data -} - -/** - * Update a relation - * @param updating - * @param t - * @param silent - */ -export async function updateRelationDB( - updating: Omit, - t: RelationTransaction<'readwrite'>, - silent = false, -): Promise { - const old = await t - .objectStore('relations') - .get(IDBKeyRange.only([updating.linked.toText(), updating.profile.toText()])) - - if (!old) throw new Error('Updating a non exists record') - - const nextRecord: RelationRecordDB = relationRecordToDB({ - ...relationRecordOutDB(old), - ...updating, - }) - - await t.objectStore('relations').put(nextRecord) - if (!silent) { - MaskMessages.events.relationsChanged.sendToAll([ - { of: updating.profile, favor: updating.favor, reason: 'update' }, - ]) - } -} - -//#endregion - -//#region Type -export interface ProfileRecord { - identifier: ProfileIdentifier - nickname?: string - localKey?: AESJsonWebKey - linkedPersona?: PersonaIdentifier - createdAt: Date - updatedAt: Date -} - -export interface LinkedProfileDetails { - connectionConfirmState: 'confirmed' | 'pending' | 'denied' -} - -export interface PersonaRecord { - identifier: PersonaIdentifier - /** - * If this key is generated by the mnemonic word, this field should be set. - */ - mnemonic?: { - words: string - parameter: { path: string; withPassword: boolean } - } - publicKey: EC_Public_JsonWebKey - privateKey?: EC_Private_JsonWebKey - localKey?: AESJsonWebKey - nickname?: string - linkedProfiles: IdentifierMap - createdAt: Date - updatedAt: Date - hasLogout?: boolean - /** - * create a dummy persona which should hide to the user until - * connected at least one SNS identity - */ - uninitialized?: boolean -} - -export interface RelationRecord { - profile: ProfileIdentifier - linked: PersonaIdentifier - network: string - favor: RelationFavor -} - -type ProfileRecordDB = Omit & { - identifier: string - network: string - linkedPersona?: PrototypeLess -} - -type PersonaRecordDB = Omit & { - identifier: string - linkedProfiles: Map - /** - * This field is used as index of the db. - */ - hasPrivateKey: 'no' | 'yes' -} -type RelationRecordDB = Omit & { - network: string - profile: string - linked: string -} - -export interface PersonaDB extends DBSchema { - /** Use inline keys */ - personas: { - value: PersonaRecordDB - key: string - indexes: { - hasPrivateKey: string - } - } - /** Use inline keys */ - profiles: { - value: ProfileRecordDB - key: string - indexes: { - // Use `network` field as index - network: string - } - } - /** Use inline keys **/ - relations: { - key: IDBValidKey[] - value: RelationRecordDB - indexes: { - 'linked, profile, favor': [string, string, number] - 'favor, profile, linked': [number, string, string] - } - } -} -//#endregion - -//#region out db & to db -function profileToDB(x: ProfileRecord): ProfileRecordDB { - return { - ...x, - identifier: x.identifier.toText(), - network: x.identifier.network, - } -} -function profileOutDB({ network, ...x }: ProfileRecordDB): ProfileRecord { - if (x.linkedPersona) { - if (x.linkedPersona.type !== 'ec_key') throw new Error('Unknown type of linkedPersona') - } - return { - ...x, - identifier: Identifier.fromString(x.identifier, ProfileIdentifier).unwrap(), - linkedPersona: restorePrototype(x.linkedPersona, ECKeyIdentifier.prototype), - } -} -function personaRecordToDB(x: PersonaRecord): PersonaRecordDB { - return { - ...x, - identifier: x.identifier.toText(), - hasPrivateKey: x.privateKey ? 'yes' : 'no', - linkedProfiles: x.linkedProfiles.__raw_map__, - } -} -function personaRecordOutDB(x: PersonaRecordDB): PersonaRecord { - // @ts-ignore - delete x.hasPrivateKey - const obj: PersonaRecord = { - ...x, - identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), - linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), - } - return obj -} - -function relationRecordToDB(x: Omit): RelationRecordDB { - return { - ...x, - network: x.profile.network, - profile: x.profile.toText(), - linked: x.linked.toText(), - } -} - -function relationRecordOutDB(x: RelationRecordDB): RelationRecord { - return { - ...x, - profile: Identifier.fromString(x.profile, ProfileIdentifier).unwrap(), - linked: Identifier.fromString(x.linked, ECKeyIdentifier).unwrap(), - } -} - -//#endregion diff --git a/packages/mask/background/database/persona/type.ts b/packages/mask/background/database/persona/type.ts new file mode 100644 index 000000000000..362c86083e9e --- /dev/null +++ b/packages/mask/background/database/persona/type.ts @@ -0,0 +1,135 @@ +import type { IDBPSafeTransaction } from '../utils/openDB' +import type { DBSchema } from 'idb/with-async-ittr' +import type { PrototypeLess } from '../../../utils-pure' +import type { + PersonaIdentifier, + AESJsonWebKey, + EC_Private_JsonWebKey, + EC_Public_JsonWebKey, + IdentifierMap, + ProfileIdentifier, + RelationFavor, +} from '@masknet/shared-base' + +export type FullPersonaDBTransaction = IDBPSafeTransaction< + PersonaDB, + ['personas', 'profiles', 'relations'], + Mode +> + +export type ProfileTransaction = IDBPSafeTransaction< + PersonaDB, + ['profiles'], + Mode +> + +export type PersonasTransaction = IDBPSafeTransaction< + PersonaDB, + ['personas'], + Mode +> + +export type RelationTransaction = IDBPSafeTransaction< + PersonaDB, + ['relations'], + Mode +> + +//#region Type +export type PersonaRecordDB = Omit & { + identifier: string + linkedProfiles: Map + /** + * This field is used as index of the db. + */ + hasPrivateKey: 'no' | 'yes' +} + +export type ProfileRecordDB = Omit & { + identifier: string + network: string + linkedPersona?: PrototypeLess +} + +export type RelationRecordDB = Omit & { + network: string + profile: string + linked: string +} + +export interface PersonaDB extends DBSchema { + /** Use inline keys */ + personas: { + value: PersonaRecordDB + key: string + indexes: { + hasPrivateKey: string + } + } + /** Use inline keys */ + profiles: { + value: ProfileRecordDB + key: string + indexes: { + // Use `network` field as index + network: string + } + } + /** Use inline keys **/ + relations: { + key: IDBValidKey[] + value: RelationRecordDB + indexes: { + 'linked, profile, favor': [string, string, number] + 'favor, profile, linked': [number, string, string] + } + } +} + +export interface RelationRecord { + profile: ProfileIdentifier + linked: PersonaIdentifier + network: string + favor: RelationFavor +} + +export interface ProfileRecord { + identifier: ProfileIdentifier + nickname?: string + localKey?: AESJsonWebKey + linkedPersona?: PersonaIdentifier + createdAt: Date + updatedAt: Date +} + +export interface PersonaRecord { + identifier: PersonaIdentifier + /** + * If this key is generated by the mnemonic word, this field should be set. + */ + mnemonic?: { + words: string + parameter: { path: string; withPassword: boolean } + } + publicKey: EC_Public_JsonWebKey + privateKey?: EC_Private_JsonWebKey + localKey?: AESJsonWebKey + nickname?: string + linkedProfiles: IdentifierMap + createdAt: Date + updatedAt: Date + hasLogout?: boolean + /** + * create a dummy persona which should hide to the user until + * connected at least one SNS identity + */ + uninitialized?: boolean +} + +export interface LinkedProfileDetails { + connectionConfirmState: 'confirmed' | 'pending' | 'denied' +} + +export type PersonaRecordWithPrivateKey = PersonaRecord & Required> + +//#endregion diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts new file mode 100644 index 000000000000..fa6515f0cf98 --- /dev/null +++ b/packages/mask/background/database/persona/web.ts @@ -0,0 +1,684 @@ +import Fuse from 'fuse.js' +import { openDB } from 'idb/with-async-ittr' +import { CryptoKeyToJsonWebKey, restorePrototype } from '../../../utils-pure' +import { createDBAccessWithAsyncUpgrade, createTransaction } from '../utils/openDB' +import { assertPersonaDBConsistency } from './consistency' +import { + AESJsonWebKey, + ECKeyIdentifier, + Identifier, + IdentifierMap, + PersonaIdentifier, + ProfileIdentifier, + RelationFavor, +} from '@masknet/shared-base' +import { MaskMessages } from '../../../shared' +import type { + FullPersonaDBTransaction, + ProfileTransaction, + PersonasTransaction, + RelationTransaction, + PersonaRecordWithPrivateKey, + PersonaDB, + PersonaRecordDB, + ProfileRecord, + ProfileRecordDB, + LinkedProfileDetails, + RelationRecord, + RelationRecordDB, + PersonaRecord, +} from './type' +/** + * Database structure: + * + * # ObjectStore `persona`: + * @description Store Personas. + * @type {PersonaRecordDB} + * @keys inline, {@link PersonaRecordDb.identifier} + * + * # ObjectStore `profiles`: + * @description Store profiles. + * @type {ProfileRecord} + * A persona links to 0 or more profiles. + * Each profile links to 0 or 1 persona. + * @keys inline, {@link ProfileRecord.identifier} + * + * # ObjectStore `relations`: + * @description Store relations. + * @type {RelationRecord} + * Save the relationship between persona and profile. + * @keys inline {@link RelationRecord.linked @link RelationRecord.profile} + */ + +const db = createDBAccessWithAsyncUpgrade( + 1, + 4, + (currentOpenVersion, knowledge) => { + return openDB('maskbook-persona', currentOpenVersion, { + upgrade(db, oldVersion, newVersion, transaction) { + function v0_v1() { + db.createObjectStore('personas', { keyPath: 'identifier' }) + db.createObjectStore('profiles', { keyPath: 'identifier' }) + transaction.objectStore('profiles').createIndex('network', 'network', { unique: false }) + transaction.objectStore('personas').createIndex('hasPrivateKey', 'hasPrivateKey', { unique: false }) + } + async function v1_v2() { + const persona = transaction.objectStore('personas') + const profile = transaction.objectStore('profiles') + await update(persona) + await update(profile) + async function update(q: typeof persona | typeof profile) { + for await (const rec of persona) { + if (!rec.value.localKey) continue + const jwk = knowledge?.data.get(rec.value.identifier) + if (!jwk) { + // !!! This should not happen + // !!! Remove it will implicitly drop user's localKey + delete rec.value.localKey + // !!! Keep it will leave a bug, broken data in the DB + // continue + // !!! DON'T throw cause it will break the database upgrade + } + rec.value.localKey = jwk + await rec.update(rec.value) + } + } + } + async function v2_v3() { + try { + db.createObjectStore('relations', { keyPath: ['linked', 'profile'] }) + transaction + .objectStore('relations') + .createIndex('linked, profile, favor', ['linked', 'profile', 'favor'], { unique: true }) + } catch {} + } + async function v3_v4() { + try { + transaction.objectStore('relations').deleteIndex('linked, profile, favor') + transaction + .objectStore('relations') + .createIndex('favor, profile, linked', ['favor', 'profile', 'linked'], { unique: true }) + const relation = transaction.objectStore('relations') + + await update(relation) + async function update(q: typeof relation) { + for await (const rec of relation) { + rec.value.favor = + rec.value.favor === RelationFavor.DEPRECATED + ? RelationFavor.UNCOLLECTED + : RelationFavor.COLLECTED + + await rec.update(rec.value) + } + } + } catch {} + } + if (oldVersion < 1) v0_v1() + if (oldVersion < 2) v1_v2() + if (oldVersion < 3) v2_v3() + if (oldVersion < 4) v3_v4() + }, + }) + }, + async (db) => { + if (db.version === 1) { + const map: V1To2 = { version: 2, data: new Map() } + const t = createTransaction(db, 'readonly')('personas', 'profiles') + const a = await t.objectStore('personas').getAll() + const b = await t.objectStore('profiles').getAll() + for (const rec of [...a, ...b]) { + if (!rec.localKey) continue + map.data.set(rec.identifier, (await CryptoKeyToJsonWebKey(rec.localKey as any)) as any) + } + return map + } + return undefined + }, +) +type V1To2 = { version: 2; data: Map } +type Knowledge = V1To2 + +export async function createRelationsTransaction() { + const database = await db() + return createTransaction(database, 'readwrite')('relations') +} + +// @deprecated Please create a transaction directly +export async function consistentPersonaDBWriteAccess( + action: (t: FullPersonaDBTransaction<'readwrite'>) => Promise, + tryToAutoFix = true, +) { + // TODO: collect all changes on this transaction then only perform consistency check on those records. + const database = await db() + let t = createTransaction(database, 'readwrite')('profiles', 'personas', 'relations') + let finished = false + const finish = () => (finished = true) + t.addEventListener('abort', finish) + t.addEventListener('complete', finish) + t.addEventListener('error', finish) + + // Pause those events when patching write access + const resumeProfile = MaskMessages.events.profilesChanged.pause() + const resumePersona = MaskMessages.events.ownPersonaChanged.pause() + const resumeRelation = MaskMessages.events.relationsChanged.pause() + try { + await action(t) + } finally { + if (finished) { + console.warn('The transaction ends too early! There MUST be a bug in the program!') + console.trace() + // start a new transaction to check consistency + t = createTransaction(database, 'readwrite')('profiles', 'personas', 'relations') + } + try { + await assertPersonaDBConsistency(tryToAutoFix ? 'fix' : 'throw', 'full check', t) + resumeProfile((data) => [data.flat()]) + resumePersona((data) => (data.length ? [undefined] : [])) + resumeRelation((data) => [data.flat()]) + } finally { + // If the consistency check throws, we drop all pending events + resumeProfile(() => []) + resumePersona(() => []) + resumeRelation(() => []) + } + } +} + +export async function createReadonlyPersonaTransaction() { + return createTransaction(await db(), 'readonly') +} + +//#region Plain methods +/** Create a new Persona. */ +export async function createPersonaDB(record: PersonaRecord, t: PersonasTransaction<'readwrite'>): Promise { + await t.objectStore('personas').add(personaRecordToDB(record)) + record.privateKey && MaskMessages.events.ownPersonaChanged.sendToAll(undefined) +} + +export async function queryPersonaByProfileDB( + query: ProfileIdentifier, + t?: FullPersonaDBTransaction<'readonly'>, +): Promise { + t = t || createTransaction(await db(), 'readonly')('personas', 'profiles', 'relations') + const x = await t.objectStore('profiles').get(query.toText()) + if (!x?.linkedPersona) return null + return queryPersonaDB(restorePrototype(x.linkedPersona, ECKeyIdentifier.prototype), t) +} + +/** + * Query a Persona. + */ +export async function queryPersonaDB( + query: PersonaIdentifier, + t?: PersonasTransaction<'readonly'>, + isIncludeLogout?: boolean, +): Promise { + t = t || createTransaction(await db(), 'readonly')('personas') + const x = await t.objectStore('personas').get(query.toText()) + if (x && (isIncludeLogout || !x.hasLogout)) return personaRecordOutDB(x) + return null +} + +/** + * Query many Personas. + */ +export async function queryPersonasDB( + query: (record: PersonaRecord) => boolean, + t?: PersonasTransaction<'readonly'>, + isIncludeLogout?: boolean, +): Promise { + t = t || createTransaction(await db(), 'readonly')('personas') + const records: PersonaRecord[] = [] + for await (const each of t.objectStore('personas')) { + const out = personaRecordOutDB(each.value) + if (query(out) && (isIncludeLogout || !out.hasLogout)) records.push(out) + } + return records +} + +/** + * Query many Personas. + */ +export async function queryPersonasWithPrivateKey( + t?: FullPersonaDBTransaction<'readonly'>, +): Promise { + t = t || createTransaction(await db(), 'readonly')('personas', 'profiles', 'relations') + const records: PersonaRecord[] = [] + records.push( + ...(await t.objectStore('personas').index('hasPrivateKey').getAll(IDBKeyRange.only('yes'))).map( + personaRecordOutDB, + ), + ) + return records as PersonaRecordWithPrivateKey[] +} + +/** + * Update an existing Persona record. + * @param nextRecord The partial record to be merged + * @param howToMerge How to merge linkedProfiles and `field: undefined` + * @param t transaction + */ +export async function updatePersonaDB( + // Do a copy here. We need to delete keys from it. + { ...nextRecord }: Readonly & Pick>, + howToMerge: { + linkedProfiles: 'replace' | 'merge' + explicitUndefinedField: 'ignore' | 'delete field' + }, + t: PersonasTransaction<'readwrite'>, +): Promise { + const _old = await t.objectStore('personas').get(nextRecord.identifier.toText()) + if (!_old) throw new TypeError('Update an non-exist data') + const old = personaRecordOutDB(_old) + let nextLinkedProfiles = old.linkedProfiles + if (nextRecord.linkedProfiles) { + if (howToMerge.linkedProfiles === 'merge') + nextLinkedProfiles = new IdentifierMap( + new Map([...nextLinkedProfiles.__raw_map__, ...nextRecord.linkedProfiles.__raw_map__]), + ) + else nextLinkedProfiles = nextRecord.linkedProfiles + } + if (howToMerge.explicitUndefinedField === 'ignore') { + for (const _key in nextRecord) { + const key = _key as keyof typeof nextRecord + if (nextRecord[key] === undefined) { + delete nextRecord[key as keyof typeof nextRecord] + } + } + } + const next: PersonaRecordDB = personaRecordToDB({ + ...old, + ...nextRecord, + linkedProfiles: nextLinkedProfiles, + updatedAt: nextRecord.updatedAt ?? new Date(), + }) + await t.objectStore('personas').put(next) + ;(next.privateKey || old.privateKey) && MaskMessages.events.ownPersonaChanged.sendToAll(undefined) +} + +export async function createOrUpdatePersonaDB( + record: Partial & Pick, + howToMerge: Parameters[1] & { protectPrivateKey?: boolean }, + t: PersonasTransaction<'readwrite'>, +) { + const personaInDB = await t.objectStore('personas').get(record.identifier.toText()) + + if (howToMerge.protectPrivateKey && !!personaInDB?.privateKey && !record.privateKey) return + + if (howToMerge.protectPrivateKey && !!personaInDB?.privateKey) { + const nextRecord = personaRecordOutDB(personaInDB) + nextRecord.hasLogout = false + + return updatePersonaDB(nextRecord, howToMerge, t) + } + + if (personaInDB) return updatePersonaDB(record, howToMerge, t) + else + return createPersonaDB( + { + ...record, + createdAt: record.createdAt ?? new Date(), + updatedAt: record.updatedAt ?? new Date(), + linkedProfiles: new IdentifierMap(new Map()), + }, + t, + ) +} + +/** + * Delete a Persona + */ +export async function deletePersonaDB( + id: PersonaIdentifier, + confirm: 'delete even with private' | "don't delete if have private key", + t: PersonasTransaction<'readwrite'>, +): Promise { + const r = await t.objectStore('personas').get(id.toText()) + if (!r) return + if (confirm !== 'delete even with private' && r.privateKey) + throw new TypeError('Cannot delete a persona with a private key') + await t.objectStore('personas').delete(id.toText()) + r.privateKey && MaskMessages.events.ownPersonaChanged.sendToAll() +} +/** + * Delete a Persona + * @returns a boolean. true: the record no longer exists; false: the record is kept. + */ +export async function safeDeletePersonaDB( + id: PersonaIdentifier, + t?: FullPersonaDBTransaction<'readwrite'>, +): Promise { + t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') + const r = await queryPersonaDB(id, t) + if (!r) return true + if (r.linkedProfiles.size !== 0) return false + if (r.privateKey) return false + await deletePersonaDB(id, "don't delete if have private key", t) + return true +} + +/** + * Create a new profile. + */ +export async function createProfileDB(record: ProfileRecord, t: ProfileTransaction<'readwrite'>): Promise { + await t.objectStore('profiles').add(profileToDB(record)) + MaskMessages.events.profilesChanged.sendToAll([{ of: record.identifier, reason: 'update' }]) +} + +/** + * Query a profile. + */ +export async function queryProfileDB( + id: ProfileIdentifier, + t?: ProfileTransaction<'readonly'>, +): Promise { + t = t || createTransaction(await db(), 'readonly')('profiles') + const result = await t.objectStore('profiles').get(id.toText()) + if (result) return profileOutDB(result) + return null +} + +/** + * Query many profiles. + */ +export async function queryProfilesDB( + network: string | ((record: ProfileRecord) => boolean), + t?: ProfileTransaction<'readonly'>, +): Promise { + t = t || createTransaction(await db(), 'readonly')('profiles') + const result: ProfileRecord[] = [] + if (typeof network === 'string') { + result.push( + ...(await t.objectStore('profiles').index('network').getAll(IDBKeyRange.only(network))).map(profileOutDB), + ) + } else { + for await (const each of t.objectStore('profiles').iterate()) { + const out = profileOutDB(each.value) + if (network(out)) result.push(out) + } + } + return result +} + +const fuse = new Fuse([] as ProfileRecord[], { + shouldSort: true, + threshold: 0.45, + minMatchCharLength: 1, + keys: [ + { name: 'nickname', weight: 0.8 }, + { name: 'identifier.network', weight: 0.2 }, + ], +}) + +export async function queryProfilesPagedDB( + options: { + after?: ProfileIdentifier + query?: string + }, + count: number, +): Promise { + const t = createTransaction(await db(), 'readonly')('profiles') + const breakPoint = options.after?.toText() + let firstRecord = true + const data: ProfileRecord[] = [] + for await (const rec of t.objectStore('profiles').iterate()) { + if (firstRecord && breakPoint && rec.key !== breakPoint) { + rec.continue(breakPoint) + firstRecord = false + continue + } + firstRecord = false + // after this record + if (rec.key === breakPoint) continue + if (count <= 0) break + const outData = profileOutDB(rec.value) + if (typeof options.query === 'string') { + fuse.setCollection([outData]) + if (!fuse.search(options.query).length) continue + } + count -= 1 + data.push(outData) + } + return data +} + +/** + * Update a profile. + */ +export async function updateProfileDB( + updating: Partial & Pick, + t: ProfileTransaction<'readwrite'>, +): Promise { + const old = await t.objectStore('profiles').get(updating.identifier.toText()) + if (!old) throw new Error('Updating a non exists record') + + const nextRecord: ProfileRecordDB = profileToDB({ + ...profileOutDB(old), + ...updating, + }) + await t.objectStore('profiles').put(nextRecord) + MaskMessages.events.profilesChanged.sendToAll([{ reason: 'update', of: updating.identifier }]) +} +export async function createOrUpdateProfileDB(rec: ProfileRecord, t: ProfileTransaction<'readwrite'>) { + if (await queryProfileDB(rec.identifier, t)) return updateProfileDB(rec, t) + else return createProfileDB(rec, t) +} + +/** + * Detach a profile from it's linking persona. + * @param identifier The profile want to detach + * @param t A living transaction + */ +export async function detachProfileDB( + identifier: ProfileIdentifier, + t?: FullPersonaDBTransaction<'readwrite'>, +): Promise { + t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') + const profile = await queryProfileDB(identifier, t) + if (!profile?.linkedPersona) return + + const linkedPersona = profile.linkedPersona + const persona = await queryPersonaDB(linkedPersona, t) + persona?.linkedProfiles.delete(identifier) + + if (persona) { + await updatePersonaDB(persona, { linkedProfiles: 'replace', explicitUndefinedField: 'delete field' }, t) + if (persona.privateKey) MaskMessages.events.ownPersonaChanged.sendToAll(undefined) + } + profile.linkedPersona = undefined + await updateProfileDB(profile, t) +} + +/** + * attach a profile. + */ +export async function attachProfileDB( + identifier: ProfileIdentifier, + attachTo: PersonaIdentifier, + data: LinkedProfileDetails, + t?: FullPersonaDBTransaction<'readwrite'>, +): Promise { + t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') + const profile = + (await queryProfileDB(identifier, t)) || + (await createProfileDB({ identifier, createdAt: new Date(), updatedAt: new Date() }, t)) || + (await queryProfileDB(identifier, t)) + const persona = await queryPersonaDB(attachTo, t) + if (!persona || !profile) return + + if (profile.linkedPersona !== undefined && !profile.linkedPersona.equals(attachTo)) { + await detachProfileDB(identifier, t) + } + + profile.linkedPersona = attachTo + persona.linkedProfiles.set(identifier, data) + + await updatePersonaDB(persona, { linkedProfiles: 'merge', explicitUndefinedField: 'ignore' }, t) + await updateProfileDB(profile, t) + + if (persona.privateKey) MaskMessages.events.ownPersonaChanged.sendToAll(undefined) +} + +/** + * Delete a profile + */ +export async function deleteProfileDB(id: ProfileIdentifier, t: ProfileTransaction<'readwrite'>): Promise { + await t.objectStore('profiles').delete(id.toText()) + MaskMessages.events.profilesChanged.sendToAll([{ reason: 'delete', of: id }]) +} + +/** + * Create a new Relation + */ +export async function createRelationDB( + record: Omit, + t: RelationTransaction<'readwrite'>, + silent = false, +): Promise { + await t.objectStore('relations').add(relationRecordToDB(record)) + if (!silent) + MaskMessages.events.relationsChanged.sendToAll([{ of: record.profile, reason: 'update', favor: record.favor }]) +} + +export async function queryRelations(query: (record: RelationRecord) => boolean, t?: RelationTransaction<'readonly'>) { + t = t || createTransaction(await db(), 'readonly')('relations') + const records: RelationRecord[] = [] + + for await (const each of t.objectStore('relations')) { + const out = relationRecordOutDB(each.value) + if (query(out)) records.push(out) + } + + return records +} + +/** + * Query relations by paged + */ +export async function queryRelationsPagedDB( + linked: PersonaIdentifier, + options: { + network: string + after?: RelationRecord + }, + count: number, +) { + const t = createTransaction(await db(), 'readonly')('relations') + let firstRecord = true + + const data: RelationRecord[] = [] + + for await (const cursor of t.objectStore('relations').index('favor, profile, linked').iterate()) { + if (cursor.value.linked !== linked.toText()) continue + if (cursor.value.network !== options.network) continue + + if (firstRecord && options.after && options.after.profile.toText() !== cursor?.value.profile) { + cursor.continue([options.after.favor, options.after.profile.toText(), options.after.linked.toText()]) + firstRecord = false + continue + } + + firstRecord = false + + //after this record + if ( + options.after?.linked.toText() === cursor?.value.linked && + options.after?.profile.toText() === cursor?.value.profile + ) + continue + + if (count <= 0) break + const outData = relationRecordOutDB(cursor.value) + count -= 1 + data.push(outData) + } + return data +} + +/** + * Update a relation + * @param updating + * @param t + * @param silent + */ +export async function updateRelationDB( + updating: Omit, + t: RelationTransaction<'readwrite'>, + silent = false, +): Promise { + const old = await t + .objectStore('relations') + .get(IDBKeyRange.only([updating.linked.toText(), updating.profile.toText()])) + + if (!old) throw new Error('Updating a non exists record') + + const nextRecord: RelationRecordDB = relationRecordToDB({ + ...relationRecordOutDB(old), + ...updating, + }) + + await t.objectStore('relations').put(nextRecord) + if (!silent) { + MaskMessages.events.relationsChanged.sendToAll([ + { of: updating.profile, favor: updating.favor, reason: 'update' }, + ]) + } +} + +//#endregion + +//#region out db & to db +function profileToDB(x: ProfileRecord): ProfileRecordDB { + return { + ...x, + identifier: x.identifier.toText(), + network: x.identifier.network, + } +} +function profileOutDB({ network, ...x }: ProfileRecordDB): ProfileRecord { + if (x.linkedPersona) { + if (x.linkedPersona.type !== 'ec_key') throw new Error('Unknown type of linkedPersona') + } + return { + ...x, + identifier: Identifier.fromString(x.identifier, ProfileIdentifier).unwrap(), + linkedPersona: restorePrototype(x.linkedPersona, ECKeyIdentifier.prototype), + } +} +function personaRecordToDB(x: PersonaRecord): PersonaRecordDB { + return { + ...x, + identifier: x.identifier.toText(), + hasPrivateKey: x.privateKey ? 'yes' : 'no', + linkedProfiles: x.linkedProfiles.__raw_map__, + } +} +function personaRecordOutDB(x: PersonaRecordDB): PersonaRecord { + // @ts-ignore + delete x.hasPrivateKey + const obj: PersonaRecord = { + ...x, + identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), + } + return obj +} + +function relationRecordToDB(x: Omit): RelationRecordDB { + return { + ...x, + network: x.profile.network, + profile: x.profile.toText(), + linked: x.linked.toText(), + } +} + +function relationRecordOutDB(x: RelationRecordDB): RelationRecord { + return { + ...x, + profile: Identifier.fromString(x.profile, ProfileIdentifier).unwrap(), + linked: Identifier.fromString(x.linked, ECKeyIdentifier).unwrap(), + } +} + +//#endregion diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 4dd516e5d58b..65a0d492dabe 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -36,14 +36,14 @@ import { createRelationDB, createRelationsTransaction, deleteProfileDB, - LinkedProfileDetails, - ProfileRecord, queryPersonaDB, queryPersonasDB, queryProfilesDB, queryRelationsPagedDB, - RelationRecord, updateRelationDB, + ProfileRecord, + LinkedProfileDetails, + RelationRecord, } from '../../../background/database/persona/db' import { BackupJSONFileLatest, UpgradeBackupJSONFile } from '../../utils/type-transform/BackupFormat/JSON/latest' import { restoreBackup } from './WelcomeServices/restoreBackup' From 773a13cde0b6494897f89f01f44533b4c30ca92f Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 17 Jan 2022 17:20:28 +0800 Subject: [PATCH 02/18] feat: fake write access function --- .../mask/background/database/persona/app.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index e7190ae7740d..a320044be8f1 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -11,6 +11,10 @@ import type { } from './type' import type { ProfileIdentifier, PersonaIdentifier } from '@masknet/shared-base' +export async function consistentPersonaDBWriteAccess(action: () => Promise) { + await action() +} + export async function createPersonaDB(record: PersonaRecord): Promise { return } @@ -47,7 +51,7 @@ export async function updatePersonaDB( // Do a copy here. We need to delete keys linkedProfiles: 'replace' | 'merge' explicitUndefinedField: 'ignore' | 'delete field' }, - t: PersonasTransaction<'readwrite'>, + t?: PersonasTransaction<'readwrite'>, ): Promise { return } @@ -55,7 +59,7 @@ export async function updatePersonaDB( // Do a copy here. We need to delete keys export async function createOrUpdatePersonaDB( record: Partial & Pick, howToMerge: Parameters[1] & { protectPrivateKey?: boolean }, - t: PersonasTransaction<'readwrite'>, + t?: PersonasTransaction<'readwrite'>, ): Promise {} /** @@ -64,7 +68,7 @@ export async function createOrUpdatePersonaDB( export async function deletePersonaDB( id: PersonaIdentifier, confirm: 'delete even with private' | "don't delete if have private key", - t: PersonasTransaction<'readwrite'>, + t?: PersonasTransaction<'readwrite'>, ): Promise {} /** @@ -79,7 +83,7 @@ export async function safeDeletePersonaDB( /** * Create a new profile. */ -export async function createProfileDB(record: ProfileRecordDB, t: ProfileTransaction<'readwrite'>): Promise {} +export async function createProfileDB(record: ProfileRecordDB, t?: ProfileTransaction<'readwrite'>): Promise {} /** * Query a profile. @@ -121,12 +125,12 @@ export async function queryProfilesPagedDB( */ export async function updateProfileDB( updating: Partial & Pick, - t: ProfileTransaction<'readwrite'>, + t?: ProfileTransaction<'readwrite'>, ): Promise { return } -export async function createOrUpdateProfileDB(rec: ProfileRecord, t: ProfileTransaction<'readwrite'>): Promise {} +export async function createOrUpdateProfileDB(rec: ProfileRecord, t?: ProfileTransaction<'readwrite'>): Promise {} /** * Detach a profile from it's linking persona. @@ -148,14 +152,14 @@ export async function attachProfileDB(): Promise { /** * Delete a profile */ -export async function deleteProfileDB(id: ProfileIdentifier, t: ProfileTransaction<'readwrite'>): Promise {} +export async function deleteProfileDB(id: ProfileIdentifier, t?: ProfileTransaction<'readwrite'>): Promise {} /** * Create a new Relation */ export async function createRelationDB( record: Omit, - t: RelationTransaction<'readwrite'>, + t?: RelationTransaction<'readwrite'>, silent = false, ): Promise { return @@ -187,6 +191,6 @@ export async function queryRelationsPagedDB( */ export async function updateRelationDB( updating: Omit, - t: RelationTransaction<'readwrite'>, + t?: RelationTransaction<'readwrite'>, silent = false, ): Promise {} From d5a24028bec154e4f534e61f3a1aa373de58f3fb Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Tue, 18 Jan 2022 13:19:02 +0800 Subject: [PATCH 03/18] feat(wip): define native api type --- packages/public-api/src/mobile.ts | 41 +++++++++++++++++++++++++- packages/public-api/src/types/index.ts | 31 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index cc0288ffadc6..93ad2821db34 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -1,4 +1,4 @@ -import type { JsonRpcPayload, JsonRpcResponse } from './types' +import type { JsonRpcPayload, JsonRpcResponse, PersonaRecord, PageOption, ProfileRecord } from './types' /** * APIs that both Android and iOS implements and have the same API signature @@ -11,6 +11,45 @@ export interface SharedNativeAPIs { wallet_switchBlockChain(payload: { coinId?: number; networkId: number }): Promise misc_openCreateWalletView(): Promise misc_openDashboardView(): Promise + /** + * DB JSON RPC + */ + query_personas(params: { + identifier?: string + hasPrivateKey?: boolean + includeLogout?: boolean + nameContains?: string + pageOption?: PageOption + }): Promise + update_persona(params: { + persona: PersonaRecord + options: { + linkedProfileMergePolicy?: 0 | 1 + deleteUndefinedFields?: boolean + protectPrivateKey?: boolean + createWhenNotExist?: boolean + } + }): Promise + delete_persona(params: { identifier: string; options: { safeDelete?: boolean } }): Promise + query_profiles(params: { + identifier?: string + network?: string + nameContains?: string + pageOption: PageOption + }): Promise + update_profile(params: { + profile: ProfileRecord + options: { + createWhenNotExist?: boolean + } + }): Promise + delete_profile(params: { identifier: string }): Promise + attachProfile(params: { + profileIdentifier: string + personaIdentifier: string + state: string // ? + }): Promise + deattachProfile(params: { identifier: string }): Promise } /** * APIs that only implemented by iOS Mask Network diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index ad439d7d6fb6..b9a462942749 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -11,3 +11,34 @@ export interface JsonRpcResponse { result?: any error?: string } + +export interface PersonaRecord { + identifier: string + mnemonic?: { + words: string + parameter: { path: string; withPassword: string } + } + publicKey: string + privateKey?: string + localKey?: string // ? + nickname?: string + linkedProfiles: Map // ? + createAt: string + updateAt: string + hasLogout?: boolean + uninitialized?: boolean +} + +export interface ProfileRecord { + identifier: string + nickname?: string + localKey?: string // ? + linkedPersona?: string + createAt?: string + updateAt?: string +} + +export type PageOption = { + pageSize: number + pageOffset: number +} From e9df1ee464662dd78ea4a2c560f754af60383116 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Tue, 18 Jan 2022 15:56:28 +0800 Subject: [PATCH 04/18] chore: cspell --- cspell.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index 979bbbada9a3..904f11d6ab50 100644 --- a/cspell.json +++ b/cspell.json @@ -271,6 +271,7 @@ "cryptopunks", "Csvg", "CUSD", + "deattach", "Defi", "devnet", "Dnoxg", @@ -345,6 +346,7 @@ "testweb", "tinycolor", "treeshake", + "txreceipt", "txts", "uaddr", "unauthenticate", @@ -358,8 +360,7 @@ "walletlink", "WNATIVE", "xdescribe", - "xtest", - "txreceipt" + "xtest" ], "ignoreRegExpList": ["/@servie/"], "overrides": [ From 539190a0c3e64e5aa2dcdaa951f97e444ec97f73 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Tue, 18 Jan 2022 17:38:31 +0800 Subject: [PATCH 05/18] refactor: move native-rpc to mask/shared --- .../{src/utils => shared}/native-rpc/Android.channel.ts | 0 .../mask/{src/utils => shared}/native-rpc/iOS.channel.ts | 0 packages/mask/{src/utils => shared}/native-rpc/index.ts | 9 ++++++++- packages/mask/src/background-service.ts | 2 +- .../components/InjectedComponents/ToolboxUnstyled.tsx | 3 ++- packages/mask/src/components/Welcomes/Banner.tsx | 3 ++- .../background-script/EthereumServices/request.ts | 2 +- .../extension/background-script/EthereumServices/send.ts | 2 +- .../Wallet/SNSAdaptor/SelectProviderDialog/index.tsx | 2 +- packages/mask/src/plugins/Wallet/services/account.ts | 2 +- .../mask/src/plugins/Wallet/services/legacyWallet.ts | 2 +- .../mask/src/plugins/Wallet/services/wallet/index.ts | 2 +- packages/mask/src/utils/index.ts | 1 - 13 files changed, 19 insertions(+), 11 deletions(-) rename packages/mask/{src/utils => shared}/native-rpc/Android.channel.ts (100%) rename packages/mask/{src/utils => shared}/native-rpc/iOS.channel.ts (100%) rename packages/mask/{src/utils => shared}/native-rpc/index.ts (85%) diff --git a/packages/mask/src/utils/native-rpc/Android.channel.ts b/packages/mask/shared/native-rpc/Android.channel.ts similarity index 100% rename from packages/mask/src/utils/native-rpc/Android.channel.ts rename to packages/mask/shared/native-rpc/Android.channel.ts diff --git a/packages/mask/src/utils/native-rpc/iOS.channel.ts b/packages/mask/shared/native-rpc/iOS.channel.ts similarity index 100% rename from packages/mask/src/utils/native-rpc/iOS.channel.ts rename to packages/mask/shared/native-rpc/iOS.channel.ts diff --git a/packages/mask/src/utils/native-rpc/index.ts b/packages/mask/shared/native-rpc/index.ts similarity index 85% rename from packages/mask/src/utils/native-rpc/index.ts rename to packages/mask/shared/native-rpc/index.ts index bb7f349ca9b6..34ec98805d84 100644 --- a/packages/mask/src/utils/native-rpc/index.ts +++ b/packages/mask/shared/native-rpc/index.ts @@ -1,9 +1,16 @@ import { AsyncCall, AsyncCallOptions, _AsyncVersionOf } from 'async-call-rpc/full' import { AndroidGeckoViewChannel } from './Android.channel' import { iOSWebkitChannel } from './iOS.channel' -import { MaskNetworkAPI } from './Web' import type { AndroidNativeAPIs, iOSNativeAPIs } from '@masknet/public-api' +/** + * Typescript will not add the file to the project dependency tree + * but webpack will do constant folding + */ +//@ts-ignore +// eslint-disable-next-line no-useless-concat +const MaskNetworkAPI = import('../../src/utils/native-rpc/' + 'Web.ts') + // This module won't be used in Web. Let it not effecting HMR. if (process.env.architecture === 'web' && import.meta.webpackHot) import.meta.webpackHot.accept() export const hasNativeAPI = process.env.architecture === 'app' diff --git a/packages/mask/src/background-service.ts b/packages/mask/src/background-service.ts index 550db7e23a56..a43d24e207ea 100644 --- a/packages/mask/src/background-service.ts +++ b/packages/mask/src/background-service.ts @@ -1,6 +1,6 @@ import '../background/setup' import './extension/service' // setup Services.* -import './utils/native-rpc' // setup Android and iOS API server +import '../shared/native-rpc' // setup Android and iOS API server import './social-network-adaptor' // setup social network providers import './extension/background-script/Jobs' // start jobs import './utils/debug/general' diff --git a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx index dd73b57afd5d..f951e8ccac03 100644 --- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx +++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx @@ -25,7 +25,8 @@ import { import { useCallback, useMemo } from 'react' import { useRemoteControlledDialog, WalletIcon } from '@masknet/shared' import { WalletMessages } from '../../plugins/Wallet/messages' -import { hasNativeAPI, nativeAPI, useI18N } from '../../utils' +import { useI18N } from '../../utils' +import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' import { useRecentTransactions } from '../../plugins/Wallet/hooks/useRecentTransactions' import GuideStep from '../GuideStep' import { MaskFilledIcon } from '../../resources/MaskIcon' diff --git a/packages/mask/src/components/Welcomes/Banner.tsx b/packages/mask/src/components/Welcomes/Banner.tsx index 4f482f0a8595..1b891a46f4d5 100644 --- a/packages/mask/src/components/Welcomes/Banner.tsx +++ b/packages/mask/src/components/Welcomes/Banner.tsx @@ -8,7 +8,8 @@ import { useValueRef } from '@masknet/shared' import { DashboardRoutes } from '@masknet/shared-base' import { MaskSharpIcon } from '../../resources/MaskIcon' import { useMount } from 'react-use' -import { hasNativeAPI, nativeAPI, useI18N } from '../../utils' +import { useI18N } from '../../utils' +import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' import GuideStep from '../GuideStep' import { userGuideStatus } from '../../settings/settings' diff --git a/packages/mask/src/extension/background-script/EthereumServices/request.ts b/packages/mask/src/extension/background-script/EthereumServices/request.ts index 397928066fe6..377db87ebaf0 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/request.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/request.ts @@ -15,7 +15,7 @@ import { import { WalletRPC } from '../../../plugins/Wallet/messages' import { INTERNAL_nativeSend, INTERNAL_send } from './send' import { defer } from '@masknet/shared-base' -import { hasNativeAPI, nativeAPI } from '../../../utils/native-rpc' +import { hasNativeAPI, nativeAPI } from '../../../../shared/native-rpc' import { openPopupWindow } from '../HelperService' import Services from '../../service' diff --git a/packages/mask/src/extension/background-script/EthereumServices/send.ts b/packages/mask/src/extension/background-script/EthereumServices/send.ts index d1488a3750e9..9bbeeb027064 100644 --- a/packages/mask/src/extension/background-script/EthereumServices/send.ts +++ b/packages/mask/src/extension/background-script/EthereumServices/send.ts @@ -35,7 +35,7 @@ import { } from '../../../plugins/Wallet/settings' import { debugModeSetting } from '../../../settings/settings' import { Flags } from '../../../../shared' -import { nativeAPI } from '../../../utils/native-rpc' +import { nativeAPI } from '../../../../shared/native-rpc' import { WalletRPC } from '../../../plugins/Wallet/messages' import { getSendTransactionComputedPayload } from './rpc' import { getError, hasError } from './error' diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectProviderDialog/index.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectProviderDialog/index.tsx index b352090361e0..3b786ec165aa 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectProviderDialog/index.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectProviderDialog/index.tsx @@ -14,7 +14,7 @@ import { isDashboardPage } from '@masknet/shared-base' import { useI18N } from '../../../../utils/i18n-next-ui' import { WalletMessages } from '../../messages' import { InjectedDialog } from '../../../../components/shared/InjectedDialog' -import { hasNativeAPI, nativeAPI } from '../../../../utils' +import { hasNativeAPI, nativeAPI } from '../../../../../shared/native-rpc' import { PluginProviderRender } from './PluginProviderRender' import { pluginIDSettings } from '../../../../settings/settings' diff --git a/packages/mask/src/plugins/Wallet/services/account.ts b/packages/mask/src/plugins/Wallet/services/account.ts index 5d0e988a81fa..d88a433dceac 100644 --- a/packages/mask/src/plugins/Wallet/services/account.ts +++ b/packages/mask/src/plugins/Wallet/services/account.ts @@ -17,7 +17,7 @@ import { currentProviderSettings, } from '../settings' import { getWallets, hasWallet, updateWallet } from './wallet' -import { hasNativeAPI, nativeAPI } from '../../../utils' +import { hasNativeAPI, nativeAPI } from '../../../../shared/native-rpc' import { Flags } from '../../../../shared' export async function updateAccount( diff --git a/packages/mask/src/plugins/Wallet/services/legacyWallet.ts b/packages/mask/src/plugins/Wallet/services/legacyWallet.ts index fc1b1637e4de..accc5f0b8911 100644 --- a/packages/mask/src/plugins/Wallet/services/legacyWallet.ts +++ b/packages/mask/src/plugins/Wallet/services/legacyWallet.ts @@ -11,7 +11,7 @@ import { currySameAddress, isSameAddress, ProviderType } from '@masknet/web3-sha import { LegacyWalletRecordOutDB } from './helpers' import { currentAccountSettings, currentProviderSettings } from '../settings' import { HD_PATH_WITHOUT_INDEX_ETHEREUM } from '../constants' -import { hasNativeAPI } from '../../../utils/native-rpc' +import { hasNativeAPI } from '../../../../shared/native-rpc' import { getAccounts } from '../../../extension/background-script/EthereumService' function sortWallet(a: LegacyWalletRecord, b: LegacyWalletRecord) { diff --git a/packages/mask/src/plugins/Wallet/services/wallet/index.ts b/packages/mask/src/plugins/Wallet/services/wallet/index.ts index b1e91ce2a32a..b05b7e325a0b 100644 --- a/packages/mask/src/plugins/Wallet/services/wallet/index.ts +++ b/packages/mask/src/plugins/Wallet/services/wallet/index.ts @@ -10,7 +10,7 @@ import * as sdk from './maskwallet' import * as database from './database' import * as password from './password' import * as EthereumServices from '../../../../extension/background-script/EthereumService' -import { hasNativeAPI } from '../../../../utils' +import { hasNativeAPI } from '../../../../../shared/native-rpc' import type { WalletRecord } from './type' function bumpDerivationPath(path = `${HD_PATH_WITHOUT_INDEX_ETHEREUM}/0`) { diff --git a/packages/mask/src/utils/index.ts b/packages/mask/src/utils/index.ts index c7f0facb60e6..8336323e4d8a 100644 --- a/packages/mask/src/utils/index.ts +++ b/packages/mask/src/utils/index.ts @@ -1,7 +1,6 @@ export * from './components/' export * from './debug' export * from './hooks' -export * from './native-rpc' export * from './shadow-root' export * from './suspends' export * from './type-transform' From 559b41cf72a6ee7699d85e9cadad94d3378ec413 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Tue, 18 Jan 2022 19:44:53 +0800 Subject: [PATCH 06/18] feat: implement query persona with native api --- .../mask/background/database/persona/app.ts | 45 +++++++++++++++++-- packages/public-api/src/index.ts | 1 + packages/public-api/src/mobile.ts | 9 +++- packages/public-api/src/types/index.ts | 34 +++++++++----- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index a320044be8f1..5ecbfea047a1 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -9,8 +9,18 @@ import type { RelationRecord, RelationTransaction, } from './type' -import type { ProfileIdentifier, PersonaIdentifier } from '@masknet/shared-base' - +import { + ProfileIdentifier, + PersonaIdentifier, + Identifier, + IdentifierMap, + ECKeyIdentifier, + EC_Public_JsonWebKey, + EC_Private_JsonWebKey, + AESJsonWebKey, +} from '@masknet/shared-base' +import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' +import type { PersonaRecord as NativePersonaRecord } from '@masknet/public-api' export async function consistentPersonaDBWriteAccess(action: () => Promise) { await action() } @@ -31,11 +41,23 @@ export async function queryPersonaDB( t?: PersonasTransaction<'readonly'>, isIncludeLogout?: boolean, ): Promise { - return null + if (!hasNativeAPI) return null + const x = await nativeAPI?.api.query_persona({ + identifier: query.toText(), + includeLogout: isIncludeLogout, + }) + if (!x) return null + return personaRecordOutDB(x) } export async function queryPersonasWithPrivateKey(): Promise { - return [] + if (!hasNativeAPI) return [] + const results = await nativeAPI?.api.query_personas({ + hasPrivateKey: true, + }) + + if (!results) return [] + return results.map((x) => personaRecordOutDB(x)) as PersonaRecordWithPrivateKey[] } /** @@ -194,3 +216,18 @@ export async function updateRelationDB( t?: RelationTransaction<'readwrite'>, silent = false, ): Promise {} + +// #region out db & to db +function personaRecordOutDB(x: NativePersonaRecord): PersonaRecord { + return { + ...x, + publicKey: x.publicKey as JsonWebKey as unknown as EC_Public_JsonWebKey, + privateKey: x.privateKey as JsonWebKey as unknown as EC_Private_JsonWebKey, + localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, + identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), + createdAt: new Date(x.createAt), + updatedAt: new Date(x.updateAt), + } +} +// #endregion diff --git a/packages/public-api/src/index.ts b/packages/public-api/src/index.ts index e624ef74cd42..54f2acdad9ff 100644 --- a/packages/public-api/src/index.ts +++ b/packages/public-api/src/index.ts @@ -2,3 +2,4 @@ export * from './web' // Following is the API that implemented by the native side. export * from './mobile' +export * from './types' diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index 93ad2821db34..bbabb204db65 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -14,13 +14,20 @@ export interface SharedNativeAPIs { /** * DB JSON RPC */ - query_personas(params: { + query_persona(params: { identifier?: string hasPrivateKey?: boolean includeLogout?: boolean nameContains?: string pageOption?: PageOption }): Promise + query_personas(params: { + identifier?: string + hasPrivateKey?: boolean + includeLogout?: boolean + nameContains?: string + pageOption?: PageOption + }): Promise update_persona(params: { persona: PersonaRecord options: { diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index b9a462942749..b7b22d31ea9f 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -12,19 +12,33 @@ export interface JsonRpcResponse { error?: string } +export interface LinkedProfileDetails { + connectionConfirmState: 'confirmed' | 'pending' | 'denied' +} + +// These type MUST be sync with packages/shared-base/src/crypto/JWKType +export interface EC_Public_JsonWebKey extends JsonWebKey, Nominal<'EC public'> {} +export interface EC_Private_JsonWebKey extends JsonWebKey, Nominal<'EC private'> {} +export interface AESJsonWebKey extends JsonWebKey, Nominal<'AES'> {} + +declare class Nominal { + /** Ghost property, don't use it! */ + private __brand: T +} + export interface PersonaRecord { identifier: string mnemonic?: { words: string - parameter: { path: string; withPassword: string } + parameter: { path: string; withPassword: boolean } } - publicKey: string - privateKey?: string - localKey?: string // ? + publicKey: EC_Public_JsonWebKey + privateKey?: EC_Private_JsonWebKey + localKey?: AESJsonWebKey nickname?: string - linkedProfiles: Map // ? - createAt: string - updateAt: string + linkedProfiles: Map + createAt: number + updateAt: number hasLogout?: boolean uninitialized?: boolean } @@ -32,10 +46,10 @@ export interface PersonaRecord { export interface ProfileRecord { identifier: string nickname?: string - localKey?: string // ? + localKey?: AESJsonWebKey linkedPersona?: string - createAt?: string - updateAt?: string + createAt?: number + updateAt?: number } export type PageOption = { From f7eb402562ef9b8a0b6a2509e425b9c408f06808 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Wed, 19 Jan 2022 14:08:40 +0800 Subject: [PATCH 07/18] fix: bugfix --- packages/mask/background/database/persona/app.ts | 4 +--- packages/mask/background/database/persona/db.ts | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 5ecbfea047a1..792302544043 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -19,7 +19,7 @@ import { EC_Private_JsonWebKey, AESJsonWebKey, } from '@masknet/shared-base' -import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' +import { nativeAPI } from '../../../shared/native-rpc' import type { PersonaRecord as NativePersonaRecord } from '@masknet/public-api' export async function consistentPersonaDBWriteAccess(action: () => Promise) { await action() @@ -41,7 +41,6 @@ export async function queryPersonaDB( t?: PersonasTransaction<'readonly'>, isIncludeLogout?: boolean, ): Promise { - if (!hasNativeAPI) return null const x = await nativeAPI?.api.query_persona({ identifier: query.toText(), includeLogout: isIncludeLogout, @@ -51,7 +50,6 @@ export async function queryPersonaDB( } export async function queryPersonasWithPrivateKey(): Promise { - if (!hasNativeAPI) return [] const results = await nativeAPI?.api.query_personas({ hasPrivateKey: true, }) diff --git a/packages/mask/background/database/persona/db.ts b/packages/mask/background/database/persona/db.ts index e78b3e0ce28a..3fc09d0d443a 100644 --- a/packages/mask/background/database/persona/db.ts +++ b/packages/mask/background/database/persona/db.ts @@ -1,3 +1,4 @@ +import { hasNativeAPI } from '../../../shared/native-rpc' export * from './type' export const { @@ -27,7 +28,7 @@ export const { } = new Proxy({} as any as typeof import('./web'), { get(_, key) { return async function () { - if (process.env.arch === 'app') { + if (hasNativeAPI) { return import('./app').then((x) => (x as any)[key]) } From 16db29a649310c5371782af0a0e10ef4de5dad49 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Wed, 19 Jan 2022 15:42:44 +0800 Subject: [PATCH 08/18] fix: add args to proxy function --- packages/mask/background/database/persona/db.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mask/background/database/persona/db.ts b/packages/mask/background/database/persona/db.ts index 3fc09d0d443a..fae1a9249f96 100644 --- a/packages/mask/background/database/persona/db.ts +++ b/packages/mask/background/database/persona/db.ts @@ -27,12 +27,12 @@ export const { queryRelations, } = new Proxy({} as any as typeof import('./web'), { get(_, key) { - return async function () { + return async function (...args: any) { if (hasNativeAPI) { - return import('./app').then((x) => (x as any)[key]) + return import('./app').then((x) => (x as any)[key](...args)) } - return import('./web').then((x) => (x as any)[key]) + return import('./web').then((x) => (x as any)[key](...args)) } }, }) From 203061aad8ae57b1f59e9cee37d34eb253794358 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Wed, 19 Jan 2022 16:32:42 +0800 Subject: [PATCH 09/18] fix: bugfix --- packages/mask/shared/native-rpc/index.ts | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/mask/shared/native-rpc/index.ts b/packages/mask/shared/native-rpc/index.ts index 34ec98805d84..49ea2ed8dac4 100644 --- a/packages/mask/shared/native-rpc/index.ts +++ b/packages/mask/shared/native-rpc/index.ts @@ -3,14 +3,6 @@ import { AndroidGeckoViewChannel } from './Android.channel' import { iOSWebkitChannel } from './iOS.channel' import type { AndroidNativeAPIs, iOSNativeAPIs } from '@masknet/public-api' -/** - * Typescript will not add the file to the project dependency tree - * but webpack will do constant folding - */ -//@ts-ignore -// eslint-disable-next-line no-useless-concat -const MaskNetworkAPI = import('../../src/utils/native-rpc/' + 'Web.ts') - // This module won't be used in Web. Let it not effecting HMR. if (process.env.architecture === 'web' && import.meta.webpackHot) import.meta.webpackHot.accept() export const hasNativeAPI = process.env.architecture === 'app' @@ -26,16 +18,30 @@ if (process.env.architecture === 'app') { parameterStructures: 'by-name', } if (process.env.engine === 'safari') { - const api = (sharedNativeAPI = AsyncCall(MaskNetworkAPI, { - ...options, - channel: new iOSWebkitChannel(), - })) + const api = (sharedNativeAPI = AsyncCall( + /** + * Typescript will not add the file to the project dependency tree + * but webpack will do constant folding + */ + // @ts-ignore + // eslint-disable-next-line no-useless-concat + import('../../src/utils/native-rpc/' + 'Web.ts').then((x) => x.MaskNetworkAPI), + { + ...options, + channel: new iOSWebkitChannel(), + }, + )) nativeAPI = { type: 'iOS', api } } else if (process.env.engine === 'firefox') { - const api = (sharedNativeAPI = AsyncCall(MaskNetworkAPI, { - ...options, - channel: new AndroidGeckoViewChannel(), - })) + const api = (sharedNativeAPI = AsyncCall( + // @ts-ignore + // eslint-disable-next-line no-useless-concat + import('../../src/utils/native-rpc/' + 'Web.ts').then((x) => x.MaskNetworkAPI), + { + ...options, + channel: new AndroidGeckoViewChannel(), + }, + )) nativeAPI = { type: 'Android', api } } } From c1e6d63121d8e9bf2b43ec2bff160f058e4b04f6 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Wed, 19 Jan 2022 17:04:20 +0800 Subject: [PATCH 10/18] fix: implement query personas --- packages/mask/background/database/persona/app.ts | 13 +++++++++++++ packages/mask/background/database/persona/web.ts | 10 +++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 792302544043..66739d392c2b 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -49,6 +49,19 @@ export async function queryPersonaDB( return personaRecordOutDB(x) } +export async function queryPersonasDB( + query: (record: PersonaRecord) => boolean, + t?: PersonasTransaction<'readonly'>, + isIncludeLogout?: boolean, +): Promise { + const results = await nativeAPI?.api.query_personas({ + includeLogout: isIncludeLogout, + }) + + if (!results) return [] + return results.map((x) => personaRecordOutDB(x)) +} + export async function queryPersonasWithPrivateKey(): Promise { const results = await nativeAPI?.api.query_personas({ hasPrivateKey: true, diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index fa6515f0cf98..0fb418dc2742 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -188,7 +188,7 @@ export async function createReadonlyPersonaTransaction() { return createTransaction(await db(), 'readonly') } -//#region Plain methods +// #region Plain methods /** Create a new Persona. */ export async function createPersonaDB(record: PersonaRecord, t: PersonasTransaction<'readwrite'>): Promise { await t.objectStore('personas').add(personaRecordToDB(record)) @@ -580,7 +580,7 @@ export async function queryRelationsPagedDB( firstRecord = false - //after this record + // after this record if ( options.after?.linked.toText() === cursor?.value.linked && options.after?.profile.toText() === cursor?.value.profile @@ -625,9 +625,9 @@ export async function updateRelationDB( } } -//#endregion +// #endregion -//#region out db & to db +// #region out db & to db function profileToDB(x: ProfileRecord): ProfileRecordDB { return { ...x, @@ -681,4 +681,4 @@ function relationRecordOutDB(x: RelationRecordDB): RelationRecord { } } -//#endregion +// #endregion From f686efb7a841fc0c3b603dce76bd950b7e70425c Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Wed, 19 Jan 2022 17:17:58 +0800 Subject: [PATCH 11/18] chore: eslint --- packages/mask/background/database/persona/type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mask/background/database/persona/type.ts b/packages/mask/background/database/persona/type.ts index 362c86083e9e..9f3b8a47ecce 100644 --- a/packages/mask/background/database/persona/type.ts +++ b/packages/mask/background/database/persona/type.ts @@ -35,7 +35,7 @@ export type RelationTransaction = IDBPSaf Mode > -//#region Type +// #region Type export type PersonaRecordDB = Omit & { identifier: string linkedProfiles: Map @@ -132,4 +132,4 @@ export interface LinkedProfileDetails { export type PersonaRecordWithPrivateKey = PersonaRecord & Required> -//#endregion +// #endregion From 8f7b30b3c48b92fd2b3cc4852a9f5375250c6baa Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Thu, 20 Jan 2022 19:55:28 +0800 Subject: [PATCH 12/18] feat: native api --- cspell.json | 1 - .../src/pages/Personas/hooks/useContacts.ts | 1 + .../mask/background/database/persona/app.ts | 255 ++++++++++++++++-- .../mask/background/database/persona/web.ts | 30 ++- packages/mask/src/database/Persona/helpers.ts | 4 +- .../debugShowAllPossibleHashForPost.ts | 2 +- .../background-script/IdentityService.ts | 11 +- .../WelcomeServices/generateBackupJSON.ts | 8 +- packages/mask/src/utils/native-rpc/Web.ts | 14 + packages/public-api/src/mobile.ts | 64 ++++- packages/public-api/src/types/index.ts | 21 +- packages/public-api/src/web.ts | 21 +- 12 files changed, 359 insertions(+), 73 deletions(-) diff --git a/cspell.json b/cspell.json index 7cded1ae46ff..286d6aecce1b 100644 --- a/cspell.json +++ b/cspell.json @@ -281,7 +281,6 @@ "cryptopunks", "Csvg", "CUSD", - "deattach", "Defi", "devnet", "Dnoxg", diff --git a/packages/dashboard/src/pages/Personas/hooks/useContacts.ts b/packages/dashboard/src/pages/Personas/hooks/useContacts.ts index b9417ea382f4..39fb5b23b611 100644 --- a/packages/dashboard/src/pages/Personas/hooks/useContacts.ts +++ b/packages/dashboard/src/pages/Personas/hooks/useContacts.ts @@ -21,6 +21,7 @@ export const useContacts = (network: string, page: number, size = 20) => { { network, after: lastValue, + pageOffset: page * size, }, size, ) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 66739d392c2b..fda500fd75a9 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -2,12 +2,12 @@ import type { PersonaRecord, FullPersonaDBTransaction, PersonasTransaction, - ProfileRecordDB, PersonaRecordWithPrivateKey, ProfileRecord, ProfileTransaction, RelationRecord, RelationTransaction, + LinkedProfileDetails, } from './type' import { ProfileIdentifier, @@ -20,20 +20,38 @@ import { AESJsonWebKey, } from '@masknet/shared-base' import { nativeAPI } from '../../../shared/native-rpc' -import type { PersonaRecord as NativePersonaRecord } from '@masknet/public-api' +import type { + PersonaRecord as NativePersonaRecord, + ProfileRecord as NativeProfileRecord, + RelationRecord as NativeRelationRecord, + EC_Private_JsonWebKey as Native_EC_Private_JsonWebKey, + EC_Public_JsonWebKey as Native_EC_Public_JsonWebKey, + AESJsonWebKey as Native_AESJsonWebKey, +} from '@masknet/public-api' +import { MaskMessages } from '../../../shared' export async function consistentPersonaDBWriteAccess(action: () => Promise) { await action() } export async function createPersonaDB(record: PersonaRecord): Promise { - return + await nativeAPI?.api.create_persona({ + persona: personaRecordToDB(record), + }) } -export async function createPersonaByProfileDB( +export async function queryPersonaByProfileDB( query: ProfileIdentifier, t?: FullPersonaDBTransaction<'readonly'>, ): Promise { - return null + const x = await nativeAPI?.api.query_persona_by_profile({ + options: { + profileIdentifier: query.toText(), + }, + }) + + if (!x) return null + + return personaRecordOutDB(x) } export async function queryPersonaDB( @@ -86,14 +104,30 @@ export async function updatePersonaDB( // Do a copy here. We need to delete keys }, t?: PersonasTransaction<'readwrite'>, ): Promise { - return + return nativeAPI?.api.update_persona({ + persona: partialPersonaRecordToDB(nextRecord), + options: { + linkedProfileMergePolicy: howToMerge.linkedProfiles === 'replace' ? 0 : 1, + deleteUndefinedFields: howToMerge.explicitUndefinedField !== 'ignore', + }, + }) } export async function createOrUpdatePersonaDB( record: Partial & Pick, howToMerge: Parameters[1] & { protectPrivateKey?: boolean }, t?: PersonasTransaction<'readwrite'>, -): Promise {} +): Promise { + return nativeAPI?.api.update_persona({ + persona: partialPersonaRecordToDB(record), + options: { + linkedProfileMergePolicy: howToMerge.linkedProfiles === 'replace' ? 0 : 1, + deleteUndefinedFields: howToMerge.explicitUndefinedField !== 'ignore', + protectPrivateKey: howToMerge.protectPrivateKey, + createWhenNotExist: true, + }, + }) +} /** * Delete a Persona @@ -102,7 +136,14 @@ export async function deletePersonaDB( id: PersonaIdentifier, confirm: 'delete even with private' | "don't delete if have private key", t?: PersonasTransaction<'readwrite'>, -): Promise {} +): Promise { + return nativeAPI?.api.delete_persona({ + identifier: id.toText(), + options: { + safeDelete: confirm === 'delete even with private', + }, + }) +} /** * Delete a Persona @@ -111,12 +152,19 @@ export async function deletePersonaDB( export async function safeDeletePersonaDB( id: PersonaIdentifier, t?: FullPersonaDBTransaction<'readwrite'>, -): Promise {} +): Promise { + return deletePersonaDB(id, "don't delete if have private key") +} /** * Create a new profile. */ -export async function createProfileDB(record: ProfileRecordDB, t?: ProfileTransaction<'readwrite'>): Promise {} +export async function createProfileDB(record: ProfileRecord, t?: ProfileTransaction<'readwrite'>): Promise { + await nativeAPI?.api.create_profile({ + profile: profileRecordToDB(record), + }) + MaskMessages.events.profilesChanged.sendToAll([{ of: record.identifier, reason: 'update' }]) +} /** * Query a profile. @@ -125,6 +173,14 @@ export async function queryProfileDB( id: ProfileIdentifier, t?: ProfileTransaction<'readonly'>, ): Promise { + const x = await nativeAPI?.api.query_profile({ + options: { + identifier: id.toText(), + }, + }) + + if (x) return profileRecordOutDB(x) + return null } @@ -132,13 +188,25 @@ export async function queryProfileDB( * Query many profiles. */ export async function queryProfilesDB( - network: string | ((record: ProfileRecord) => boolean), + query: { + network?: string + identifiers?: ProfileIdentifier[] + hasLinkedPersona?: boolean + }, t?: ProfileTransaction<'readonly'>, ): Promise { - return [] + const profiles = await nativeAPI?.api.query_profiles({ + identifiers: query.identifiers?.map((x) => x.toText()), + network: query.network, + hasLinkedPersona: query.hasLinkedPersona, + }) + + if (!profiles) return [] + return profiles.map((x) => profileRecordOutDB(x)) } /** + * @deprecated * query profiles with paged * @param options * @param count @@ -160,10 +228,19 @@ export async function updateProfileDB( updating: Partial & Pick, t?: ProfileTransaction<'readwrite'>, ): Promise { - return + return nativeAPI?.api.update_profile({ + profile: partialProfileRecordToDB(updating), + }) } -export async function createOrUpdateProfileDB(rec: ProfileRecord, t?: ProfileTransaction<'readwrite'>): Promise {} +export async function createOrUpdateProfileDB(rec: ProfileRecord, t?: ProfileTransaction<'readwrite'>): Promise { + return nativeAPI?.api.update_profile({ + profile: partialProfileRecordToDB(rec), + options: { + createWhenNotExist: true, + }, + }) +} /** * Detach a profile from it's linking persona. @@ -173,19 +250,40 @@ export async function createOrUpdateProfileDB(rec: ProfileRecord, t?: ProfileTra export async function detachProfileDB( identifier: ProfileIdentifier, t?: FullPersonaDBTransaction<'readwrite'>, -): Promise {} +): Promise { + return nativeAPI?.api.detach_profile({ + identifier: identifier.toText(), + }) +} /** * attach a profile. */ -export async function attachProfileDB(): Promise { - return +export async function attachProfileDB( + identifier: ProfileIdentifier, + attachTo: PersonaIdentifier, + data: LinkedProfileDetails, +): Promise { + await nativeAPI?.api.attach_profile({ + profileIdentifier: identifier.toText(), + personaIdentifier: attachTo.toText(), + state: data, + }) + + const persona = await queryPersonaDB(attachTo) + + if (persona?.privateKey) MaskMessages.events.ownPersonaChanged.sendToAll(undefined) } /** * Delete a profile */ -export async function deleteProfileDB(id: ProfileIdentifier, t?: ProfileTransaction<'readwrite'>): Promise {} +export async function deleteProfileDB(id: ProfileIdentifier, t?: ProfileTransaction<'readwrite'>): Promise { + await nativeAPI?.api.delete_profile({ + identifier: id.toText(), + }) + MaskMessages.events.profilesChanged.sendToAll([{ reason: 'delete', of: id }]) +} /** * Create a new Relation @@ -195,14 +293,22 @@ export async function createRelationDB( t?: RelationTransaction<'readwrite'>, silent = false, ): Promise { - return + await nativeAPI?.api.create_relation({ + relation: relationRecordToDB(record), + }) + + if (!silent) + MaskMessages.events.relationsChanged.sendToAll([{ of: record.profile, reason: 'update', favor: record.favor }]) } export async function queryRelations( query: (record: RelationRecord) => boolean, t?: RelationTransaction<'readonly'>, ): Promise { - return [] + const results = await nativeAPI?.api.query_relations({}) + + if (!results?.length) return [] + return results.map((x) => relationRecordOutDB(x)) } export async function queryRelationsPagedDB( @@ -210,10 +316,24 @@ export async function queryRelationsPagedDB( options: { network: string after?: RelationRecord + pageOffset?: number }, count: number, ): Promise { - return [] + const results = await nativeAPI?.api.query_relations({ + options: { + network: options.network, + pageOption: options.pageOffset + ? { + pageSize: count, + pageOffset: options.pageOffset, + } + : undefined, + }, + }) + + if (!results?.length) return [] + return results.map((x) => relationRecordOutDB(x)) } /** @@ -226,19 +346,104 @@ export async function updateRelationDB( updating: Omit, t?: RelationTransaction<'readwrite'>, silent = false, -): Promise {} +): Promise { + await nativeAPI?.api.update_relation({ + relation: relationRecordToDB(updating), + }) + if (!silent) { + MaskMessages.events.relationsChanged.sendToAll([ + { of: updating.profile, favor: updating.favor, reason: 'update' }, + ]) + } +} // #region out db & to db -function personaRecordOutDB(x: NativePersonaRecord): PersonaRecord { +function personaRecordToDB(x: PersonaRecord): NativePersonaRecord { return { ...x, + publicKey: x.publicKey as JsonWebKey as unknown as Native_EC_Public_JsonWebKey, + privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + identifier: x.identifier.toText(), + linkedProfiles: x.linkedProfiles.__raw_map__, + createdAt: x.createdAt.getTime(), + updatedAt: x.createdAt.getTime(), + } +} + +function partialPersonaRecordToDB( + x: Readonly & Pick>, +): Partial { + return { + publicKey: x.publicKey as JsonWebKey as unknown as Native_EC_Public_JsonWebKey, + privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + identifier: x.identifier.toText(), + linkedProfiles: x.linkedProfiles?.__raw_map__, + createdAt: x.createdAt?.getTime(), + updatedAt: x.createdAt?.getTime(), + } +} + +function personaRecordOutDB(x: NativePersonaRecord): PersonaRecord { + return { publicKey: x.publicKey as JsonWebKey as unknown as EC_Public_JsonWebKey, privateKey: x.privateKey as JsonWebKey as unknown as EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), - createdAt: new Date(x.createAt), - updatedAt: new Date(x.updateAt), + createdAt: new Date(x.createdAt), + updatedAt: new Date(x.updatedAt), + } +} + +function profileRecordToDB(x: ProfileRecord): NativeProfileRecord { + return { + ...x, + identifier: x.identifier.toText(), + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + linkedPersona: x.linkedPersona?.toText(), + createdAt: x.createdAt.getTime(), + updatedAt: x.updatedAt.getTime(), + } +} + +function profileRecordOutDB(x: NativeProfileRecord): ProfileRecord { + return { + localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, + identifier: Identifier.fromString(x.identifier, ProfileIdentifier).unwrap(), + linkedPersona: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + createdAt: new Date(x.createdAt), + updatedAt: new Date(x.updatedAt), + } +} + +function partialProfileRecordToDB( + x: Readonly & Pick>, +): Partial { + return { + ...x, + identifier: x.identifier.toText(), + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + linkedPersona: x.linkedPersona?.toText(), + createdAt: x.createdAt?.getTime(), + updatedAt: x.updatedAt?.getTime(), + } +} + +function relationRecordToDB(x: Omit): Omit { + return { + ...x, + profile: x.profile.toText(), + linked: x.linked.toText(), + } +} + +function relationRecordOutDB(x: NativeRelationRecord): RelationRecord { + return { + ...x, + profile: Identifier.fromString(x.profile, ProfileIdentifier).unwrap(), + linked: Identifier.fromString(x.linked, ECKeyIdentifier).unwrap(), } } // #endregion diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 0fb418dc2742..449ef3fa6c4a 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -382,19 +382,29 @@ export async function queryProfileDB( * Query many profiles. */ export async function queryProfilesDB( - network: string | ((record: ProfileRecord) => boolean), + query: { + network?: string + identifiers?: ProfileIdentifier[] + hasLinkedPersona?: boolean + }, t?: ProfileTransaction<'readonly'>, ): Promise { t = t || createTransaction(await db(), 'readonly')('profiles') const result: ProfileRecord[] = [] - if (typeof network === 'string') { - result.push( - ...(await t.objectStore('profiles').index('network').getAll(IDBKeyRange.only(network))).map(profileOutDB), - ) - } else { + + if (query.network) { + const results = await t.objectStore('profiles').index('network').getAll(IDBKeyRange.only(query.network)) + + results.forEach((each) => { + const out = profileOutDB(each) + if (query.hasLinkedPersona && !out.linkedPersona) return + result.push(out) + }) + } else if (query.identifiers?.length) { for await (const each of t.objectStore('profiles').iterate()) { const out = profileOutDB(each.value) - if (network(out)) result.push(out) + if (query.hasLinkedPersona && !out.linkedPersona) continue + if (query.identifiers.some((x) => out.identifier.equals(x))) result.push(out) } } return result @@ -410,6 +420,11 @@ const fuse = new Fuse([] as ProfileRecord[], { ], }) +/** + * @deprecated + * @param options + * @param count + */ export async function queryProfilesPagedDB( options: { after?: ProfileIdentifier @@ -560,6 +575,7 @@ export async function queryRelationsPagedDB( options: { network: string after?: RelationRecord + pageOffset?: number }, count: number, ) { diff --git a/packages/mask/src/database/Persona/helpers.ts b/packages/mask/src/database/Persona/helpers.ts index b6a6565ca503..b38ecb8a4203 100644 --- a/packages/mask/src/database/Persona/helpers.ts +++ b/packages/mask/src/database/Persona/helpers.ts @@ -102,8 +102,8 @@ export async function queryPersona(identifier: PersonaIdentifier): Promise[0]): Promise { - const _ = await queryProfilesDB(query || ((_) => true)) +export async function queryProfilesWithQuery(query: Parameters[0]): Promise { + const _ = await queryProfilesDB(query) return Promise.all(_.map(profileRecordToProfile)) } diff --git a/packages/mask/src/extension/background-script/CryptoServices/debugShowAllPossibleHashForPost.ts b/packages/mask/src/extension/background-script/CryptoServices/debugShowAllPossibleHashForPost.ts index 1684afe3a79c..93fdb4b2c5c3 100644 --- a/packages/mask/src/extension/background-script/CryptoServices/debugShowAllPossibleHashForPost.ts +++ b/packages/mask/src/extension/background-script/CryptoServices/debugShowAllPossibleHashForPost.ts @@ -4,7 +4,7 @@ import { queryProfilesWithQuery, queryPublicKey } from '../../../database' import { getNetworkWorkerUninitialized } from '../../../social-network/worker' export async function debugShowAllPossibleHashForPost(post: PostIVIdentifier, payloadVersion: -38 | -39 | -40) { - const friends = await queryProfilesWithQuery((x) => x.identifier.network === post.network) + const friends = await queryProfilesWithQuery({ network: post.network }) const gunHint = getNetworkWorkerUninitialized(post.network)?.gunNetworkHint return Promise.all( friends diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 9e9b3ca4356a..805d301f3962 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -65,11 +65,11 @@ export { validateMnemonic } from '../../utils/mnemonic-code' export { queryProfile, queryProfilePaged } from '../../database' export function queryProfiles(network?: string): Promise { - return queryProfilesWithQuery(network) + return queryProfilesWithQuery({ network }) } export function queryProfilesWithIdentifiers(identifiers: ProfileIdentifier[]) { - return queryProfilesWithQuery((record) => identifiers.some((x) => record.identifier.equals(x))) + return queryProfilesWithQuery({ identifiers }) } export async function queryMyProfiles(network?: string): Promise { const myPersonas = (await queryMyPersonas(network)).filter((x) => !x.uninitialized) @@ -304,10 +304,15 @@ export async function createNewRelation( await createRelationDB({ profile, linked, favor }, t) } +export async function queryRelations(): Promise { + return queryRelations() +} + export async function queryRelationPaged( options: { network: string after?: RelationRecord + pageOffset?: number }, count: number, ): Promise { @@ -332,7 +337,7 @@ export async function resolveIdentity(identifier: ProfileIdentifier): Promise x.identifier.equals(unknown) || x.identifier.equals(self)) + const r = await queryProfilesDB({ identifiers: [unknown, self] }) if (!r.length) return const final = { ...r.reduce((p, c) => ({ ...p, ...c })), diff --git a/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts b/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts index a36f91430a81..2cb66f1475ae 100644 --- a/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts +++ b/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts @@ -84,11 +84,9 @@ export async function generateBackupJSON(opts: Partial = {}): Pro async function backProfiles(of?: ProfileIdentifier[]) { const data = ( - await queryProfilesDB((p) => { - if (of === undefined) return true - if (!of.some((x) => x.equals(p.identifier))) return false - if (!p.linkedPersona) return false - return true + await queryProfilesDB({ + identifiers: of, + hasLinkedPersona: true, }) ).map(ProfileRecordToJSONFormat) profiles.push(...data) diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts index ac00aa79f5e1..0ff1244855ff 100644 --- a/packages/mask/src/utils/native-rpc/Web.ts +++ b/packages/mask/src/utils/native-rpc/Web.ts @@ -340,6 +340,20 @@ export const MaskNetworkAPI: MaskNetworkAPIs = { const { activatedSocialNetworkUI } = await import('../../social-network') return activatedSocialNetworkUI.collecting.identityProvider?.recognized.value.identifier.toText() }, + get_all_indexedDB_records: async () => { + const personas = await Services.Identity.queryPersonas() + const profiles = await Services.Identity.queryProfiles() + const relations = await Services.Identity.queryRelations() + return { + personas: personas.map(personaFormatter), + profiles: profiles.map(profileFormatter), + relations: relations.map((x) => ({ + ...x, + profile: x.profile.toText(), + linked: x.linked.toText(), + })), + } + }, } function wrapWithAssert(env: Environment, f: Function) { diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index bbabb204db65..8dcfb8f4a37f 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -1,4 +1,12 @@ -import type { JsonRpcPayload, JsonRpcResponse, PersonaRecord, PageOption, ProfileRecord } from './types' +import type { + JsonRpcPayload, + JsonRpcResponse, + PersonaRecord, + PageOption, + ProfileRecord, + LinkedProfileDetails, + RelationRecord, +} from './types' /** * APIs that both Android and iOS implements and have the same API signature @@ -14,6 +22,7 @@ export interface SharedNativeAPIs { /** * DB JSON RPC */ + create_persona(params: { persona: PersonaRecord }): Promise query_persona(params: { identifier?: string hasPrivateKey?: boolean @@ -28,35 +37,66 @@ export interface SharedNativeAPIs { nameContains?: string pageOption?: PageOption }): Promise + query_persona_by_profile(params: { + options?: { + profileIdentifier: string + hasPrivateKey?: boolean + includeLogout?: boolean + nameContains?: string + pageOption?: PageOption + } + }): Promise update_persona(params: { - persona: PersonaRecord - options: { + persona: Partial + options?: { linkedProfileMergePolicy?: 0 | 1 deleteUndefinedFields?: boolean protectPrivateKey?: boolean createWhenNotExist?: boolean } }): Promise - delete_persona(params: { identifier: string; options: { safeDelete?: boolean } }): Promise + delete_persona(params: { identifier: string; options?: { safeDelete?: boolean } }): Promise + create_profile(params: { profile: ProfileRecord }): Promise + query_profile(params: { + options?: { + identifier: string + network?: string + nameContains?: string + pageOption?: PageOption + } + }): Promise query_profiles(params: { - identifier?: string + identifiers?: string[] network?: string nameContains?: string - pageOption: PageOption - }): Promise + hasLinkedPersona?: boolean + pageOption?: PageOption + }): Promise update_profile(params: { - profile: ProfileRecord - options: { + profile: Partial + options?: { createWhenNotExist?: boolean } }): Promise delete_profile(params: { identifier: string }): Promise - attachProfile(params: { + attach_profile(params: { profileIdentifier: string personaIdentifier: string - state: string // ? + state: LinkedProfileDetails }): Promise - deattachProfile(params: { identifier: string }): Promise + detach_profile(params: { identifier: string }): Promise + create_relation(params: { relation: Omit }): Promise + query_relations(params: { + options?: { + personaIdentifier?: string + network?: string + nameContains?: string + favor?: string + pageOption?: PageOption + } + }): Promise + update_relation(params: { relation: Omit }): Promise + delete_relation(params: { personaIdentifier: string; profileIdentifier: string }): Promise } /** * APIs that only implemented by iOS Mask Network diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index b7b22d31ea9f..7d4bd094fee8 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -37,8 +37,8 @@ export interface PersonaRecord { localKey?: AESJsonWebKey nickname?: string linkedProfiles: Map - createAt: number - updateAt: number + createdAt: number + updatedAt: number hasLogout?: boolean uninitialized?: boolean } @@ -48,8 +48,21 @@ export interface ProfileRecord { nickname?: string localKey?: AESJsonWebKey linkedPersona?: string - createAt?: number - updateAt?: number + createdAt: number + updatedAt: number +} + +export enum RelationFavor { + COLLECTED = -1, + UNCOLLECTED = 1, + DEPRECATED = 0, +} + +export interface RelationRecord { + profile: string + linked: string + network: string + favor: RelationFavor } export type PageOption = { diff --git a/packages/public-api/src/web.ts b/packages/public-api/src/web.ts index 5bde212caefb..3961d179e40e 100644 --- a/packages/public-api/src/web.ts +++ b/packages/public-api/src/web.ts @@ -1,4 +1,7 @@ +import type { RelationFavor, RelationRecord } from './types' + // This interface uses by-name style JSON RPC. + /** * Methods starts with "SNSAdaptor_" can only be called in SNS Adaptor. * Other methods can only be called in the background page. @@ -78,19 +81,11 @@ export interface MaskNetworkAPIs { wallet_updateEthereumChainId(params: { chainId: number }): Promise wallet_getLegacyWalletInfo(): Promise SNSAdaptor_getCurrentDetectedProfile(): Promise -} - -export interface RelationRecord { - profile: ProfileIdentifier_string - linked: PersonaIdentifier_string - network: string - favor: RelationFavor -} - -export enum RelationFavor { - COLLECTED = -1, - UNCOLLECTED = 1, - DEPRECATED = 0, + get_all_indexedDB_records(): Promise<{ + personas: Persona[] + profiles: Profile[] + relations: RelationRecord[] + }> } export interface WalletInfo { From 3f01be0c037586d92b259c5051ca3d596faf87ba Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Fri, 21 Jan 2022 15:32:35 +0800 Subject: [PATCH 13/18] fix: recovery method --- .../mask/background/database/persona/app.ts | 8 ++++- .../mask/background/database/persona/web.ts | 17 +++++++-- packages/mask/src/database/Persona/helpers.ts | 6 ++-- .../background-script/IdentityService.ts | 10 +++++- .../WelcomeServices/generateBackupJSON.ts | 10 +++--- packages/mask/src/utils/native-rpc/Web.ts | 35 ++++++++++++++++--- packages/public-api/src/mobile.ts | 3 ++ packages/public-api/src/web.ts | 6 ++-- 8 files changed, 74 insertions(+), 21 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index fda500fd75a9..7beddca7130c 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -68,11 +68,17 @@ export async function queryPersonaDB( } export async function queryPersonasDB( - query: (record: PersonaRecord) => boolean, + query: { + identifiers?: PersonaIdentifier[] + hasPrivateKey?: boolean + nameContains?: string + initialized?: boolean + }, t?: PersonasTransaction<'readonly'>, isIncludeLogout?: boolean, ): Promise { const results = await nativeAPI?.api.query_personas({ + ...query, includeLogout: isIncludeLogout, }) diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 449ef3fa6c4a..21a0f1c63cd6 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -223,7 +223,12 @@ export async function queryPersonaDB( * Query many Personas. */ export async function queryPersonasDB( - query: (record: PersonaRecord) => boolean, + query?: { + identifiers?: PersonaIdentifier[] + hasPrivateKey?: boolean + nameContains?: string + initialized?: boolean + }, t?: PersonasTransaction<'readonly'>, isIncludeLogout?: boolean, ): Promise { @@ -231,7 +236,15 @@ export async function queryPersonasDB( const records: PersonaRecord[] = [] for await (const each of t.objectStore('personas')) { const out = personaRecordOutDB(each.value) - if (query(out) && (isIncludeLogout || !out.hasLogout)) records.push(out) + if ( + (query?.hasPrivateKey && !out.privateKey) || + (query?.nameContains && out.nickname !== query.nameContains) || + (query?.identifiers && !query.identifiers.some((x) => x.equals(out.identifier))) || + (query?.initialized && out.uninitialized) + ) + continue + + if (isIncludeLogout || !out.hasLogout) records.push(out) } return records } diff --git a/packages/mask/src/database/Persona/helpers.ts b/packages/mask/src/database/Persona/helpers.ts index b38ecb8a4203..cca56b4c76b7 100644 --- a/packages/mask/src/database/Persona/helpers.ts +++ b/packages/mask/src/database/Persona/helpers.ts @@ -111,7 +111,7 @@ export async function queryProfilesWithQuery(query: Parameters[0]): Promise { - const _ = await queryPersonasDB(query || ((_) => true)) + const _ = await queryPersonasDB(query) return _.map(personaRecordToPersona) } @@ -148,7 +148,7 @@ export async function logoutPersona(identifier: PersonaIdentifier) { } export async function renamePersona(identifier: PersonaIdentifier, nickname: string) { - const personas = await queryPersonasWithQuery(({ nickname: name }) => name === nickname) + const personas = await queryPersonasWithQuery({ nameContains: nickname }) if (personas.length > 0) { throw new Error('Nickname already exists') } @@ -210,7 +210,7 @@ export async function createPersonaByMnemonic( } export async function createPersonaByMnemonicV2(mnemonicWord: string, nickname: string | undefined, password: string) { - const personas = await queryPersonasWithQuery(({ nickname: name }) => name === nickname) + const personas = await queryPersonasWithQuery({ nameContains: nickname }) if (personas.length > 0) throw new Error('Nickname already exists') const verify = validateMnemonic(mnemonicWord) diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 805d301f3962..93accb223828 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -68,6 +68,10 @@ export function queryProfiles(network?: string): Promise { return queryProfilesWithQuery({ network }) } +export async function queryProfileRecord() { + return queryProfilesDB({}) +} + export function queryProfilesWithIdentifiers(identifiers: ProfileIdentifier[]) { return queryProfilesWithQuery({ identifiers }) } @@ -130,12 +134,16 @@ export async function queryPersonaByMnemonic(mnemonic: string, password: '') { } export async function queryPersonas(identifier?: PersonaIdentifier, requirePrivateKey = false): Promise { if (typeof identifier === 'undefined') - return (await queryPersonasDB((k) => (requirePrivateKey ? !!k.privateKey : true))).map(personaRecordToPersona) + return (await queryPersonasDB({ hasPrivateKey: requirePrivateKey })).map(personaRecordToPersona) const x = await queryPersonaDB(identifier) if (!x || (!x.privateKey && requirePrivateKey)) return [] return [personaRecordToPersona(x)] } +export async function queryPersonaRecords() { + return queryPersonasDB() +} + export function queryMyPersonas(network?: string): Promise { return queryPersonas(undefined, true).then((x) => typeof network === 'string' diff --git a/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts b/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts index 2cb66f1475ae..95c1dc98a789 100644 --- a/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts +++ b/packages/mask/src/extension/background-script/WelcomeServices/generateBackupJSON.ts @@ -95,12 +95,10 @@ export async function generateBackupJSON(opts: Partial = {}): Pro async function backupPersonas(of?: PersonaIdentifier[]) { const data = ( await queryPersonasDB( - (p) => { - if (p.uninitialized) return false - if (opts.hasPrivateKeyOnly && !p.privateKey) return false - if (of === undefined) return true - if (!of.some((x) => x.equals(p.identifier))) return false - return true + { + initialized: true, + hasPrivateKey: opts.hasPrivateKeyOnly, + identifiers: of, }, undefined, true, diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts index 0ff1244855ff..fa34336120af 100644 --- a/packages/mask/src/utils/native-rpc/Web.ts +++ b/packages/mask/src/utils/native-rpc/Web.ts @@ -12,6 +12,11 @@ import { WalletRPC } from '../../plugins/Wallet/messages' import { ProviderType } from '@masknet/web3-shared-evm' import { MaskMessages } from '../messages' import type { PersonaInformation } from '@masknet/shared-base' +import type { + EC_Private_JsonWebKey as Native_EC_Private_JsonWebKey, + EC_Public_JsonWebKey as Native_EC_Public_JsonWebKey, + AESJsonWebKey as Native_AESJsonWebKey, +} from '@masknet/public-api' const stringToPersonaIdentifier = (str: string) => Identifier.fromString(str, ECKeyIdentifier).unwrap() const stringToProfileIdentifier = (str: string) => Identifier.fromString(str, ProfileIdentifier).unwrap() @@ -341,16 +346,36 @@ export const MaskNetworkAPI: MaskNetworkAPIs = { return activatedSocialNetworkUI.collecting.identityProvider?.recognized.value.identifier.toText() }, get_all_indexedDB_records: async () => { - const personas = await Services.Identity.queryPersonas() - const profiles = await Services.Identity.queryProfiles() + const personas = await Services.Identity.queryPersonaRecords() + const profiles = await Services.Identity.queryProfileRecord() const relations = await Services.Identity.queryRelations() return { - personas: personas.map(personaFormatter), - profiles: profiles.map(profileFormatter), + personas: personas.map((x) => ({ + mnemonic: x.mnemonic, + nickname: x.nickname, + publicKey: x.publicKey as JsonWebKey as unknown as Native_EC_Public_JsonWebKey, + privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + identifier: x.identifier.toText(), + linkedProfiles: x.linkedProfiles.__raw_map__, + createdAt: x.createdAt.getTime(), + updatedAt: x.createdAt.getTime(), + hasLogout: x.hasLogout, + uninitialized: x.uninitialized, + })), + profiles: profiles.map((x) => ({ + identifier: x.identifier.toText(), + nickname: x.nickname, + localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, + linkedPersona: x.linkedPersona?.toText(), + createdAt: x.createdAt.getTime(), + updatedAt: x.updatedAt.getTime(), + })), relations: relations.map((x) => ({ - ...x, profile: x.profile.toText(), linked: x.linked.toText(), + network: x.network, + favor: x.favor, })), } }, diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index 8dcfb8f4a37f..c9751d0b2778 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -28,6 +28,7 @@ export interface SharedNativeAPIs { hasPrivateKey?: boolean includeLogout?: boolean nameContains?: string + initialized?: boolean pageOption?: PageOption }): Promise query_personas(params: { @@ -35,6 +36,7 @@ export interface SharedNativeAPIs { hasPrivateKey?: boolean includeLogout?: boolean nameContains?: string + initialized?: boolean pageOption?: PageOption }): Promise query_persona_by_profile(params: { @@ -43,6 +45,7 @@ export interface SharedNativeAPIs { hasPrivateKey?: boolean includeLogout?: boolean nameContains?: string + initialized?: boolean pageOption?: PageOption } }): Promise diff --git a/packages/public-api/src/web.ts b/packages/public-api/src/web.ts index 3961d179e40e..6b6f977b6f8f 100644 --- a/packages/public-api/src/web.ts +++ b/packages/public-api/src/web.ts @@ -1,4 +1,4 @@ -import type { RelationFavor, RelationRecord } from './types' +import type { PersonaRecord, ProfileRecord, RelationFavor, RelationRecord } from './types' // This interface uses by-name style JSON RPC. @@ -82,8 +82,8 @@ export interface MaskNetworkAPIs { wallet_getLegacyWalletInfo(): Promise SNSAdaptor_getCurrentDetectedProfile(): Promise get_all_indexedDB_records(): Promise<{ - personas: Persona[] - profiles: Profile[] + personas: PersonaRecord[] + profiles: ProfileRecord[] relations: RelationRecord[] }> } From 65eb0273f117f713df8f6f72e8658a7b00ca4c4e Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Sat, 22 Jan 2022 17:14:45 +0800 Subject: [PATCH 14/18] fix: replace method source to web when call recovery --- .../background-script/IdentityService.ts | 17 +++++++++++------ packages/mask/src/utils/native-rpc/Web.ts | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 93accb223828..928a83c61f0e 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -45,6 +45,11 @@ import { LinkedProfileDetails, RelationRecord, } from '../../../background/database/persona/db' +import { + queryPersonasDB as queryPersonasFromIndexedDB, + queryProfilesDB as queryProfilesFromIndexedDB, + queryRelations as queryRelationsFromIndexedDB, +} from '../../../background/database/persona/web' import { BackupJSONFileLatest, UpgradeBackupJSONFile } from '../../utils/type-transform/BackupFormat/JSON/latest' import { restoreBackup } from './WelcomeServices/restoreBackup' import { restoreNewIdentityWithMnemonicWord } from './WelcomeService' @@ -68,8 +73,8 @@ export function queryProfiles(network?: string): Promise { return queryProfilesWithQuery({ network }) } -export async function queryProfileRecord() { - return queryProfilesDB({}) +export async function queryProfileRecordFromIndexedDB() { + return queryProfilesFromIndexedDB({}) } export function queryProfilesWithIdentifiers(identifiers: ProfileIdentifier[]) { @@ -140,8 +145,8 @@ export async function queryPersonas(identifier?: PersonaIdentifier, requirePriva return [personaRecordToPersona(x)] } -export async function queryPersonaRecords() { - return queryPersonasDB() +export async function queryPersonaRecordsFromIndexedDB() { + return queryPersonasFromIndexedDB() } export function queryMyPersonas(network?: string): Promise { @@ -312,8 +317,8 @@ export async function createNewRelation( await createRelationDB({ profile, linked, favor }, t) } -export async function queryRelations(): Promise { - return queryRelations() +export async function queryRelationsRecordFromIndexedDB(): Promise { + return queryRelationsFromIndexedDB(() => true) } export async function queryRelationPaged( diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts index fa34336120af..a933ddd78723 100644 --- a/packages/mask/src/utils/native-rpc/Web.ts +++ b/packages/mask/src/utils/native-rpc/Web.ts @@ -346,9 +346,9 @@ export const MaskNetworkAPI: MaskNetworkAPIs = { return activatedSocialNetworkUI.collecting.identityProvider?.recognized.value.identifier.toText() }, get_all_indexedDB_records: async () => { - const personas = await Services.Identity.queryPersonaRecords() - const profiles = await Services.Identity.queryProfileRecord() - const relations = await Services.Identity.queryRelations() + const personas = await Services.Identity.queryPersonaRecordsFromIndexedDB() + const profiles = await Services.Identity.queryProfileRecordFromIndexedDB() + const relations = await Services.Identity.queryRelationsRecordFromIndexedDB() return { personas: personas.map((x) => ({ mnemonic: x.mnemonic, From d0c3e8afd57e2258886878c3d68bbb17bb08ea78 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 24 Jan 2022 11:50:58 +0800 Subject: [PATCH 15/18] fix: bugfix --- packages/mask/background/database/persona/app.ts | 2 +- packages/mask/background/database/persona/web.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 7beddca7130c..c02dd2dfe3ea 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -397,7 +397,7 @@ function personaRecordOutDB(x: NativePersonaRecord): PersonaRecord { privateKey: x.privateKey as JsonWebKey as unknown as EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), - linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), + linkedProfiles: new IdentifierMap(new Map(Object.entries(x.linkedProfiles)), ProfileIdentifier), createdAt: new Date(x.createdAt), updatedAt: new Date(x.updatedAt), } diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 21a0f1c63cd6..9c07de0168ca 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -28,6 +28,7 @@ import type { RelationRecordDB, PersonaRecord, } from './type' +import { isEmpty } from 'lodash-unified' /** * Database structure: * @@ -405,6 +406,14 @@ export async function queryProfilesDB( t = t || createTransaction(await db(), 'readonly')('profiles') const result: ProfileRecord[] = [] + if (isEmpty(query)) { + const results = await t.objectStore('profiles').getAll() + results.forEach((each) => { + const out = profileOutDB(each) + result.push(out) + }) + } + if (query.network) { const results = await t.objectStore('profiles').index('network').getAll(IDBKeyRange.only(query.network)) From a1458012699607b8755676e8994fdf73a1ad42b9 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 24 Jan 2022 14:14:16 +0800 Subject: [PATCH 16/18] fix: bugfix --- packages/mask/background/database/persona/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index c02dd2dfe3ea..5ef1f8053fe6 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -418,7 +418,7 @@ function profileRecordOutDB(x: NativeProfileRecord): ProfileRecord { return { localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, identifier: Identifier.fromString(x.identifier, ProfileIdentifier).unwrap(), - linkedPersona: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + linkedPersona: x.linkedPersona ? Identifier.fromString(x.linkedPersona, ECKeyIdentifier).unwrap() : undefined, createdAt: new Date(x.createdAt), updatedAt: new Date(x.updatedAt), } From 431f577fc1fb432328a14c3baf851110137e7b05 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 24 Jan 2022 16:51:42 +0800 Subject: [PATCH 17/18] fix: bugfix --- packages/mask/background/database/persona/app.ts | 4 ++-- packages/mask/src/utils/native-rpc/Web.ts | 2 +- packages/public-api/src/types/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index 5ef1f8053fe6..f3b8f60a8c47 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -371,7 +371,7 @@ function personaRecordToDB(x: PersonaRecord): NativePersonaRecord { privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, identifier: x.identifier.toText(), - linkedProfiles: x.linkedProfiles.__raw_map__, + linkedProfiles: Object.fromEntries(x.linkedProfiles.__raw_map__), createdAt: x.createdAt.getTime(), updatedAt: x.createdAt.getTime(), } @@ -385,7 +385,7 @@ function partialPersonaRecordToDB( privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, identifier: x.identifier.toText(), - linkedProfiles: x.linkedProfiles?.__raw_map__, + linkedProfiles: x.linkedProfiles?.__raw_map__ ? Object.fromEntries(x.linkedProfiles.__raw_map__) : {}, createdAt: x.createdAt?.getTime(), updatedAt: x.createdAt?.getTime(), } diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts index a933ddd78723..c12d3326483d 100644 --- a/packages/mask/src/utils/native-rpc/Web.ts +++ b/packages/mask/src/utils/native-rpc/Web.ts @@ -357,7 +357,7 @@ export const MaskNetworkAPI: MaskNetworkAPIs = { privateKey: x.privateKey as JsonWebKey as unknown as Native_EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as Native_AESJsonWebKey, identifier: x.identifier.toText(), - linkedProfiles: x.linkedProfiles.__raw_map__, + linkedProfiles: Object.fromEntries(x.linkedProfiles.__raw_map__), createdAt: x.createdAt.getTime(), updatedAt: x.createdAt.getTime(), hasLogout: x.hasLogout, diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index 7d4bd094fee8..4210601ae269 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -36,7 +36,7 @@ export interface PersonaRecord { privateKey?: EC_Private_JsonWebKey localKey?: AESJsonWebKey nickname?: string - linkedProfiles: Map + linkedProfiles: Record createdAt: number updatedAt: number hasLogout?: boolean From 082a446949e00e80750a597e7e314795b661a9bc Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Tue, 25 Jan 2022 14:39:49 +0800 Subject: [PATCH 18/18] fix: build error --- packages/mask/src/components/Welcomes/Banner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/components/Welcomes/Banner.tsx b/packages/mask/src/components/Welcomes/Banner.tsx index d1b97f52067c..f12b227f2939 100644 --- a/packages/mask/src/components/Welcomes/Banner.tsx +++ b/packages/mask/src/components/Welcomes/Banner.tsx @@ -7,8 +7,8 @@ import { activatedSocialNetworkUI } from '../../social-network' import { DashboardRoutes } from '@masknet/shared-base' import { MaskSharpIcon } from '../../resources/MaskIcon' import { useMount } from 'react-use' -import { hasNativeAPI, nativeAPI } from '../../utils' import { usePersonaConnectStatus } from '../DataSource/usePersonaConnectStatus' +import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' interface BannerUIProps extends withClasses { description?: string