From b54833a9519cf669407958d9bf56e7bd5b867e76 Mon Sep 17 00:00:00 2001
From: mtyszczak <mateusz.tyszczak@gmail.com>
Date: Mon, 24 Mar 2025 15:58:29 +0100
Subject: [PATCH] Implement hb-auth signer extension

---
 .../__tests__/assets/fixture.ts               |  2 +-
 .../__tests__/index.spec.ts                   |  4 ++
 examples/ts/signature-extension/package.json  |  2 +
 .../test/hb-auth-worker.js                    |  1 +
 .../ts/signature-extension/test/index.html    | 56 ++++++++++-----
 ts/packages/signers-hb-auth/README.md         | 37 ++++++++++
 ts/packages/signers-hb-auth/package.json      | 40 +++++++++++
 ts/packages/signers-hb-auth/src/index.ts      | 68 +++++++++++++++++++
 ts/packages/signers-hb-auth/tsconfig.json     | 14 ++++
 ts/packages/signers-keychain/src/index.ts     |  3 +
 ts/pnpm-lock.yaml                             | 42 ++++++++++++
 11 files changed, 252 insertions(+), 17 deletions(-)
 create mode 120000 examples/ts/signature-extension/test/hb-auth-worker.js
 create mode 100644 ts/packages/signers-hb-auth/README.md
 create mode 100644 ts/packages/signers-hb-auth/package.json
 create mode 100644 ts/packages/signers-hb-auth/src/index.ts
 create mode 100644 ts/packages/signers-hb-auth/tsconfig.json

diff --git a/examples/ts/signature-extension/__tests__/assets/fixture.ts b/examples/ts/signature-extension/__tests__/assets/fixture.ts
index 30e4aa430..6e09a2dd3 100644
--- a/examples/ts/signature-extension/__tests__/assets/fixture.ts
+++ b/examples/ts/signature-extension/__tests__/assets/fixture.ts
@@ -12,7 +12,7 @@ export const test = base.extend<{
 }>({
   context: async ({}, use) => {
     console.log('Launched browser');
-    const browserContext = await chromium.launchPersistentContext('');
+    const browserContext = await chromium.launchPersistentContext('', { headless: typeof process.env.PLAYWRIGHT_HEADLESS === "undefined" });
 
     console.log('Before use browserContext');
     await use(browserContext);
diff --git a/examples/ts/signature-extension/__tests__/index.spec.ts b/examples/ts/signature-extension/__tests__/index.spec.ts
index 24c41a95d..273f89417 100644
--- a/examples/ts/signature-extension/__tests__/index.spec.ts
+++ b/examples/ts/signature-extension/__tests__/index.spec.ts
@@ -3,6 +3,10 @@ import { expect, Page } from "@playwright/test";
 
 test.describe('Signature extension tests', () => {
   test('Should be able to sign transction using key chain extension.', async ({ page, extensionId, context, baseDirectoryPath, testedAccountAuthorityData}) => {
+    page.on("console", (msg) => {
+      console.log(`[${msg.type()}]>> Page console: ${msg.text()}`);
+    });
+
     page.setViewportSize({ width: 500, height: 700 });
 
     //////////////// Import settings begin (containing mirrornet endpoint configuration)
diff --git a/examples/ts/signature-extension/package.json b/examples/ts/signature-extension/package.json
index 75df6e5cd..536f45f47 100644
--- a/examples/ts/signature-extension/package.json
+++ b/examples/ts/signature-extension/package.json
@@ -9,8 +9,10 @@
   },
   "dependencies": {
     "@hiveio/beekeeper": "1.27.10-stable.250305202831",
+    "@hiveio/hb-auth": "0.0.1-stable.250131124251",
     "@hiveio/wax": "file:../../../ts",
     "@hiveio/wax-signers-beekeeper": "file:../../../ts/packages/signers-beekeeper",
+    "@hiveio/wax-signers-hb-auth": "file:../../../ts/packages/signers-hb-auth",
     "@hiveio/wax-signers-keychain": "file:../../../ts/packages/signers-keychain",
     "@hiveio/wax-signers-metamask": "file:../../../ts/packages/signers-metamask",
     "@hiveio/wax-signers-peakvault": "file:../../../ts/packages/signers-peakvault"
diff --git a/examples/ts/signature-extension/test/hb-auth-worker.js b/examples/ts/signature-extension/test/hb-auth-worker.js
new file mode 120000
index 000000000..2f06d0b6b
--- /dev/null
+++ b/examples/ts/signature-extension/test/hb-auth-worker.js
@@ -0,0 +1 @@
+../node_modules/@hiveio/hb-auth/dist/worker.js
\ No newline at end of file
diff --git a/examples/ts/signature-extension/test/index.html b/examples/ts/signature-extension/test/index.html
index fd4fef120..4d46dd3cf 100644
--- a/examples/ts/signature-extension/test/index.html
+++ b/examples/ts/signature-extension/test/index.html
@@ -14,6 +14,7 @@
   <button onclick="usePeakVault()">Use Peak Vault</button>
   <button onclick="useMetaMask()">Use MetaMask</button>
   <button onclick="useBeekeeper()">Use Beekeeper</button>
+  <button onclick="useHbAuth()">Use HBAuth</button>
 
   <pre><code id="operating-private-key"></code></pre>
   <pre><code id="tx-result"></code></pre>
@@ -26,6 +27,8 @@
     import PeakVaultProvider from "@hiveio/wax-signers-peakvault";
     import MetaMaskProvider from "@hiveio/wax-signers-metamask";
     import BeekeeperProvider from "@hiveio/wax-signers-beekeeper";
+    import HBAuthProvider from "@hiveio/wax-signers-hb-auth";
+    import { OfflineClient } from "@hiveio/hb-auth";
 
     const txResult = document.getElementById('tx-result');
     const operatingPrivateKeyPlaceholder = document.getElementById('operating-private-key');
@@ -34,21 +37,6 @@
     (async()=> {
       const testEnv = await prepareTestingEnvironemnt();
       const accountName = testEnv.accountName;
-      const keychainProvider = KeychainProvider.for(accountName, testEnv.role);
-      let beekeeperProvider;
-      try {
-        beekeeperProvider = await BeekeeperProvider.for(testEnv.preparedBeekeeperWallet, testEnv.publicKey);
-      } catch (error) {
-        console.error('Beekeeper provider not available', error);
-      }
-      const peakVaultProvider = PeakVaultProvider.for(accountName, testEnv.role);
-      let metaMaskProvider;
-      try {
-        metaMaskProvider = await MetaMaskProvider.for(0);
-        await metaMaskProvider.installSnap();
-      } catch (error) {
-        console.error('Metamask provider not available', error);
-      }
 
       operatingPrivateKeyPlaceholder.textContent = `${accountName}@${testEnv.role} private key to be imported to wallets: ${testEnv.privateKey}`;
 
@@ -97,15 +85,51 @@
           await tx.sign(provider);
           await verifySignature(tx);
         } catch (error) {
-          console.error(error);
+          console.error('Error signing transaction:', error);
           txResult.textContent = `Error: ${error.message}`;
         }
       };
 
+      const keychainProvider = KeychainProvider.for(accountName, testEnv.role);
+
       window.useKeychain = () => void sign(keychainProvider);
+
+      let beekeeperProvider;
+      try {
+        beekeeperProvider = await BeekeeperProvider.for(testEnv.preparedBeekeeperWallet, testEnv.publicKey);
+      } catch (error) {
+        console.error('Beekeeper provider not available', error);
+      }
+      const hbAuthClient = new OfflineClient({
+        chainId: testEnv.configuredChain.chainId,
+        node: testEnv.configuredChain.endpointUrl,
+        workerUrl: new URL('./hb-auth-worker.js', import.meta.url),
+        sessionTimeout: 900
+      });
+      const hbAuthProvider = await HBAuthProvider.for(hbAuthClient);
+      try {
+        await hbAuthClient.initialize();
+        const registeredUser = await hbAuthClient.getRegisteredUserByUsername(accountName);
+        if (registeredUser)
+          await hbAuthClient.authenticate(accountName, "password", testEnv.role);
+        else
+          await hbAuthClient.register(accountName, "password", testEnv.privateKey, testEnv.role);
+      } catch (error) {
+        console.error('Could not initialize hb-auth', error);
+      }
+      const peakVaultProvider = PeakVaultProvider.for(accountName, testEnv.role);
+      let metaMaskProvider;
+      try {
+        metaMaskProvider = await MetaMaskProvider.for(0);
+        await metaMaskProvider.installSnap();
+      } catch (error) {
+        console.error('Metamask provider not available', error);
+      }
+
       window.usePeakVault = () => void sign(peakVaultProvider);
       window.useMetaMask = () => void sign(metaMaskProvider);
       window.useBeekeeper = () => void sign(beekeeperProvider);
+      window.useHbAuth = () => void sign(hbAuthProvider);
     })();
   </script>
 </body>
diff --git a/ts/packages/signers-hb-auth/README.md b/ts/packages/signers-hb-auth/README.md
new file mode 100644
index 000000000..6abd32c5e
--- /dev/null
+++ b/ts/packages/signers-hb-auth/README.md
@@ -0,0 +1,37 @@
+# @hiveio/wax-signers-hbauth
+
+Wax signer library extending transaction signing possibilities by a 3rd party Web-only extension - hb-auth
+
+## Example usage
+
+```ts
+import { createHiveChain } from "@hiveio/wax";
+import HBAuthProvider from "@hiveio/wax-signers-hb-auth";
+
+const chain = await createHiveChain();
+
+const provider = HBAuthProvider.for(hbAuthClient);
+
+// Create a transaction using the Wax Hive chain instance
+const tx = await chain.createTransaction();
+
+// Perform some operations, e.g. push the vote operation:
+tx.pushOperation({
+  vote: {
+    voter: "alice",
+    author: "bob",
+    permlink: "example-post",
+    weight: 10000
+  }
+});
+
+// Wait for the keychain to sign the transaction
+await tx.sign(provider);
+
+// broadcast the transaction
+await chain.broadcast(tx);
+```
+
+## License
+
+See license in [LICENSE.md](LICENSE.md) file
diff --git a/ts/packages/signers-hb-auth/package.json b/ts/packages/signers-hb-auth/package.json
new file mode 100644
index 000000000..c990fda27
--- /dev/null
+++ b/ts/packages/signers-hb-auth/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "@hiveio/wax-signers-hb-auth",
+  "version": "0.0.0-Run-Prepack",
+  "description": "Wax signer library extending transaction signing possibilities by a 3rd party Web-only extension - hb-auth",
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
+  "type": "module",
+  "private": false,
+  "scripts": {
+    "build": "tsc",
+    "prepack": "jq --argfile source ../../package.json '.version = $source.version | .publishConfig.registry = $source.publishConfig.registry | .publishConfig.tag = $source.publishConfig.tag' package.json > package.json.tmp && mv package.json.tmp package.json"
+  },
+  "devDependencies": {
+    "typescript": "catalog:typescript-toolset"
+  },
+  "dependencies": {
+    "@hiveio/wax": "workspace:../..",
+    "@hiveio/hb-auth": "0.0.1-stable.250131124251"
+  },
+  "files": [
+    "dist/index.d.ts",
+    "dist/index.js",
+    "README.md",
+    "LICENSE.md"
+  ],
+  "license": "SEE LICENSE IN LICENSE.md",
+  "keywords": [
+    "wax",
+    "blockchain",
+    "hive"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://gitlab.syncad.com/hive/wax.git"
+  },
+  "publishConfig": {
+    "registry": "https://RegistryPlaceholder",
+    "tag": "DistTagPlaceholder"
+  }
+}
\ No newline at end of file
diff --git a/ts/packages/signers-hb-auth/src/index.ts b/ts/packages/signers-hb-auth/src/index.ts
new file mode 100644
index 000000000..c49ca0290
--- /dev/null
+++ b/ts/packages/signers-hb-auth/src/index.ts
@@ -0,0 +1,68 @@
+import type { IOnlineSignatureProvider, ITransaction } from "@hiveio/wax";
+
+import { type OfflineClient, type OnlineClient } from "@hiveio/hb-auth";
+
+// We do not extend from WaxError to avoid runtime dependencies, such as: /vite or /web - without it we can import only types
+export class WaxHBAuthProviderError extends Error {}
+
+/**
+ * Wax transaction signature provider using the hb-auth.
+ *
+ * @example
+ * ```
+ * const provider = HBAuthProvider.for(hbAuthClient);
+ *
+ * // Create a transaction using the Wax Hive chain instance
+ * const tx = await chain.createTransaction();
+ *
+ * // Perform some operations, e.g. pushing operations...
+ *
+ * // Sign the transaction
+ * await tx.sign(provider);
+ *
+ * // broadcast
+ * await chain.broadcast(tx);
+ * ```
+ */
+class HBAuthProvider implements IOnlineSignatureProvider {
+  private constructor(
+    public readonly client: OnlineClient | OfflineClient,
+    public readonly username?: string
+  ) {}
+
+  public static for(client: OnlineClient | OfflineClient, username?: string): HBAuthProvider {
+    return new HBAuthProvider(client, username);
+  }
+
+  public async signTransaction(transaction: ITransaction): Promise<void> {
+    const requiredAuthorities = transaction.requiredAuthorities;
+
+    const signatures: string[] = [];
+
+    const digest = transaction.sigDigest;
+
+    for(const auth in requiredAuthorities)
+      if (auth !== "other")
+        for(const actor of requiredAuthorities[auth])
+          if (this.username === undefined || actor === this.username)
+            signatures.push(await this.client.sign(actor, digest, auth as 'posting' | 'active' | 'owner'));
+
+    if (signatures.length === 0)
+      throw new WaxHBAuthProviderError(`Failed to sign the transaction`);
+
+    for(const signature of signatures)
+      transaction.sign(signature);
+  }
+}
+
+export interface WaxHBAuthProviderCreator {
+  /**
+   * We assume you already called #initialize() on the client and client has imported the keys.
+   *
+   * @param client - The hb-auth client instance.
+   * @param username - The username to sign the transaction with. If not provided - every user imported to the hb-auth will be allowed to sign the transaction.
+   */
+  for(client: OnlineClient | OfflineClient, username?: string): HBAuthProvider;
+}
+
+export default HBAuthProvider as WaxHBAuthProviderCreator;
diff --git a/ts/packages/signers-hb-auth/tsconfig.json b/ts/packages/signers-hb-auth/tsconfig.json
new file mode 100644
index 000000000..25ef87095
--- /dev/null
+++ b/ts/packages/signers-hb-auth/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "extends": "../../npm-common-config/ts-common/tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "baseUrl": ".",
+    "outDir": "./dist",
+    "incremental": true,
+    "tsBuildInfoFile": "./dist/.tsbuildinfo",
+    "skipLibCheck": true
+  },
+  "include": [
+    "./src"
+  ]
+}
\ No newline at end of file
diff --git a/ts/packages/signers-keychain/src/index.ts b/ts/packages/signers-keychain/src/index.ts
index b997ac21b..1337a7f12 100644
--- a/ts/packages/signers-keychain/src/index.ts
+++ b/ts/packages/signers-keychain/src/index.ts
@@ -54,6 +54,9 @@ class KeychainProvider implements IOnlineSignatureProvider {
   }
 
   public async signTransaction(transaction: ITransaction): Promise<void> {
+    if (!(await KeychainProvider.keychain.isKeychainInstalled()))
+      throw new WaxKeychainProviderError(`Keychain is not installed`);
+
     const data = await KeychainProvider.keychain.signTx({
       method: this.role,
       username: this.accountName,
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index 8eae27819..15f09f82c 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -201,6 +201,19 @@ importers:
         specifier: catalog:typescript-toolset
         version: 5.7.3
 
+  packages/signers-hb-auth:
+    dependencies:
+      '@hiveio/hb-auth':
+        specifier: 0.0.1-stable.250131124251
+        version: 0.0.1-stable.250131124251
+      '@hiveio/wax':
+        specifier: workspace:../..
+        version: link:../..
+    devDependencies:
+      typescript:
+        specifier: catalog:typescript-toolset
+        version: 5.7.3
+
   packages/signers-keychain:
     dependencies:
       '@hiveio/wax':
@@ -428,9 +441,20 @@ packages:
     resolution: {integrity: sha1-mPx0QrDh3NSPSOYGyR1SUyi3lyA=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.27.10-stable.250305202831.tgz}
     engines: {node: ^20.11 || >= 21.2}
 
+  '@hiveio/beekeeper@1.27.8-stable.250131103618':
+    resolution: {integrity: sha1-0IYuFBbxIIKfRTrI6ZZc7Jf+qLA=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.27.8-stable.250131103618.tgz}
+    engines: {node: ^20.11 || >= 21.2}
+
   '@hiveio/dhive@1.3.2':
     resolution: {integrity: sha512-kJjp3TbpIlODxjJX4BWwvOf+cMxT8CFH/mNQ40RRjR2LP0a4baSWae1G+U/q/NtgjsIQz6Ja40tvnw6KF12I+g==, tarball: https://registry.npmjs.org/@hiveio/dhive/-/dhive-1.3.2.tgz}
 
+  '@hiveio/hb-auth@0.0.1-stable.250131124251':
+    resolution: {integrity: sha1-HX9GbxRjpo+ezDR5SAvuEIVr6II=, tarball: https://gitlab.syncad.com/api/v4/projects/429/packages/npm/@hiveio/hb-auth/-/@hiveio/hb-auth-0.0.1-stable.250131124251.tgz}
+
+  '@hiveio/wax@1.27.6-rc7-stable.250131113706':
+    resolution: {integrity: sha1-Xi2VV8y9T6MdBHOBSzxZNpmGnF8=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.27.6-rc7-stable.250131113706.tgz}
+    engines: {node: ^20.11 || >= 21.2}
+
   '@isaacs/cliui@8.0.2':
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
@@ -978,6 +1002,9 @@ packages:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
 
+  comlink@4.4.2:
+    resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
+
   commander@2.20.3:
     resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
 
@@ -2589,6 +2616,8 @@ snapshots:
 
   '@hiveio/beekeeper@1.27.10-stable.250305202831': {}
 
+  '@hiveio/beekeeper@1.27.8-stable.250131103618': {}
+
   '@hiveio/dhive@1.3.2':
     dependencies:
       '@ecency/bytebuffer': 6.0.0
@@ -2607,6 +2636,17 @@ snapshots:
     transitivePeerDependencies:
       - encoding
 
+  '@hiveio/hb-auth@0.0.1-stable.250131124251':
+    dependencies:
+      '@hiveio/wax': 1.27.6-rc7-stable.250131113706
+      comlink: 4.4.2
+
+  '@hiveio/wax@1.27.6-rc7-stable.250131113706':
+    dependencies:
+      '@hiveio/beekeeper': 1.27.8-stable.250131103618
+      events: 3.3.0
+      long: 5.2.3
+
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
@@ -3189,6 +3229,8 @@ snapshots:
     dependencies:
       delayed-stream: 1.0.0
 
+  comlink@4.4.2: {}
+
   commander@2.20.3: {}
 
   commondir@1.0.1: {}
-- 
GitLab