]> git.codecow.com Git - libnemo.git/commitdiff
Use bytes to store account private key instead of string. Rename private class proper...
authorChris Duncan <chris@zoso.dev>
Sun, 6 Jul 2025 07:04:22 +0000 (00:04 -0700)
committerChris Duncan <chris@zoso.dev>
Sun, 6 Jul 2025 07:04:22 +0000 (00:04 -0700)
src/lib/account.ts

index 2e6432e893dcb57cf8449537dba18abc6bfb9f84..a8ce5f52dd2514b55361bc1f49e44baaf4b55fc4 100644 (file)
@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: GPL-3.0-or-later\r
 \r
 import { Blake2b } from './blake2b'\r
-import { ACCOUNT_KEY_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
+import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
 import { base32, bytes, hex, utf8 } from './convert'\r
 import { Pool } from './pool'\r
 import { Rpc } from './rpc'\r
@@ -17,53 +17,60 @@ import { NanoNaCl, SafeWorker } from '#workers'
 export class Account {\r
        static #isInternal: boolean = false\r
        static #poolSafe: Pool\r
-       #a: string\r
+\r
+       #address: string\r
+       #locked: boolean\r
        #pub: string\r
-       #prv: string | null\r
-       #i?: number\r
-       #f?: string\r
-       #b?: bigint\r
-       #r?: bigint\r
-       #rep?: Account\r
-       #w?: bigint\r
+       #prv: Uint8Array<ArrayBuffer>\r
+\r
+       #balance?: bigint\r
+       #frontier?: string\r
+       #index?: number\r
+       #receivable?: bigint\r
+       #representative?: Account\r
+       #weight?: bigint\r
 \r
-       get address () { return `${PREFIX}${this.#a}` }\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 this.#prv }\r
-       get index () { return this.#i }\r
-       get frontier () { return this.#f }\r
-       get balance () { return this.#b }\r
-       get receivable () { return this.#r }\r
-       get representative () { return this.#rep }\r
-       get weight () { return this.#w }\r
+       get privateKey () { return bytes.toHex(this.#prv) }\r
 \r
-       set frontier (v) { this.#f = v }\r
-       set balance (v) { this.#b = v ? BigInt(v) : undefined }\r
-       set receivable (v) { this.#r = v ? BigInt(v) : undefined }\r
+       get balance () { return this.#balance }\r
+       get frontier () { return this.#frontier }\r
+       get index () { return this.#index }\r
+       get receivable () { return this.#receivable }\r
+       get representative () { return this.#representative }\r
+       get weight () { return this.#weight }\r
+\r
+       set balance (v) { this.#balance = v ? BigInt(v) : undefined }\r
+       set frontier (v) { this.#frontier = v }\r
+       set receivable (v) { this.#receivable = v ? BigInt(v) : undefined }\r
        set representative (v) {\r
                if (v?.constructor === Account) {\r
-                       this.#rep = v\r
+                       this.#representative = v\r
                } else if (typeof v === 'string') {\r
-                       this.#rep = Account.fromAddress(v)\r
+                       this.#representative = Account.fromAddress(v)\r
                } else {\r
                        throw new TypeError(`Invalid argument for account representative: ${v}`)\r
                }\r
        }\r
-       set weight (v) { this.#w = v ? BigInt(v) : undefined }\r
+       set weight (v) { this.#weight = v ? BigInt(v) : undefined }\r
 \r
-       constructor (address: string, publicKey: string, privateKey?: string, index?: number) {\r
+       constructor (address: string, publicKey: string, privateKey: Uint8Array<ArrayBuffer>, index?: number) {\r
                if (!Account.#isInternal) {\r
                        throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`)\r
                }\r
                if (index !== undefined && typeof index !== 'number') {\r
                        throw new TypeError(`Invalid index ${index} when creating Account ${address}`)\r
                }\r
-               this.#a = address\r
+               this.#address = address\r
                        .replace(PREFIX, '')\r
                        .replace(PREFIX_LEGACY, '')\r
+               this.#index = index\r
+               this.#locked = false\r
                this.#pub = publicKey\r
-               this.#prv = privateKey ?? null\r
-               this.#i = index\r
+               this.#prv = privateKey\r
                Account.#poolSafe ??= new Pool(SafeWorker)\r
                Account.#isInternal = false\r
        }\r
@@ -73,17 +80,17 @@ export class Account {
        * allow garbage collection.\r
        */\r
        async destroy (): Promise<void> {\r
+               this.#prv.fill(0)\r
                await Account.#poolSafe.assign({\r
                        method: 'destroy',\r
                        name: this.#pub\r
                })\r
-               this.#prv = null\r
-               this.#i = undefined\r
-               this.#f = undefined\r
-               this.#b = undefined\r
-               this.#r = undefined\r
-               this.#rep = undefined\r
-               this.#w = undefined\r
+               this.#index = undefined\r
+               this.#frontier = undefined\r
+               this.#balance = undefined\r
+               this.#receivable = undefined\r
+               this.#representative = undefined\r
+               this.#weight = undefined\r
        }\r
 \r
        /**\r
@@ -97,7 +104,7 @@ export class Account {
                Account.#isInternal = true\r
                Account.validate(address)\r
                const publicKey = Account.#addressToKey(address)\r
-               const account = new this(address, publicKey, undefined, index)\r
+               const account = new this(address, publicKey, new Uint8Array(32), index)\r
                return account\r
        }\r
 \r
@@ -112,27 +119,34 @@ export class Account {
                Account.#isInternal = true\r
                Account.#validateKey(publicKey)\r
                const address = Account.#keyToAddress(publicKey)\r
-               const account = new this(address, publicKey, undefined, index)\r
+               const account = new this(address, publicKey, new Uint8Array(32), index)\r
                return account\r
        }\r
 \r
        /**\r
-       * Instantiates an Account object from its private key. The\r
-       * corresponding public key will automatically be derived and saved.\r
+       * Instantiates an Account object from its private key. The corresponding\r
+       * public key will automatically be derived and saved.\r
        *\r
        * @param {string} privateKey - Private key of the account\r
        * @param {number} [index] - Account number used when deriving the key\r
        * @returns {Account} A new Account object\r
        */\r
-       static fromPrivateKey (privateKey: string, index?: number): Account {\r
+       static fromPrivateKey (privateKey: string | Uint8Array<ArrayBuffer>, index?: number): Account {\r
+               if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey)\r
                Account.#isInternal = true\r
                Account.#validateKey(privateKey)\r
                const publicKey = NanoNaCl.convert(privateKey)\r
-               const account = Account.fromPublicKey(publicKey, index)\r
-               account.#prv = privateKey.toUpperCase()\r
+               const address = Account.#keyToAddress(publicKey)\r
+               const account = new this(address, publicKey, privateKey, index)\r
                return account\r
        }\r
 \r
+       /**\r
+       * Locks the account with a password that will be needed to unlock it later.\r
+       *\r
+       * @param {(string|Uint8Array)} password Used to lock the account\r
+       * @returns True if successfully locked\r
+       */\r
        async lock (password: string | Uint8Array): Promise<boolean> {\r
                if (typeof password === 'string') {\r
                        password = utf8.toBytes(password)\r
@@ -141,20 +155,17 @@ export class Account {
                        throw new Error('Failed to unlock wallet')\r
                }\r
                try {\r
-                       const data: { id: string, privateKey: string | null } = {\r
+                       const data: { id: string, privateKey: Uint8Array } = {\r
                                id: this.#pub,\r
-                               privateKey: null\r
-                       }\r
-                       if (this.#prv != null) {\r
-                               data.privateKey = this.#prv\r
+                               privateKey: this.#prv\r
                        }\r
                        const response = (await Account.#poolSafe.assign({\r
-                               method: 'put',\r
+                               method: 'set',\r
                                name: this.#pub,\r
                                password,\r
                                data\r
                        }))[0]\r
-                       const success = response.result\r
+                       const success = response?.result\r
                        if (!success) {\r
                                throw null\r
                        }\r
@@ -164,10 +175,17 @@ export class Account {
                } finally {\r
                        password.fill(0)\r
                }\r
-               this.#prv = null\r
+               this.#prv.fill(0)\r
+               this.#locked = true\r
                return true\r
        }\r
 \r
+       /**\r
+       * Unlocks the account using the same password as used prior to lock it.\r
+       *\r
+       * @param {(string|Uint8Array)} password Used previously to lock the account\r
+       * @returns True if successfully unlocked\r
+       */\r
        async unlock (password: string | Uint8Array): Promise<boolean> {\r
                if (typeof password === 'string') {\r
                        password = utf8.toBytes(password)\r
@@ -181,19 +199,18 @@ export class Account {
                                name: this.#pub,\r
                                password\r
                        }))[0]\r
-                       const { id, privateKey } = response.result\r
-                       if (id !== this.#pub) {\r
+                       const { id, privateKey } = response?.result\r
+                       if (id == null || id !== this.#pub) {\r
                                throw null\r
                        }\r
-                       if (privateKey != null) {\r
-                               this.#prv = privateKey\r
-                       }\r
+                       this.#prv.set(privateKey)\r
                } catch (err) {\r
                        console.error(`Failed to unlock account ${this.address}`, err)\r
                        return false\r
                } finally {\r
                        password.fill(0)\r
                }\r
+               this.#locked = false\r
                return true\r
        }\r
 \r
@@ -254,11 +271,11 @@ export class Account {
                if (frontier == null) {\r
                        throw new Error('Account not found')\r
                }\r
-               this.#b = BigInt(balance)\r
-               this.#f = frontier\r
-               this.#r = BigInt(receivable)\r
-               this.#rep = Account.fromAddress(representative)\r
-               this.#w = BigInt(weight)\r
+               this.#balance = BigInt(balance)\r
+               this.#frontier = frontier\r
+               this.#receivable = BigInt(receivable)\r
+               this.#representative = Account.fromAddress(representative)\r
+               this.#weight = BigInt(weight)\r
        }\r
 \r
        static #addressToKey (v: string): string {\r
@@ -279,18 +296,28 @@ export class Account {
                return `${PREFIX}${encodedPublicKey}${encodedChecksum}`\r
        }\r
 \r
-       static #validateKey (key: string): void {\r
+       static #validateKey (key: unknown): asserts key is (string | Uint8Array) {\r
                if (key === undefined) {\r
                        throw new TypeError(`Key is undefined`)\r
                }\r
-               if (typeof key !== 'string') {\r
-                       throw new TypeError(`Key must be a string`)\r
+               if (typeof key !== 'string' && !(key instanceof Uint8Array)) {\r
+                       throw new TypeError(`Key must be a string or Uint8Array`)\r
                }\r
-               if (key.length !== ACCOUNT_KEY_LENGTH) {\r
-                       throw new TypeError(`Key must be ${ACCOUNT_KEY_LENGTH} characters`)\r
+               if (typeof key === 'string') {\r
+                       if (key.length !== ACCOUNT_KEY_HEX_LENGTH) {\r
+                               throw new TypeError(`Key must be ${ACCOUNT_KEY_HEX_LENGTH} characters`)\r
+                       }\r
+                       if (!/^[A-Fa-f0-9]{64}$/i.test(key)) {\r
+                               throw new RangeError(`Key is not a valid hexadecimal value`)\r
+                       }\r
                }\r
-               if (!/^[0-9a-fA-F]+$/i.test(key)) {\r
-                       throw new RangeError('Key is not a valid hexadecimal value')\r
+               if (key instanceof Uint8Array) {\r
+                       if (key.byteLength !== ACCOUNT_KEY_BYTE_LENGTH) {\r
+                               throw new TypeError(`Key must be ${ACCOUNT_KEY_BYTE_LENGTH} BYTES`)\r
+                       }\r
+                       if (key.every(v => v === 0)) {\r
+                               throw new TypeError(`Key is not a valid byte array`)\r
+                       }\r
                }\r
        }\r
 }\r