]> git.codecow.com Git - libnemo.git/commitdiff
Migrate rolodex to IndexedDB. Fix safe methods to only return once transaction is...
authorChris Duncan <chris@zoso.dev>
Mon, 21 Jul 2025 18:26:27 +0000 (11:26 -0700)
committerChris Duncan <chris@zoso.dev>
Mon, 21 Jul 2025 18:26:27 +0000 (11:26 -0700)
src/lib/rolodex.ts
src/lib/workers/safe.ts
src/types.d.ts
test/test.manage-rolodex.mjs

index c33b1c9cbee5a10910c96c96e3c9f3a68d217ded..a7374b00a30bb60c8a4a9a38c6afc7ad8da5654e 100644 (file)
@@ -3,7 +3,7 @@
 
 import { Account } from './account'
 import { bytes, utf8 } from './convert'
-import { RolodexEntry } from '#types'
+import { NamedData } from '#types'
 import { SafeWorker } from '#workers'
 
 /**
@@ -11,8 +11,6 @@ import { SafeWorker } from '#workers'
 * saved under one nickname.
 */
 export class Rolodex {
-       #entries: RolodexEntry[] = []
-
        /**
        * Adds an address to the rolodex under a specific nickname.
        *
@@ -20,10 +18,11 @@ export class Rolodex {
        * the account exists under a different name, update the name. If no matches
        * are found at all, add a new entry.
        *
-       * @param {string} name - Alias for the address
+       * @param {string} name - Contact alias for the address
        * @param {string} address - Nano account address
+       * @returns {Promise<boolean>} Promise for true if name and address are added to the rolodex, else false
        */
-       async add (name: string, address: string): Promise<boolean> {
+       static async add (name: string, address: string): Promise<boolean> {
                if (name == null || name === '') {
                        throw new Error('Name is required for rolodex entries')
                }
@@ -46,88 +45,100 @@ export class Rolodex {
                        .replaceAll('\\', '\\u005c')
                const account = Account.import(address)
 
-               const existingName = await this.getName(account.address)
-               if (existingName == null) {
-                       const { result } = await SafeWorker.assign<boolean>({
-                               method: 'store',
-                               store: 'Rolodex',
-                               password: '',
-                               [address]: utf8.toBytes(name).buffer
-                       })
-                       return result
-               } else if (existingName !== name) {
-                       const { result: isDestroyed } = await SafeWorker.assign<boolean>({
-                               method: 'destroy',
-                               store: 'Rolodex',
-                               name: address
-                       })
-                       if (!isDestroyed) {
-                               throw new Error('Failed to replace existing rolodex entry')
+               try {
+                       const existingName = await this.getName(account.address)
+                       if (existingName === name) {
+                               return true
                        }
-                       const { result: isUpdated } = await SafeWorker.assign<boolean>({
+                       const data: NamedData = {
                                method: 'store',
                                store: 'Rolodex',
-                               password: '',
+                               password: utf8.toBytes('').buffer,
                                [address]: utf8.toBytes(name).buffer
-                       })
-                       return isUpdated
-               } else {
-                       const addresses = await this.getAddresses(name)
-                       if (addresses.some(a => a === account.address)) {
-                               return true
-                       } else {
-                               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
                        }
+                       if (existingName != null) {
+                               const filteredAddresses = (await this.getAddresses(existingName)).filter(a => a !== address).sort()
+                               data[existingName] = utf8.toBytes(JSON.stringify(filteredAddresses)).buffer
+                       }
+                       const existingAddresses = await this.getAddresses(name)
+                       existingAddresses.push(account.address)
+                       data[name] = utf8.toBytes(JSON.stringify(existingAddresses)).buffer
+                       const { result } = await SafeWorker.assign<boolean>(data)
+                       return result
+               } catch (err) {
+                       throw new Error('failed to add address', { cause: err })
                }
        }
 
        /**
-       * Gets the name associated with a specific Nano address from the rolodex.
+       * Removes a Nano address from its related contact in the rolodex.
        *
        * @param {string} address - Nano account address
-       * @returns {Promise<string|null>} Promise for the name associated with the address, or null if not found
+       * @returns {Promise<boolean>} Promise for true if address successfully removed, else false
        */
-       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
+       static async deleteAddress (address: string): Promise<boolean> {
+               const name = await this.getName(address)
+               if (name == null) {
+                       return false
+               }
+               const addresses = (await this.getAddresses(name)).filter(a => a !== address).sort()
+               const { result: isUpdated } = await SafeWorker.assign<boolean>({
+                       method: 'store',
+                       store: 'Rolodex',
+                       password: utf8.toBytes('').buffer,
+                       [name]: utf8.toBytes(JSON.stringify(addresses)).buffer
+               })
+               if (!isUpdated) {
+                       throw new Error('failed to remove address from existing name')
                }
+               const { result } = await SafeWorker.assign<boolean>({
+                       method: 'destroy',
+                       store: 'Rolodex',
+                       [address]: name
+               })
+               return result
        }
 
        /**
-       * Gets all Nano addresses associated with a name from the rolodex.
+       * Removes a contact and its related Nano addresses from the rolodex.
+       *
+       * @param {string} name - Contact name to delete
+       * @returns {Promise<boolean>} Promise for true if name and related addresses successfully removed, else false
+       */
+       static async deleteName (name: string): Promise<boolean> {
+               const data: NamedData = {
+                       method: 'destroy',
+                       store: 'Rolodex',
+                       [name]: name
+               }
+               const addresses = await this.getAddresses(name)
+               for (const address of addresses) {
+                       data[address] = name
+               }
+               const { result } = await SafeWorker.assign<boolean>(data)
+               return result
+       }
+
+       /**
+       * Gets all Nano account addresses associated with a name from the rolodex.
        *
        * @param {string} name - Alias to look up
        * @returns {Promise<string[]>} Promise for a list of Nano addresses associated with the name
        */
-       async getAddresses (name: string): Promise<string[]> {
+       static async getAddresses (name: string): Promise<string[]> {
                try {
                        const response = await SafeWorker.assign<ArrayBuffer>({
                                method: 'fetch',
                                name,
                                store: 'Rolodex',
-                               password: ''
+                               password: utf8.toBytes('').buffer
                        })
-                       const addressesJson = bytes.toUtf8(new Uint8Array(response[name]))
-                       return JSON.parse(addressesJson)
+                       const addresses = response[name]
+                       return addresses
+                               ? JSON.parse(bytes.toUtf8(new Uint8Array(addresses))).sort()
+                               : []
                } catch (err) {
-                       console.log(err)
+                       console.error(err)
                        return []
                }
        }
@@ -137,21 +148,45 @@ export class Rolodex {
        *
        * @returns {Promise<string[]>} Promise for a list of all names stored in the rolodex
        */
-       async getAllNames (): Promise<string[]> {
+       static async getAllNames (): Promise<string[]> {
                try {
                        const response = await SafeWorker.assign<ArrayBuffer>({
                                method: 'export',
                                name: '',
                                store: 'Rolodex',
-                               password: ''
+                               password: utf8.toBytes('').buffer
                        })
                        return Object.keys(response).filter(v => v.slice(0, 5) !== 'nano_')
                } catch (err) {
-                       console.log(err)
+                       console.error(err)
                        return []
                }
        }
 
+       /**
+       * Gets the name associated with a specific Nano address from the rolodex.
+       *
+       * @param {string} address - Nano account address
+       * @returns {Promise<string|null>} Promise for the name associated with the address, or null if not found
+       */
+       static async getName (address: string): Promise<string | null> {
+               try {
+                       const response = await SafeWorker.assign<ArrayBuffer>({
+                               method: 'fetch',
+                               name: address,
+                               store: 'Rolodex',
+                               password: utf8.toBytes('').buffer
+                       })
+                       const name = response[address]
+                       return name
+                               ? bytes.toUtf8(new Uint8Array(name))
+                               : null
+               } catch (err) {
+                       console.error(err)
+                       return null
+               }
+       }
+
        /**
        * Verifies whether the public key of any Nano address saved under a specific
        * name in the rolodex was used to sign a specific set of data.
@@ -161,12 +196,12 @@ export class Rolodex {
        * @param {...string} data - Signed data to verify
        * @returns {Promise<boolean>} True if the signature was used to sign the data, else false
        */
-       async verify (name: string, signature: string, ...data: string[]): Promise<boolean> {
+       static async verify (name: string, signature: string, ...data: string[]): Promise<boolean> {
                const { verify } = await import('./tools.js')
-               const entries = this.#entries.filter(e => e.name === name)
-               for (const entry of entries) {
-                       const key = entry.account.publicKey
-                       const verified = await verify(key, signature, ...data)
+               const addresses = await this.getAddresses(name)
+               for (const address of addresses) {
+                       const { publicKey } = Account.import(address)
+                       const verified = await verify(publicKey, signature, ...data)
                        if (verified) {
                                return true
                        }
@@ -174,3 +209,4 @@ export class Rolodex {
                return false
        }
 }
+
index 725168841eb8ed28a266b20ff98c6193bf0169e8..12f05bd48d8c838d4fe0aeeb420cec76c7f95d76 100644 (file)
@@ -51,14 +51,14 @@ export class Safe extends WorkerInterface {
                                        return await this.fetch(name, store, password, true)
                                }
                                case 'destroy': {
-                                       return { result: await this.destroy(name, store) }
+                                       return { result: await this.destroy(data, store) }
                                }
                                default: {
                                        throw new Error(`unknown Safe method ${method}`)
                                }
                        }
                } catch (err) {
-                       console.log(err)
+                       console.error(err)
                        throw new Error('Safe error', { cause: err })
                }
        }
@@ -66,11 +66,11 @@ export class Safe extends WorkerInterface {
        /**
        * Removes data from the Safe without decrypting.
        */
-       static async destroy (name: string, store: string): Promise<boolean> {
+       static async destroy (data: NamedData, store: string): Promise<boolean> {
                try {
-                       return await this.#delete(name, store)
+                       return await this.#delete(Object.keys(data), store)
                } catch (err) {
-                       console.log(err)
+                       console.error(err)
                        throw new Error(this.ERR_MSG)
                }
        }
@@ -131,8 +131,8 @@ export class Safe extends WorkerInterface {
                        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')
+                       if (records == null) {
+                               throw new Error('')
                        }
                        const decryptionKeys: { [salt: string]: CryptoKey } = {}
                        for (const record of records) {
@@ -144,7 +144,7 @@ export class Safe extends WorkerInterface {
                        }
                        return results
                } catch (err) {
-                       console.log(err)
+                       console.error(err)
                        throw new Error('Failed to get records', { cause: err })
                } finally {
                        bytes.erase(password)
@@ -166,15 +166,33 @@ export class Safe extends WorkerInterface {
                return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
        }
 
-       static async #delete (name: string, store: string): Promise<boolean> {
+       static async #delete (names: string[], store: string): Promise<boolean> {
                const transaction = this.#storage.transaction(store, 'readwrite')
                const db = transaction.objectStore(store)
                return new Promise((resolve, reject) => {
-                       const request = db.delete(name)
-                       request.onsuccess = (event) => {
-                               resolve((event.target as IDBRequest).result)
+                       const requests = names.map(name => {
+                               const request = db.delete(name)
+                               request.onsuccess = (event) => {
+                                       console.log('delete request successful but not yet committed')
+                               }
+                               request.onerror = (event) => {
+                                       console.error('getAll request error before transaction committed')
+                               }
+                               return request
+                       })
+                       transaction.oncomplete = (event) => {
+                               console.log('delete transaction committed')
+                               for (const request of requests) {
+                                       if (request?.error != null) {
+                                               reject(request.error)
+                                       }
+                                       if (request.result !== undefined) {
+                                               resolve(false)
+                                       }
+                               }
+                               resolve(true)
                        }
-                       request.onerror = (event) => {
+                       transaction.onerror = (event) => {
                                console.error('Database error')
                                reject((event.target as IDBRequest).error)
                        }
@@ -185,9 +203,24 @@ export class Safe extends WorkerInterface {
                const transaction = this.#storage.transaction(store, 'readonly')
                const db = transaction.objectStore(store)
                return new Promise((resolve, reject) => {
-                       const requests = names.map(name => db.get(name))
+                       const requests = names.map(name => {
+                               const request = db.get(name)
+                               request.onsuccess = (event) => {
+                                       console.log('get request successful but not yet committed')
+                               }
+                               request.onerror = (event) => {
+                                       console.error('get request error before transaction committed')
+                               }
+                               return request
+                       })
                        transaction.oncomplete = (event) => {
-                               const results = requests.map(r => r.result)
+                               console.log('get transaction committed')
+                               const results = []
+                               for (const request of requests) {
+                                       if (request?.error == null && request.result != null) {
+                                               results.push(request.result)
+                                       }
+                               }
                                resolve(results)
                        }
                        transaction.onerror = (event) => {
@@ -203,9 +236,22 @@ export class Safe extends WorkerInterface {
                return new Promise((resolve, reject) => {
                        const request = db.getAll()
                        request.onsuccess = (event) => {
-                               resolve((event.target as IDBRequest).result)
+                               console.log('getAll request successful but transaction not yet committed')
                        }
                        request.onerror = (event) => {
+                               console.error('getAll request error before transaction committed')
+                       }
+                       transaction.oncomplete = (event) => {
+                               console.log('getAll transaction committed')
+                               if (request?.error != null) {
+                                       reject(request.error)
+                               } else if (request.result == null) {
+                                       reject('getAll request failed')
+                               } else {
+                                       resolve(request.result)
+                               }
+                       }
+                       transaction.onerror = (event) => {
                                console.error('Database error')
                                reject((event.target as IDBRequest).error)
                        }
@@ -246,9 +292,25 @@ export class Safe extends WorkerInterface {
                const transaction = this.#storage.transaction(store, 'readwrite')
                const db = transaction.objectStore(store)
                return new Promise((resolve, reject) => {
-                       records.map(record => db.put(record, record.label))
+                       const requests = records.map(record => {
+                               const request = db.put(record, record.label)
+                               request.onsuccess = (event) => {
+                                       console.log('put request successful but not yet committed')
+                               }
+                               request.onerror = (event) => {
+                                       console.error('put request error before transaction committed')
+                               }
+                               return request
+                       })
                        transaction.oncomplete = (event) => {
-                               resolve((event.target as IDBRequest).error == null)
+                               console.log('put transaction committed')
+                               const results = []
+                               for (const request of requests) {
+                                       if (request?.error == null && request.result != null) {
+                                               results.push(request.result)
+                                       }
+                               }
+                               resolve(results.length > 0)
                        }
                        transaction.onerror = (event) => {
                                console.error('Database error')
index 8bc350aefcd0674155393af76a490db8323135e7..fd44dd57f9f49bfad495ef947f818d65893a5595 100644 (file)
@@ -310,12 +310,6 @@ export declare const Bip44CkdWorker: Queue
 export declare const NanoNaClWorker: Queue
 export declare const SafeWorker: Queue
 
-
-export type RolodexEntry = {
-       name: string
-       account: Account
-}
-
 /**
 * Represents a Nano network node. It primarily consists of a URL which will
 * accept RPC calls, and an optional API key header construction can be passed if
index c0b8a7b0e6d1235fab493815d7bfea3a577cb227..b05394bbf020d2a65c5f124e73e425f32dc90365 100644 (file)
@@ -9,117 +9,174 @@ import { Rolodex, Tools } from '../dist/main.min.js'
 
 await suite('Rolodex valid contact management', async () => {
 
-       await test('should create a rolodex and add two contacts', async () => {
-               const rolodex = new Rolodex()
-               assert.equal(rolodex.constructor, Rolodex)
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               await rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1)
-
-               assert.equal(rolodex.getAllNames().length, 2)
-               assert.equal(rolodex.getAllNames()[0], 'JohnDoe')
-               assert.equal(rolodex.getAllNames()[1], 'JaneSmith')
-               assert.equal(rolodex.getAddresses('JohnDoe').length, 1)
-               assert.equal(rolodex.getAddresses('JohnDoe')[0], NANO_TEST_VECTORS.ADDRESS_0)
-               assert.equal(rolodex.getAddresses('JaneSmith').length, 1)
-               assert.equal(rolodex.getAddresses('JaneSmith')[0], NANO_TEST_VECTORS.ADDRESS_1)
+       await test('export returns empty array for empty rolodex (delete db if not already empty)', async () => {
+               const result = await Rolodex.getAllNames()
+
+               assert.ok(Array.isArray(result))
+               assert.equal(result.length, 0)
        })
 
-       await test('should get a name from an address', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               assert.equal(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_0), 'JohnDoe')
+       await test('add two contacts, then delete them', async () => {
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+               await assert.resolves(Rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1))
+
+               let names = await Rolodex.getAllNames()
+               assert.ok(Array.isArray(names))
+               assert.equal(names.length, 2)
+               assert.equal(names[0], 'JaneSmith')
+               assert.equal(names[1], 'JohnDoe')
+
+               const addressesJohnDoe = await Rolodex.getAddresses('JohnDoe')
+               const addressesJaneSmith = await Rolodex.getAddresses('JaneSmith')
+
+               assert.ok(Array.isArray(addressesJohnDoe))
+               assert.equal(addressesJohnDoe.length, 1)
+               assert.equal(addressesJohnDoe[0], NANO_TEST_VECTORS.ADDRESS_0)
+               assert.ok(Array.isArray(addressesJaneSmith))
+               assert.equal(addressesJaneSmith.length, 1)
+               assert.equal(addressesJaneSmith[0], NANO_TEST_VECTORS.ADDRESS_1)
+
+               const deleteJohnDoe = await Rolodex.deleteName('JohnDoe')
+               const deleteJaneSmith = await Rolodex.deleteName('JaneSmith')
+               assert.equal(deleteJohnDoe, true)
+               assert.equal(deleteJaneSmith, true)
+
+               names = await Rolodex.getAllNames()
+               assert.ok(Array.isArray(names))
+               assert.equal(names.length, 0)
        })
 
-       await test('should add three addresses to the same contact', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_1)
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_2)
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               assert.equal(rolodex.getAddresses('JohnDoe').length, 3)
-               assert.equal(rolodex.getAddresses('JohnDoe')[0], NANO_TEST_VECTORS.ADDRESS_1)
-               assert.equal(rolodex.getAddresses('JohnDoe')[1], NANO_TEST_VECTORS.ADDRESS_2)
-               assert.equal(rolodex.getAddresses('JohnDoe')[2], NANO_TEST_VECTORS.ADDRESS_0)
+       await test('get a name from an address', async () => {
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+
+               const name = await Rolodex.getName(NANO_TEST_VECTORS.ADDRESS_0)
+               assert.equal(name, 'JohnDoe')
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
        })
 
-       await test('should update the name on an existing entry', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               await rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_0)
-               assert.equal(rolodex.getAddresses('JohnDoe').length, 0)
-               assert.equal(rolodex.getAddresses('JaneSmith').length, 1)
-               assert.equal(rolodex.getAddresses('JaneSmith')[0], NANO_TEST_VECTORS.ADDRESS_0)
+       await test('add three addresses to the same contact, then delete one address, then delete the contact', async () => {
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_1))
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_2))
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+
+               let addresses = await Rolodex.getAddresses('JohnDoe')
+               assert.ok(Array.isArray(addresses))
+               assert.equal(addresses.length, 3)
+               assert.equal(addresses[0], NANO_TEST_VECTORS.ADDRESS_0)
+               assert.equal(addresses[1], NANO_TEST_VECTORS.ADDRESS_2)
+               assert.equal(addresses[2], NANO_TEST_VECTORS.ADDRESS_1)
+
+               await assert.resolves(Rolodex.deleteAddress(NANO_TEST_VECTORS.ADDRESS_1))
+               addresses = await Rolodex.getAddresses('JohnDoe')
+               assert.ok(Array.isArray(addresses))
+               assert.equal(addresses.length, 2)
+               assert.equal(addresses[0], NANO_TEST_VECTORS.ADDRESS_0)
+               assert.equal(addresses[1], NANO_TEST_VECTORS.ADDRESS_2)
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
+               addresses = await Rolodex.getAddresses('JohnDoe')
+               const address0 = await Rolodex.getName(NANO_TEST_VECTORS.ADDRESS_0)
+               const address1 = await Rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1)
+               const address2 = await Rolodex.getName(NANO_TEST_VECTORS.ADDRESS_2)
+
+               assert.ok(Array.isArray(addresses))
+               assert.equal(addresses.length, 0)
+               assert.nullish(address0)
+               assert.nullish(address1)
+               assert.nullish(address2)
        })
 
-       await test('should return empty address array for an unknown contact', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               assert.equal(Array.isArray(rolodex.getAddresses('JaneSmith')), true)
-               assert.equal(rolodex.getAddresses('JaneSmith').length, 0)
+       await test('update the name on an existing address', async () => {
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+               await assert.resolves(Rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_0))
+               const addressesJohnDoe = await Rolodex.getAddresses('JohnDoe')
+               const addressesJaneSmith = await Rolodex.getAddresses('JaneSmith')
+
+               assert.ok(Array.isArray(addressesJohnDoe))
+               assert.equal(addressesJohnDoe.length, 0)
+               assert.ok(Array.isArray(addressesJaneSmith))
+               assert.equal(addressesJaneSmith.length, 1)
+               assert.equal(addressesJaneSmith[0], NANO_TEST_VECTORS.ADDRESS_0)
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
+               await assert.resolves(Rolodex.deleteName('JaneSmith'))
        })
 
-       await test('should return empty address array for blank contact names', () => {
-               const rolodex = new Rolodex()
-               //@ts-expect-error
-               assert.equal(Array.isArray(rolodex.getAddresses(undefined)), true)
-               //@ts-expect-error
-               assert.equal(rolodex.getAddresses(undefined).length, 0)
+       await test('return empty address array for an unknown contact', async () => {
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+               const addressesJaneSmith = await Rolodex.getAddresses('JaneSmith')
+
+               assert.equal(Array.isArray(addressesJaneSmith), true)
+               assert.equal(addressesJaneSmith.length, 0)
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
+       })
+
+       await test('return empty address array for blank contact names', async () => {
                //@ts-expect-error
-               assert.equal(Array.isArray(rolodex.getAddresses(null)), true)
+               const addressesUndefined = await Rolodex.getAddresses(undefined)
                //@ts-expect-error
-               assert.equal(rolodex.getAddresses(null).length, 0)
-               assert.equal(Array.isArray(rolodex.getAddresses('')), true)
-               assert.equal(rolodex.getAddresses('').length, 0)
+               const addressesNull = await Rolodex.getAddresses(null)
+               const addressesBlank = await Rolodex.getAddresses('')
+
+               assert.equal(Array.isArray(addressesUndefined), true)
+               assert.equal(addressesUndefined.length, 0)
+               assert.equal(Array.isArray(addressesNull), true)
+               assert.equal(addressesNull.length, 0)
+               assert.equal(Array.isArray(addressesBlank), true)
+               assert.equal(addressesBlank.length, 0)
        })
 
        await test('should return null for an unknown address', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               assert.ok(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1) === null)
-               assert.ok(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1) !== undefined)
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+               const addressUnknown = await Rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1)
+
+               assert.ok(addressUnknown === null)
+               assert.ok(addressUnknown !== undefined)
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
        })
 
        await test('should return null for a blank address', async () => {
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               //@ts-expect-error
-               assert.ok(rolodex.getName(undefined) === null)
-               //@ts-expect-error
-               assert.ok(rolodex.getName(undefined) !== undefined)
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
                //@ts-expect-error
-               assert.ok(rolodex.getName(null) === null)
+               const nameUndefined = await Rolodex.getName(undefined)
                //@ts-expect-error
-               assert.ok(rolodex.getName(null) !== undefined)
-               assert.ok(rolodex.getName('') === null)
-               assert.ok(rolodex.getName('') !== undefined)
-       })
-})
+               const nameNull = await Rolodex.getName(null)
+               const nameBlank = await Rolodex.getName('')
 
-await suite('Rolodex exceptions', async () => {
+               assert.ok(nameUndefined === null)
+               assert.ok(nameUndefined !== undefined)
+               assert.ok(nameNull === null)
+               assert.ok(nameNull !== undefined)
+               assert.ok(nameBlank === null)
+               assert.ok(nameBlank !== undefined)
+
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
+       })
 
-       await test('should throw if adding no data', async () => {
-               const rolodex = new Rolodex()
+       await test('throw if adding no data', async () => {
                //@ts-expect-error
-               await assert.rejects(rolodex.add())
+               await assert.rejects(Rolodex.add())
        })
 
-       await test('should throw if passed no address', async () => {
-               const rolodex = new Rolodex()
+       await test('throw if passed no address', async () => {
                //@ts-expect-error
-               await assert.rejects(rolodex.add('JohnDoe'))
+               await assert.rejects(Rolodex.add('JohnDoe'))
                //@ts-expect-error
-               await assert.rejects(rolodex.add('JohnDoe', undefined))
+               await assert.rejects(Rolodex.add('JohnDoe', undefined))
                //@ts-expect-error
-               await assert.rejects(rolodex.add('JohnDoe', null))
-               await assert.rejects(rolodex.add('JohnDoe', ''))
+               await assert.rejects(Rolodex.add('JohnDoe', null))
+               await assert.rejects(Rolodex.add('JohnDoe', ''))
        })
 
-       await test('should throw if name is blank', async () => {
-               const rolodex = new Rolodex()
+       await test('throw if name is blank', async () => {
                //@ts-expect-error
-               await assert.rejects(rolodex.add(undefined, NANO_TEST_VECTORS.ADDRESS_0))
+               await assert.rejects(Rolodex.add(undefined, NANO_TEST_VECTORS.ADDRESS_0))
                //@ts-expect-error
-               await assert.rejects(rolodex.add(null, NANO_TEST_VECTORS.ADDRESS_0))
-               await assert.rejects(rolodex.add('', NANO_TEST_VECTORS.ADDRESS_0))
+               await assert.rejects(Rolodex.add(null, NANO_TEST_VECTORS.ADDRESS_0))
+               await assert.rejects(Rolodex.add('', NANO_TEST_VECTORS.ADDRESS_0))
        })
 })
 
@@ -128,18 +185,18 @@ await suite('Rolodex data signature verification', async () => {
        await test('should verify valid data and signature', async () => {
                const data = 'Test data'
                const signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0, data)
-               const rolodex = new Rolodex()
-               await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)
-               const result = await rolodex.verify('JohnDoe', signature, data)
+               await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0))
+               const result = await Rolodex.verify('JohnDoe', signature, data)
+               await assert.resolves(Rolodex.deleteName('JohnDoe'))
                assert.equal(result, true)
        })
 
        await test('should reject incorrect contact for signature', async () => {
                const data = 'Test data'
                const signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0, data)
-               const rolodex = new Rolodex()
-               await rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1)
-               const result = await rolodex.verify('JaneSmith', signature, data)
+               await assert.resolves(Rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1))
+               const result = await Rolodex.verify('JaneSmith', signature, data)
+               await assert.resolves(Rolodex.deleteName('JaneSmith'))
                assert.equal(result, false)
        })
 })