// SPDX-License-Identifier: GPL-3.0-or-later
import { Account } from './account'
+import { bytes, utf8 } from './convert'
import { RolodexEntry } from '#types'
+import { SafeWorker } from '#workers'
/**
* Represents a basic address book of Nano accounts. Multiple addresses can be
*
* If the name exists, add the address as a new association to that name. If
* the account exists under a different name, update the name. If no matches
- * are found at all, add a completely new entry.
+ * are found at all, add a new entry.
*
* @param {string} name - Alias for the address
* @param {string} address - Nano account address
*/
- async add (name: string, address: string): Promise<void> {
+ async add (name: string, address: string): Promise<boolean> {
if (name == null || name === '') {
throw new Error('Name is required for rolodex entries')
}
if (typeof name !== 'string') {
throw new Error('Name must be a string for rolodex entries')
}
+ if (name.slice(0, 5) === 'nano_' || name.slice(0, 4) === 'xrb_') {
+ throw new Error('Name cannot start with an address prefix')
+ }
if (address == null || address === '') {
throw new Error('Address is required for rolodex entries')
}
.replaceAll('>', '\\u003d')
.replaceAll('\\', '\\u005c')
const account = Account.import(address)
- const nameResult = this.#entries.find(e => e.name === name)
- const accountResult = this.#entries.find(e => e.account.address === address)
- if (!accountResult) {
- this.#entries.push({ name, account })
- } else if (!nameResult) {
- accountResult.name = name
+
+ const nameResult = await this.getName(account.address)
+ if (nameResult == null) {
+ const { result } = await SafeWorker.assign<boolean>({
+ method: 'store',
+ store: 'Rolodex',
+ password: '',
+ [address]: utf8.toBytes(name).buffer
+ })
+ return result
}
+
+ const addresses = await this.getAddresses(name)
+ if (addresses.length > 0) {
+ addresses.push(account.address)
+ const addressesJson = JSON.stringify(addresses)
+ const { result } = await SafeWorker.assign<boolean>({
+ method: 'store',
+ store: 'Rolodex',
+ password: '',
+ [name]: utf8.toBytes(addressesJson).buffer
+ })
+ return result
+ }
+
+ return false
}
/**
* Gets the name associated with a specific Nano address from the rolodex.
*
* @param {string} address - Nano account address
- * @returns {string|null} Name associated with the address, or null if not found
+ * @returns {Promise<string|null>} Promise for the name associated with the address, or null if not found
*/
- getName (address: string): string | null {
- const result = this.#entries.find(e => e.account.address === address)
- return result?.name ?? null
+ async getName (address: string): Promise<string | null> {
+ try {
+ const response = await SafeWorker.assign<ArrayBuffer>({
+ method: 'fetch',
+ name: address,
+ store: 'Rolodex',
+ password: ''
+ })
+ return bytes.toUtf8(new Uint8Array(response[address]))
+ } catch (err) {
+ console.log(err)
+ return null
+ }
}
/**
* Gets all Nano addresses associated with a name from the rolodex.
*
* @param {string} name - Alias to look up
- * @returns {string[]} List of Nano addresses associated with the name
+ * @returns {Promise<string[]>} Promise for a list of Nano addresses associated with the name
*/
- getAddresses (name: string): string[] {
- const entries = this.#entries.filter(e => e.name === name)
- return entries.map(a => a.account.address)
+ async getAddresses (name: string): Promise<string[]> {
+ try {
+ const response = await SafeWorker.assign<ArrayBuffer>({
+ method: 'fetch',
+ name,
+ store: 'Rolodex',
+ password: ''
+ })
+ const addressesJson = bytes.toUtf8(new Uint8Array(response[name]))
+ return JSON.parse(addressesJson)
+ } catch (err) {
+ console.log(err)
+ return []
+ }
}
/**
* Gets all names stored in the rolodex.
*
- * @returns {string[]} List of names stored in the rolodex
+ * @returns {Promise<string[]>} Promise for a list of all names stored in the rolodex
*/
- getAllNames (): string[] {
- return this.#entries.map(e => e.name)
+ async getAllNames (): Promise<string[]> {
+ try {
+ const response = await SafeWorker.assign<ArrayBuffer>({
+ method: 'export',
+ name: '',
+ store: 'Rolodex',
+ password: ''
+ })
+ return Object.keys(response).filter(v => v.slice(0, 5) !== 'nano_')
+ } catch (err) {
+ console.log(err)
+ return []
+ }
}
/**
case 'fetch': {
return await this.fetch(name, store, password)
}
+ case 'export': {
+ return await this.fetch(name, store, password, true)
+ }
case 'destroy': {
return { result: await this.destroy(name, store) }
}
/**
* Retrieves data from the Safe and decrypts it with a password byte array.
*/
- static async fetch (name: string | string[] | unknown, store: string | unknown, password: ArrayBuffer | unknown): Promise<NamedData<ArrayBuffer>> {
+ static async fetch (name: string | string[] | unknown, store: string | unknown, password: ArrayBuffer | unknown, all: boolean = false): Promise<NamedData<ArrayBuffer>> {
const names = Array.isArray(name) ? name : [name]
if (names.some(v => typeof v !== 'string')) {
throw new Error('Invalid fields')
const results: NamedData<ArrayBuffer> = {}
try {
- const records: SafeRecord[] = await this.#get(fields, store)
+ const records: SafeRecord[] = all
+ ? await this.#getAll(store)
+ : await this.#get(fields, store)
if (records == null || records.length === 0) {
throw new Error('Failed to find records')
}
})
}
- static async #get (fields: string[], store: string): Promise<SafeRecord[]> {
+ static async #get (names: string[], store: string): Promise<SafeRecord[]> {
const transaction = this.#storage.transaction(store, 'readonly')
const db = transaction.objectStore(store)
return new Promise((resolve, reject) => {
- const requests = fields.map(field => db.get(field))
+ const requests = names.map(name => db.get(name))
transaction.oncomplete = (event) => {
const results = requests.map(r => r.result)
resolve(results)
})
}
+ static async #getAll (store: string): Promise<SafeRecord[]> {
+ const transaction = this.#storage.transaction(store, 'readonly')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const request = db.getAll()
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBRequest).result)
+ }
+ request.onerror = (event) => {
+ console.error('Database error')
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
+
static #isDataValid (data: unknown): asserts data is { [key: string]: ArrayBuffer } {
if (typeof data !== 'object') {
throw new Error('Invalid data')