]> git.codecow.com Git - libnemo.git/commitdiff
Refactor account class.
authorChris Duncan <chris@zoso.dev>
Wed, 16 Jul 2025 06:03:39 +0000 (23:03 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 16 Jul 2025 06:03:39 +0000 (23:03 -0700)
src/lib/account.ts

index f85699dcd710dbf05626db6b3c5e46a39fc9dca9..7a167afcac1650c97e2ed676c3e3227109743aa9 100644 (file)
@@ -18,9 +18,8 @@ export class Account {
        static #isInternal: boolean = false\r
 \r
        #address: string\r
-       #locked: boolean\r
-       #pub: string\r
-       #prv: Uint8Array<ArrayBuffer>\r
+       #publicKey: Uint8Array<ArrayBuffer>\r
+       #privateKey: Uint8Array<ArrayBuffer>\r
 \r
        #balance?: bigint\r
        #frontier?: string\r
@@ -30,10 +29,10 @@ export class Account {
        #weight?: bigint\r
 \r
        get address () { return `${PREFIX}${this.#address}` }\r
-       get isLocked () { return this.#locked }\r
-       get isUnlocked () { return !this.#locked }\r
-       get publicKey () { return this.#pub }\r
-       get privateKey () { return bytes.toHex(this.#prv) }\r
+       get isLocked () { return this.#privateKey.buffer.detached }\r
+       get isUnlocked () { return !this.isLocked }\r
+       get publicKey () { return bytes.toHex(this.#publicKey) }\r
+       get privateKey () { return bytes.toHex(this.#privateKey) }\r
 \r
        get balance () { return this.#balance }\r
        get frontier () { return this.#frontier }\r
@@ -56,7 +55,7 @@ export class Account {
        }\r
        set weight (v) { this.#weight = v ? BigInt(v) : undefined }\r
 \r
-       constructor (address: string, publicKey: string, privateKey: Uint8Array<ArrayBuffer>, index?: number) {\r
+       constructor (address: string, publicKey: Uint8Array<ArrayBuffer>, index?: number) {\r
                if (!Account.#isInternal) {\r
                        throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`)\r
                }\r
@@ -67,9 +66,9 @@ export class Account {
                        .replace(PREFIX, '')\r
                        .replace(PREFIX_LEGACY, '')\r
                this.#index = index\r
-               this.#locked = false\r
-               this.#pub = publicKey\r
-               this.#prv = privateKey\r
+               this.#publicKey = publicKey\r
+               this.#privateKey = new Uint8Array(0)\r
+               bytes.erase(this.#privateKey)\r
        }\r
 \r
        /**\r
@@ -77,10 +76,10 @@ export class Account {
        * allow garbage collection.\r
        */\r
        async destroy (): Promise<void> {\r
-               bytes.erase(this.#prv)\r
+               bytes.erase(this.#privateKey)\r
                await SafeWorker.add({\r
                        method: 'destroy',\r
-                       name: this.#pub\r
+                       name: this.#publicKey\r
                })\r
                this.#index = undefined\r
                this.#frontier = undefined\r
@@ -101,23 +100,23 @@ export class Account {
                this.#isInternal = true\r
                this.validate(address)\r
                const publicKey = this.#addressToKey(address)\r
-               const account = new this(address, publicKey, new Uint8Array(32), index)\r
-               return account\r
+               return new this(address, publicKey, index)\r
        }\r
 \r
        /**\r
-       * Instantiates an Account object from its public key.\r
+       * Instantiates an Account object from its public key. It is unable to sign\r
+       * blocks or messages since it has no private key.\r
        *\r
-       * @param {string} publicKey - Public key of the account\r
+       * @param {(string|Uint8Array)} publicKey - Public key of the account\r
        * @param {number} [index] - Account number used when deriving the key\r
        * @returns {Account} The instantiated Account object\r
        */\r
-       static fromPublicKey (publicKey: string, index?: number): Account {\r
-               this.#isInternal = true\r
+       static fromPublicKey (publicKey: string | Uint8Array<ArrayBuffer>, index?: number): Account {\r
                this.#validateKey(publicKey)\r
+               if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey)\r
                const address = this.#keyToAddress(publicKey)\r
-               const account = new this(address, publicKey, new Uint8Array(32), index)\r
-               return account\r
+               this.#isInternal = true\r
+               return new this(address, publicKey, index)\r
        }\r
 \r
        /**\r
@@ -129,9 +128,8 @@ export class Account {
        * @returns {Account} A new Account object\r
        */\r
        static async fromPrivateKey (privateKey: string | Uint8Array<ArrayBuffer>, index?: number): Promise<Account> {\r
-               if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey)\r
-               this.#isInternal = true\r
                this.#validateKey(privateKey)\r
+               if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey)\r
                let publicKey: string\r
                try {\r
                        const headers = {\r
@@ -144,9 +142,13 @@ export class Account {
                } catch (err) {\r
                        throw new Error(`Failed to derive public key from private key`, { cause: err })\r
                }\r
-               const address = this.#keyToAddress(publicKey)\r
-               const account = new this(address, publicKey, privateKey, index)\r
-               return account\r
+               try {\r
+                       const self = await this.fromPublicKey(publicKey, index)\r
+                       self.#privateKey = privateKey\r
+                       return self\r
+               } catch (err) {\r
+                       throw new Error(`Failed to lock new Account`, { cause: err })\r
+               }\r
        }\r
 \r
        /**\r
@@ -156,33 +158,29 @@ export class Account {
        * @returns True if successfully locked\r
        */\r
        async lock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
+               if (this.isLocked) {\r
+                       throw new Error(`Account ${this.address} is already locked`)\r
+               }\r
                if (typeof password === 'string') password = utf8.toBytes(password)\r
                if (password == null || !(password instanceof Uint8Array)) {\r
-                       throw new Error('Failed to lock account')\r
+                       throw new Error(`Failed to lock Account ${this.address}`)\r
                }\r
                try {\r
                        const headers = {\r
                                method: 'set',\r
-                               name: this.#pub\r
+                               name: this.publicKey\r
                        }\r
                        const data = {\r
                                password: password.buffer,\r
-                               id: hex.toBytes(this.#pub).buffer,\r
-                               privateKey: this.#prv.buffer\r
-                       }\r
-                       const success = await SafeWorker.add(headers, data)\r
-                       if (!success) {\r
-                               throw null\r
+                               id: new Uint8Array(this.#publicKey).buffer,\r
+                               privateKey: this.#privateKey.buffer\r
                        }\r
+                       return await SafeWorker.add(headers, data)\r
                } catch (err) {\r
-                       console.error(`Failed to lock account ${this.address}`, err)\r
-                       return false\r
+                       throw new Error(`Failed to lock Account ${this.address}`, { cause: err })\r
                } finally {\r
                        bytes.erase(password)\r
                }\r
-               bytes.erase(this.#prv)\r
-               this.#locked = true\r
-               return true\r
        }\r
 \r
        /**\r
@@ -218,23 +216,28 @@ export class Account {
        }\r
 \r
        /**\r
-       * Signs a block using the private key of the account.\r
+       * Signs a block using the private key of the account. The signature is\r
+       * appended to the signature field of the block before being returned.\r
        *\r
-       * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block -\r
+       * @param {(string|Uint8Array)} password - Required to decrypt the private key for signing\r
+       * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - The block data to be hashed and signed\r
+       * @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
        */\r
        async sign (block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {\r
-               if (this.isLocked || this.#prv.buffer.detached) {\r
-                       throw new Error('Failed to sign block with locked Account')\r
+               if (this.isLocked) {\r
+                       throw new Error(`Account ${this.address} must be unlocked prior to signing`)\r
                }\r
                try {\r
                        const headers = {\r
                                method: 'detached'\r
                        }\r
                        const data = {\r
-                               privateKey: new Uint8Array(this.#prv).buffer,\r
+                               privateKey: new Uint8Array(this.#privateKey).buffer,\r
                                msg: hex.toBytes(block.hash).buffer\r
                        }\r
-                       return await NanoNaClWorker.add(headers, data)\r
+                       const result = await NanoNaClWorker.add(headers, data)\r
+                       block.signature = result\r
+                       return result\r
                } catch (err) {\r
                        throw new Error(`Failed to sign block`, { cause: err })\r
                }\r
@@ -249,12 +252,12 @@ export class Account {
        async unlock (password: string | Uint8Array<ArrayBuffer>): Promise<boolean> {\r
                if (typeof password === 'string') password = utf8.toBytes(password)\r
                if (password == null || !(password instanceof Uint8Array)) {\r
-                       throw new Error('Failed to unlock account')\r
+                       throw new Error('Password must be string or bytes')\r
                }\r
                try {\r
                        const headers = {\r
                                method: 'get',\r
-                               name: this.#pub\r
+                               name: this.publicKey\r
                        }\r
                        const data = {\r
                                password: password.buffer\r
@@ -268,15 +271,13 @@ export class Account {
                        if (id !== this.publicKey) {\r
                                throw null\r
                        }\r
-                       this.#prv = new Uint8Array(privateKey as ArrayBuffer)\r
+                       this.#privateKey = new Uint8Array(privateKey as ArrayBuffer)\r
                } catch (err) {\r
-                       console.error(`Failed to unlock account ${this.address}`, err)\r
-                       return false\r
+                       throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err })\r
                } finally {\r
                        bytes.erase(password)\r
+                       return this.isUnlocked\r
                }\r
-               this.#locked = false\r
-               return true\r
        }\r
 \r
        /**\r
@@ -310,21 +311,20 @@ export class Account {
                }\r
        }\r
 \r
-       static #addressToKey (v: string): string {\r
-               const publicKeyBytes = base32.toBytes(v.slice(-60, -8))\r
-               const checksumBytes = base32.toBytes(v.slice(-8))\r
-               const rechecksumBytes = new Blake2b(5).update(publicKeyBytes).digest().reverse()\r
-               if (bytes.toHex(checksumBytes) !== bytes.toHex(rechecksumBytes)) {\r
+       static #addressToKey (address: string): Uint8Array<ArrayBuffer> {\r
+               const publicKey = base32.toBytes(address.slice(-60, -8))\r
+               const checksum = base32.toBytes(address.slice(-8))\r
+               const rechecksum = new Blake2b(5).update(publicKey).digest().reverse()\r
+               if (bytes.toHex(checksum) !== bytes.toHex(rechecksum)) {\r
                        throw new Error('Checksum mismatch in address')\r
                }\r
-               return bytes.toHex(publicKeyBytes)\r
+               return publicKey\r
        }\r
 \r
-       static #keyToAddress (publicKey: string): string {\r
-               const publicKeyBytes = hex.toBytes(publicKey)\r
-               const checksumBytes = new Blake2b(5).update(publicKeyBytes).digest().reverse()\r
-               const encodedPublicKey = bytes.toBase32(publicKeyBytes)\r
-               const encodedChecksum = bytes.toBase32(checksumBytes)\r
+       static #keyToAddress (publicKey: Uint8Array<ArrayBuffer>): string {\r
+               const checksum = new Blake2b(5).update(publicKey).digest().reverse()\r
+               const encodedPublicKey = bytes.toBase32(publicKey)\r
+               const encodedChecksum = bytes.toBase32(checksum)\r
                return `${PREFIX}${encodedPublicKey}${encodedChecksum}`\r
        }\r
 \r