diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b30b38af..af737eff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,9 @@ name: Build on: push: branches: - - '**' + - develop + - main + - release/** pull_request: jobs: diff --git a/.github/workflows/connect-button-ci.yml b/.github/workflows/connect-button-ci.yml index 733351fb..88785899 100644 --- a/.github/workflows/connect-button-ci.yml +++ b/.github/workflows/connect-button-ci.yml @@ -1,4 +1,4 @@ -name: 'Connect button CI/CD' +name: 'Connect Button Storybook' on: pull_request: @@ -19,6 +19,7 @@ env: jobs: build_push_container: + if: ${{ (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy-pr')) || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')) }} permissions: packages: write id-token: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f343ea04..59008b38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - next - develop - release/** workflow_dispatch: diff --git a/.gitignore b/.gitignore index 6ad61a66..555c82cd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ yarn-error.log* .envrc -examples/cdn/radix-dapp-toolkit.bundle.umd.cjs +examples/cdn/radix-dapp-toolkit.bundle.umd.js # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 53ff625a..a2fa5133 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,18 @@ eula + + coverage +
- RDT on NPM + NPM version Discord conversation - Discord conversation + NPM downloads

@@ -39,7 +42,7 @@ ## How to start? -Install [Node.js] which includes [Node Package Manager][npm]. Use `create-radix-dapp` package - paste following command into your terminal and it will walk you through all required steps! +Install [Node.js] which includes [Node Package Manager][npm]. Use `create-radix-dapp` package - paste following command into your terminal and it will walk you through all required steps. ```bash npx create-radix-dapp@latest diff --git a/examples/cdn/index.html b/examples/cdn/index.html index e671f9a8..2530058d 100644 --- a/examples/cdn/index.html +++ b/examples/cdn/index.html @@ -15,7 +15,7 @@
- + diff --git a/examples/simple-dapp/index.html b/examples/simple-dapp/index.html index faa6ed96..a6c1f65a 100644 --- a/examples/simple-dapp/index.html +++ b/examples/simple-dapp/index.html @@ -3,6 +3,7 @@ + Simple dApp diff --git a/examples/simple-dapp/src/main.ts b/examples/simple-dapp/src/main.ts index 041797da..7f09485a 100644 --- a/examples/simple-dapp/src/main.ts +++ b/examples/simple-dapp/src/main.ts @@ -7,14 +7,20 @@ import { DataRequestBuilder, OneTimeDataRequestBuilder, LocalStorageModule, + EnvironmentModule, generateRolaChallenge, + SubintentRequestBuilder, } from '@radixdlt/radix-dapp-toolkit' const dAppDefinitionAddress = import.meta.env.VITE_DAPP_DEFINITION_ADDRESS -const widgetUrl = import.meta.env.VITE_WIDGET_DAPP_URL const networkId = RadixNetwork.Stokenet const storageModule = LocalStorageModule( `rdt:${dAppDefinitionAddress}:${networkId}`, + { + providers: { + environmentModule: EnvironmentModule(), + }, + }, ) const requestsStore = storageModule.getPartition('requests') const sessionStore = storageModule.getPartition('sessions') @@ -29,7 +35,79 @@ content.innerHTML = ` -
+
+ +
+
+ +
+
+ + +
+ + +
+ + + + +

   

@@ -44,12 +122,36 @@ const sendTxButton = document.getElementById('sendTx')!
 const sessions = document.getElementById('sessions')!
 const removeCb = document.getElementById('removeCb')!
 const addCb = document.getElementById('addCb')!
+const subintentButton = document.getElementById('subintent')!
+const subintentManifest = document.getElementById(
+  'subintentManifest',
+)! as HTMLTextAreaElement
+const subintentExpirationValue = document.getElementById(
+  'subintentExpirationValue',
+)! as HTMLInputElement
 const requests = document.getElementById('requests')!
 const logs = document.getElementById('logs')!
 const state = document.getElementById('state')!
 const gatewayConfig = document.getElementById('gatewayConfig')!
 const gatewayStatus = document.getElementById('gatewayStatus')!
 const oneTimeRequest = document.getElementById('one-time-request')!
+const proofOfOwnershipRequest = document.getElementById(
+  'proof-of-ownership-request',
+)!
+
+let subintentExpiration: 'afterDelay' | 'atTime' = 'afterDelay'
+
+document.querySelectorAll('input[name="option"]').forEach((radio) => {
+  radio.addEventListener('change', () => {
+    const selectedOption = document.querySelector(
+      'input[name="option"]:checked',
+    ) as HTMLInputElement
+    if (selectedOption) {
+      console.log(`Selected value: ${selectedOption.value}`)
+      subintentExpiration = selectedOption.value as 'afterDelay' | 'atTime'
+    }
+  })
+})
 
 const logger = Logger()
 
@@ -68,6 +170,21 @@ removeCb.onclick = () => {
   document.querySelector('radix-connect-button')?.remove()
 }
 
+subintentButton.onclick = async () => {
+  console.log(subintentManifest.value)
+  console.log(subintentExpirationValue.value)
+  const result = await dAppToolkit.walletApi.sendPreAuthorizationRequest(
+    SubintentRequestBuilder()
+      .manifest(subintentManifest.value)
+      .setExpiration(
+        subintentExpiration,
+        parseInt(subintentExpirationValue.value as string),
+      ),
+  )
+
+  console.log('result', result.isOk() && result.value)
+}
+
 addCb.onclick = () => {
   const connectButton = document.createElement('radix-connect-button')
   const header = document.querySelector('header')!
@@ -76,15 +193,16 @@ addCb.onclick = () => {
 const dAppToolkit = RadixDappToolkit({
   dAppDefinitionAddress,
   networkId,
-  featureFlags: ['ExperimentalMobileSupport'],
-  logger,
+  // logger,
 })
 
 const gatewayApi = GatewayApiClient.initialize(
   dAppToolkit.gatewayApi.clientConfig,
 )
 
-dAppToolkit.walletApi.provideChallengeGenerator(async () => generateRolaChallenge())
+dAppToolkit.walletApi.provideChallengeGenerator(async () =>
+  generateRolaChallenge(),
+)
 
 dAppToolkit.walletApi.setRequestData(
   DataRequestBuilder.persona().withProof(),
@@ -105,25 +223,52 @@ resetButton.onclick = () => {
   window.location.replace(window.location.origin)
 }
 
-sendTxButton.onclick = () => {
-  dAppToolkit.walletApi.sendTransaction({
-    transactionManifest: `CALL_METHOD
-    Address("component_tdx_2_1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxyulkzl")
-    "free"
-;
-CALL_METHOD
-    Address("account_tdx_2_12yfw30hdc445j4lnepw7dmrkjcqcswsrxlff5r07mrjq9f8mnnn2r5")
-    "try_deposit_batch_or_abort"
-    Expression("ENTIRE_WORKTOP")
-    Enum<0u8>()
-;`,
+sendTxButton.onclick = async () => {
+  const res = await dAppToolkit.walletApi.sendTransaction({
+    transactionManifest: `
+    CALL_METHOD
+      Address("component_tdx_2_1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxyulkzl")
+      "free"
+    ;
+    
+    CALL_METHOD
+      Address("account_tdx_2_1299trm47s3x648jemhu3lfm4d6gt73289rd9s2hpdjm3tp5pdwq4m5")
+      "try_deposit_batch_or_abort"
+      Expression("ENTIRE_WORKTOP")
+      Enum<0u8>()
+    ;`,
   })
+  console.log('send tx result', res)
+}
+
+oneTimeRequest.onclick = async () => {
+  const res = await dAppToolkit.walletApi.sendOneTimeRequest(
+    OneTimeDataRequestBuilder.accounts().exactly(1),
+  )
+
+  console.log('one time request result', res)
 }
 
-oneTimeRequest.onclick = () => {
-  dAppToolkit.walletApi.sendOneTimeRequest(
+proofOfOwnershipRequest.onclick = async () => {
+  const connectedAccounts =
+    dAppToolkit.walletApi.getWalletData()?.accounts ?? []
+  const connectedPersona = dAppToolkit.walletApi.getWalletData()?.persona
+
+  if (connectedAccounts.length === 0 || !connectedPersona) {
+    alert('No connected account or persona')
+    return
+  }
+
+  const result = await dAppToolkit.walletApi.sendOneTimeRequest(
     OneTimeDataRequestBuilder.accounts().exactly(1),
+    OneTimeDataRequestBuilder.proofOfOwnership()
+      .accounts(connectedAccounts.map((account) => account.address))
+      .identity(connectedPersona.identityAddress),
   )
+
+  console.log(result)
+
+  alert(`Result is ok: ${result.isOk()}`)
 }
 
 setInterval(() => {
diff --git a/package-lock.json b/package-lock.json
index 1e79b74c..ea450e0e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3630,18 +3630,18 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
-      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
+      "version": "7.25.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz",
+      "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.25.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz",
+      "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -3700,10 +3700,13 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
-      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
+      "version": "7.25.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz",
+      "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==",
       "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.25.8"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -5210,19 +5213,25 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
-      "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
+      "version": "7.25.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz",
+      "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-string-parser": "^7.23.4",
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-string-parser": "^7.25.7",
+        "@babel/helper-validator-identifier": "^7.25.7",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@bcoe/v8-coverage": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+      "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+      "dev": true
+    },
     "node_modules/@cdklabs/tskb": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/@cdklabs/tskb/-/tskb-0.0.3.tgz",
@@ -5890,9 +5899,9 @@
       "dev": true
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
-      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
+      "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
       "cpu": [
         "ppc64"
       ],
@@ -5902,7 +5911,7 @@
         "aix"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/@esbuild/android-arm": {
@@ -6194,6 +6203,22 @@
         "node": ">=12"
       }
     },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
+      "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/@esbuild/openbsd-x64": {
       "version": "0.18.20",
       "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
@@ -6499,9 +6524,9 @@
       }
     },
     "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
       "dev": true,
       "engines": {
         "node": ">=12"
@@ -6889,9 +6914,9 @@
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
       "dev": true
     },
     "node_modules/@jridgewell/trace-mapping": {
@@ -12005,97 +12030,13 @@
       "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
       "dev": true
     },
-    "node_modules/@vitest/expect": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
-      "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
-      "dev": true,
-      "dependencies": {
-        "@vitest/spy": "1.4.0",
-        "@vitest/utils": "1.4.0",
-        "chai": "^4.3.10"
-      },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
-      }
-    },
-    "node_modules/@vitest/runner": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
-      "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
-      "dev": true,
-      "dependencies": {
-        "@vitest/utils": "1.4.0",
-        "p-limit": "^5.0.0",
-        "pathe": "^1.1.1"
-      },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
-      }
-    },
-    "node_modules/@vitest/runner/node_modules/p-limit": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
-      "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
-      "dev": true,
-      "dependencies": {
-        "yocto-queue": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@vitest/runner/node_modules/yocto-queue": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
-      "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.20"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@vitest/snapshot": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
-      "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
-      "dev": true,
-      "dependencies": {
-        "magic-string": "^0.30.5",
-        "pathe": "^1.1.1",
-        "pretty-format": "^29.7.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
-      }
-    },
-    "node_modules/@vitest/spy": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
-      "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
-      "dev": true,
-      "dependencies": {
-        "tinyspy": "^2.2.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
-      }
-    },
-    "node_modules/@vitest/utils": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
-      "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
+    "node_modules/@vitest/pretty-format": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz",
+      "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==",
       "dev": true,
       "dependencies": {
-        "diff-sequences": "^29.6.3",
-        "estree-walker": "^3.0.3",
-        "loupe": "^2.3.7",
-        "pretty-format": "^29.7.0"
+        "tinyrainbow": "^1.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
@@ -12831,15 +12772,6 @@
         "util": "^0.12.5"
       }
     },
-    "node_modules/assertion-error": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
-      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -13936,7 +13868,8 @@
     "node_modules/bowser": {
       "version": "2.11.0",
       "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
-      "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
+      "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==",
+      "dev": true
     },
     "node_modules/bplist-parser": {
       "version": "0.2.0",
@@ -14079,9 +14012,9 @@
       }
     },
     "node_modules/bundle-require": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
-      "integrity": "sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.0.0.tgz",
+      "integrity": "sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==",
       "dev": true,
       "dependencies": {
         "load-tsconfig": "^0.2.3"
@@ -14090,7 +14023,7 @@
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
       },
       "peerDependencies": {
-        "esbuild": ">=0.17"
+        "esbuild": ">=0.18"
       }
     },
     "node_modules/busboy": {
@@ -15160,24 +15093,6 @@
         "readable-stream": "^3.6.0"
       }
     },
-    "node_modules/chai": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
-      "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
-      "dev": true,
-      "dependencies": {
-        "assertion-error": "^1.1.0",
-        "check-error": "^1.0.3",
-        "deep-eql": "^4.1.3",
-        "get-func-name": "^2.0.2",
-        "loupe": "^2.3.6",
-        "pathval": "^1.1.1",
-        "type-detect": "^4.0.8"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -15201,18 +15116,6 @@
         "node": ">=10"
       }
     },
-    "node_modules/check-error": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
-      "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
-      "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.2"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/chokidar": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -16373,12 +16276,12 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
       "dev": true,
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -16431,18 +16334,6 @@
       "optional": true,
       "peer": true
     },
-    "node_modules/deep-eql": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
-      "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
-      "dev": true,
-      "dependencies": {
-        "type-detect": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/deep-extend": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -18038,15 +17929,6 @@
         "node": "6.* || 8.* || >= 10.*"
       }
     },
-    "node_modules/get-func-name": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/get-intrinsic": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -18231,23 +18113,21 @@
       "dev": true
     },
     "node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
-        "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
       },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
@@ -18631,6 +18511,12 @@
         "node": ">=12"
       }
     },
+    "node_modules/html-escaper": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+      "dev": true
+    },
     "node_modules/html-minifier": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
@@ -19482,16 +19368,90 @@
         "semver": "bin/semver.js"
       }
     },
-    "node_modules/jackspeak": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
-      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+    "node_modules/istanbul-lib-report": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
       "dev": true,
       "dependencies": {
-        "@isaacs/cliui": "^8.0.2"
+        "istanbul-lib-coverage": "^3.0.0",
+        "make-dir": "^4.0.0",
+        "supports-color": "^7.1.0"
       },
       "engines": {
-        "node": ">=14"
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-lib-report/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-report/node_modules/make-dir": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+      "dev": true,
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/istanbul-lib-report/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-source-maps": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+      "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.23",
+        "debug": "^4.1.1",
+        "istanbul-lib-coverage": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/istanbul-reports": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+      "dev": true,
+      "dependencies": {
+        "html-escaper": "^2.0.0",
+        "istanbul-lib-report": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -20195,12 +20155,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/jsonc-parser": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
-      "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
-      "dev": true
-    },
     "node_modules/jsonfile": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -20452,9 +20406,9 @@
       }
     },
     "node_modules/lilconfig": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
-      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
+      "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
       "dev": true,
       "engines": {
         "node": ">=14"
@@ -20550,22 +20504,6 @@
         "node": ">=6.11.5"
       }
     },
-    "node_modules/local-pkg": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
-      "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==",
-      "dev": true,
-      "dependencies": {
-        "mlly": "^1.4.2",
-        "pkg-types": "^1.0.3"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
     "node_modules/locate-path": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -20777,15 +20715,6 @@
         "loose-envify": "cli.js"
       }
     },
-    "node_modules/loupe": {
-      "version": "2.3.7",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
-      "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
-      "dev": true,
-      "dependencies": {
-        "get-func-name": "^2.0.1"
-      }
-    },
     "node_modules/lower-case": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
@@ -20802,15 +20731,23 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "version": "0.30.12",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
+      "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
       "dev": true,
       "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
-      },
-      "engines": {
-        "node": ">=12"
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/magicast": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+      "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/parser": "^7.25.4",
+        "@babel/types": "^7.25.4",
+        "source-map-js": "^1.2.0"
       }
     },
     "node_modules/make-dir": {
@@ -21175,9 +21112,9 @@
       "dev": true
     },
     "node_modules/minimatch": {
-      "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
-      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
       "dev": true,
       "dependencies": {
         "brace-expansion": "^2.0.1"
@@ -21222,9 +21159,9 @@
       }
     },
     "node_modules/minipass": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
-      "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
       "dev": true,
       "engines": {
         "node": ">=16 || 14 >=14.17"
@@ -21279,18 +21216,6 @@
       "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
       "dev": true
     },
-    "node_modules/mlly": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
-      "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.11.3",
-        "pathe": "^1.1.2",
-        "pkg-types": "^1.0.3",
-        "ufo": "^1.3.2"
-      }
-    },
     "node_modules/mnemonist": {
       "version": "0.39.8",
       "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
@@ -21420,9 +21345,9 @@
       "dev": true
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
       "dev": true
     },
     "node_modules/mute-stream": {
@@ -21481,11 +21406,6 @@
       "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==",
       "dev": true
     },
-    "node_modules/neverthrow": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-6.1.0.tgz",
-      "integrity": "sha512-xNbNjp/6M5vUV+mststgneJN9eJeJCDSYSBTaf3vxgvcKooP+8L0ATFpM8DGfmH7UWKJeoa24Qi33tBP9Ya3zA=="
-    },
     "node_modules/no-case": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
@@ -25509,6 +25429,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true
+    },
     "node_modules/pako": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
@@ -25657,29 +25583,26 @@
       "dev": true
     },
     "node_modules/path-scurry": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
-      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
       "dev": true,
       "dependencies": {
-        "lru-cache": "^9.1.1 || ^10.0.0",
+        "lru-cache": "^10.2.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": ">=16 || 14 >=14.18"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/path-scurry/node_modules/lru-cache": {
-      "version": "10.2.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
-      "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
-      "dev": true,
-      "engines": {
-        "node": "14 || >=16.14"
-      }
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true
     },
     "node_modules/path-to-regexp": {
       "version": "0.1.7",
@@ -25702,15 +25625,6 @@
       "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
       "dev": true
     },
-    "node_modules/pathval": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
-      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
-      "dev": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/peek-stream": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
@@ -25902,17 +25816,6 @@
         "node": ">=10"
       }
     },
-    "node_modules/pkg-types": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz",
-      "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==",
-      "dev": true,
-      "dependencies": {
-        "jsonc-parser": "^3.2.0",
-        "mlly": "^1.2.0",
-        "pathe": "^1.1.0"
-      }
-    },
     "node_modules/pkg-up": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
@@ -26080,9 +25983,9 @@
       }
     },
     "node_modules/postcss-load-config": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
-      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+      "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
       "dev": true,
       "funding": [
         {
@@ -26095,21 +25998,28 @@
         }
       ],
       "dependencies": {
-        "lilconfig": "^3.0.0",
-        "yaml": "^2.3.4"
+        "lilconfig": "^3.1.1"
       },
       "engines": {
-        "node": ">= 14"
+        "node": ">= 18"
       },
       "peerDependencies": {
+        "jiti": ">=1.21.0",
         "postcss": ">=8.0.9",
-        "ts-node": ">=9.0.0"
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
       },
       "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        },
         "postcss": {
           "optional": true
         },
-        "ts-node": {
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
           "optional": true
         }
       }
@@ -28190,12 +28100,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/send/node_modules/ms": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "dev": true
-    },
     "node_modules/serialize-javascript": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -29448,24 +29352,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/strip-literal": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
-      "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
-      "dev": true,
-      "dependencies": {
-        "js-tokens": "^8.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/strip-literal/node_modules/js-tokens": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
-      "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
-      "dev": true
-    },
     "node_modules/strnum": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
@@ -30139,24 +30025,60 @@
       "dev": true
     },
     "node_modules/tinybench": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
-      "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==",
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
+      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
       "dev": true
     },
-    "node_modules/tinypool": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
-      "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
+    "node_modules/tinyglobby": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz",
+      "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==",
       "dev": true,
+      "dependencies": {
+        "fdir": "^6.4.0",
+        "picomatch": "^4.0.2"
+      },
       "engines": {
-        "node": ">=14.0.0"
+        "node": ">=12.0.0"
       }
     },
-    "node_modules/tinyspy": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
-      "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz",
+      "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==",
+      "dev": true,
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+      "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -30340,24 +30262,26 @@
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
     },
     "node_modules/tsup": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.0.2.tgz",
-      "integrity": "sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz",
+      "integrity": "sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==",
       "dev": true,
       "dependencies": {
-        "bundle-require": "^4.0.0",
-        "cac": "^6.7.12",
-        "chokidar": "^3.5.1",
-        "debug": "^4.3.1",
-        "esbuild": "^0.19.2",
-        "execa": "^5.0.0",
-        "globby": "^11.0.3",
-        "joycon": "^3.0.1",
-        "postcss-load-config": "^4.0.1",
+        "bundle-require": "^5.0.0",
+        "cac": "^6.7.14",
+        "chokidar": "^3.6.0",
+        "consola": "^3.2.3",
+        "debug": "^4.3.5",
+        "esbuild": "^0.23.0",
+        "execa": "^5.1.1",
+        "joycon": "^3.1.1",
+        "picocolors": "^1.0.1",
+        "postcss-load-config": "^6.0.1",
         "resolve-from": "^5.0.0",
-        "rollup": "^4.0.2",
+        "rollup": "^4.19.0",
         "source-map": "0.8.0-beta.0",
-        "sucrase": "^3.20.3",
+        "sucrase": "^3.35.0",
+        "tinyglobby": "^0.2.1",
         "tree-kill": "^1.2.2"
       },
       "bin": {
@@ -30389,9 +30313,9 @@
       }
     },
     "node_modules/tsup/node_modules/@esbuild/android-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
-      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
+      "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
       "cpu": [
         "arm"
       ],
@@ -30401,13 +30325,13 @@
         "android"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/android-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
-      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
+      "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
       "cpu": [
         "arm64"
       ],
@@ -30417,13 +30341,13 @@
         "android"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/android-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
-      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
+      "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
       "cpu": [
         "x64"
       ],
@@ -30433,13 +30357,13 @@
         "android"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
-      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
+      "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
       "cpu": [
         "arm64"
       ],
@@ -30449,13 +30373,13 @@
         "darwin"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
-      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
+      "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
       "cpu": [
         "x64"
       ],
@@ -30465,13 +30389,13 @@
         "darwin"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
-      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
+      "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
       "cpu": [
         "arm64"
       ],
@@ -30481,13 +30405,13 @@
         "freebsd"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
-      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
+      "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
       "cpu": [
         "x64"
       ],
@@ -30497,13 +30421,13 @@
         "freebsd"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
-      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
+      "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
       "cpu": [
         "arm"
       ],
@@ -30513,13 +30437,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
-      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
+      "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
       "cpu": [
         "arm64"
       ],
@@ -30529,13 +30453,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
-      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
+      "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
       "cpu": [
         "ia32"
       ],
@@ -30545,13 +30469,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
-      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
+      "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
       "cpu": [
         "loong64"
       ],
@@ -30561,13 +30485,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
-      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
+      "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
       "cpu": [
         "mips64el"
       ],
@@ -30577,13 +30501,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
-      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
+      "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
       "cpu": [
         "ppc64"
       ],
@@ -30593,13 +30517,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
-      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
+      "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
       "cpu": [
         "riscv64"
       ],
@@ -30609,13 +30533,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
-      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
+      "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
       "cpu": [
         "s390x"
       ],
@@ -30625,13 +30549,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/linux-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
-      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
+      "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
       "cpu": [
         "x64"
       ],
@@ -30641,13 +30565,13 @@
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
+      "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
       "cpu": [
         "x64"
       ],
@@ -30657,13 +30581,13 @@
         "netbsd"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
+      "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
       "cpu": [
         "x64"
       ],
@@ -30673,13 +30597,13 @@
         "openbsd"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
-      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
+      "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
       "cpu": [
         "x64"
       ],
@@ -30689,13 +30613,13 @@
         "sunos"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
-      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
+      "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
       "cpu": [
         "arm64"
       ],
@@ -30705,13 +30629,13 @@
         "win32"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
-      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
+      "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
       "cpu": [
         "ia32"
       ],
@@ -30721,13 +30645,13 @@
         "win32"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/@esbuild/win32-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
-      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
+      "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
       "cpu": [
         "x64"
       ],
@@ -30737,45 +30661,46 @@
         "win32"
       ],
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       }
     },
     "node_modules/tsup/node_modules/esbuild": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
-      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+      "version": "0.23.1",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
+      "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
       },
       "engines": {
-        "node": ">=12"
+        "node": ">=18"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.12",
-        "@esbuild/android-arm": "0.19.12",
-        "@esbuild/android-arm64": "0.19.12",
-        "@esbuild/android-x64": "0.19.12",
-        "@esbuild/darwin-arm64": "0.19.12",
-        "@esbuild/darwin-x64": "0.19.12",
-        "@esbuild/freebsd-arm64": "0.19.12",
-        "@esbuild/freebsd-x64": "0.19.12",
-        "@esbuild/linux-arm": "0.19.12",
-        "@esbuild/linux-arm64": "0.19.12",
-        "@esbuild/linux-ia32": "0.19.12",
-        "@esbuild/linux-loong64": "0.19.12",
-        "@esbuild/linux-mips64el": "0.19.12",
-        "@esbuild/linux-ppc64": "0.19.12",
-        "@esbuild/linux-riscv64": "0.19.12",
-        "@esbuild/linux-s390x": "0.19.12",
-        "@esbuild/linux-x64": "0.19.12",
-        "@esbuild/netbsd-x64": "0.19.12",
-        "@esbuild/openbsd-x64": "0.19.12",
-        "@esbuild/sunos-x64": "0.19.12",
-        "@esbuild/win32-arm64": "0.19.12",
-        "@esbuild/win32-ia32": "0.19.12",
-        "@esbuild/win32-x64": "0.19.12"
+        "@esbuild/aix-ppc64": "0.23.1",
+        "@esbuild/android-arm": "0.23.1",
+        "@esbuild/android-arm64": "0.23.1",
+        "@esbuild/android-x64": "0.23.1",
+        "@esbuild/darwin-arm64": "0.23.1",
+        "@esbuild/darwin-x64": "0.23.1",
+        "@esbuild/freebsd-arm64": "0.23.1",
+        "@esbuild/freebsd-x64": "0.23.1",
+        "@esbuild/linux-arm": "0.23.1",
+        "@esbuild/linux-arm64": "0.23.1",
+        "@esbuild/linux-ia32": "0.23.1",
+        "@esbuild/linux-loong64": "0.23.1",
+        "@esbuild/linux-mips64el": "0.23.1",
+        "@esbuild/linux-ppc64": "0.23.1",
+        "@esbuild/linux-riscv64": "0.23.1",
+        "@esbuild/linux-s390x": "0.23.1",
+        "@esbuild/linux-x64": "0.23.1",
+        "@esbuild/netbsd-x64": "0.23.1",
+        "@esbuild/openbsd-arm64": "0.23.1",
+        "@esbuild/openbsd-x64": "0.23.1",
+        "@esbuild/sunos-x64": "0.23.1",
+        "@esbuild/win32-arm64": "0.23.1",
+        "@esbuild/win32-ia32": "0.23.1",
+        "@esbuild/win32-x64": "0.23.1"
       }
     },
     "node_modules/tsup/node_modules/resolve-from": {
@@ -30920,15 +30845,6 @@
         "win32"
       ]
     },
-    "node_modules/type-detect": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/type-fest": {
       "version": "4.15.0",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz",
@@ -30964,7 +30880,7 @@
       "version": "5.4.4",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
       "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
-      "dev": true,
+      "devOptional": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -31370,9 +31286,17 @@
       "dev": true
     },
     "node_modules/valibot": {
-      "version": "0.30.0",
-      "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.30.0.tgz",
-      "integrity": "sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA=="
+      "version": "0.42.1",
+      "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz",
+      "integrity": "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==",
+      "peerDependencies": {
+        "typescript": ">=5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
     },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
@@ -31457,1443 +31381,273 @@
         }
       }
     },
-    "node_modules/vite-node": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
-      "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
+    "node_modules/vite-plugin-ngrok": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-ngrok/-/vite-plugin-ngrok-1.0.0.tgz",
+      "integrity": "sha512-HhLjCXrxEF0XSKu/Wws3kHgcY49ZT0rqeZk/TL44XvaAFb4U7sI9HNyH6j2hCjiu0KLrXAz7ta1eAwJMDWHjBQ==",
       "dev": true,
       "dependencies": {
-        "cac": "^6.7.14",
-        "debug": "^4.3.4",
-        "pathe": "^1.1.1",
-        "picocolors": "^1.0.0",
-        "vite": "^5.0.0"
+        "@ngrok/ngrok": "^0.9.1",
+        "picocolors": "^1.0.0"
       },
+      "peerDependencies": {
+        "vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
+      }
+    },
+    "node_modules/vite/node_modules/rollup": {
+      "version": "3.29.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
+      "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
+      "dev": true,
       "bin": {
-        "vite-node": "vite-node.mjs"
+        "rollup": "dist/bin/rollup"
       },
       "engines": {
-        "node": "^18.0.0 || >=20.0.0"
+        "node": ">=14.18.0",
+        "npm": ">=8.0.0"
       },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
-      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
-      "cpu": [
-        "ppc64"
-      ],
+    "node_modules/w3c-xmlserializer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+      "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
       "dev": true,
       "optional": true,
-      "os": [
-        "aix"
-      ],
+      "peer": true,
+      "dependencies": {
+        "xml-name-validator": "^4.0.0"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">=14"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/android-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
-      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
-      "cpu": [
-        "arm"
-      ],
+    "node_modules/walker": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+      "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "makeerror": "1.0.12"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/android-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
-      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/watchpack": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
+      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
+      "dependencies": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">=10.13.0"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/android-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
-      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/wcwidth": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "defaults": "^1.0.3"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
-      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
       "engines": {
-        "node": ">=12"
+        "node": ">= 8"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/darwin-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
-      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/webcrypto-core": {
+      "version": "1.7.8",
+      "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.8.tgz",
+      "integrity": "sha512-eBR98r9nQXTqXt/yDRtInszPMjTaSAMJAFDg2AHsgrnczawT1asx9YNBX6k5p+MekbPF4+s/UJJrr88zsTqkSg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.3.8",
+        "@peculiar/json-schema": "^1.1.12",
+        "asn1js": "^3.0.1",
+        "pvtsutils": "^1.3.5",
+        "tslib": "^2.6.2"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
-      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
       "dev": true,
       "optional": true,
-      "os": [
-        "freebsd"
-      ],
+      "peer": true,
       "engines": {
         "node": ">=12"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
-      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/webpack": {
+      "version": "5.90.3",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
+      "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
+      "peer": true,
+      "dependencies": {
+        "@types/eslint-scope": "^3.7.3",
+        "@types/estree": "^1.0.5",
+        "@webassemblyjs/ast": "^1.11.5",
+        "@webassemblyjs/wasm-edit": "^1.11.5",
+        "@webassemblyjs/wasm-parser": "^1.11.5",
+        "acorn": "^8.7.1",
+        "acorn-import-assertions": "^1.9.0",
+        "browserslist": "^4.21.10",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.15.0",
+        "es-module-lexer": "^1.2.1",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.2.9",
+        "json-parse-even-better-errors": "^2.3.1",
+        "loader-runner": "^4.2.0",
+        "mime-types": "^2.1.27",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.2.0",
+        "tapable": "^2.1.1",
+        "terser-webpack-plugin": "^5.3.10",
+        "watchpack": "^2.4.0",
+        "webpack-sources": "^3.2.3"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
-      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
-      "cpu": [
-        "arm"
-      ],
+    "node_modules/webpack-sources": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+      "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
       "engines": {
-        "node": ">=12"
+        "node": ">=10.13.0"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
-      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
+      "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==",
+      "dev": true
+    },
+    "node_modules/webpack/node_modules/es-module-lexer": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.2.tgz",
+      "integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
+      "peer": true
+    },
+    "node_modules/webpack/node_modules/schema-utils": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
-      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
-      "cpu": [
-        "ia32"
-      ],
+    "node_modules/websocket-stream": {
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.2.tgz",
+      "integrity": "sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "duplexify": "^3.5.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.3",
+        "safe-buffer": "^5.1.2",
+        "ws": "^3.2.0",
+        "xtend": "^4.0.0"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-loong64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
-      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
-      "cpu": [
-        "loong64"
-      ],
+    "node_modules/websocket-stream/node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true
+    },
+    "node_modules/websocket-stream/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
-      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
-      "cpu": [
-        "mips64el"
-      ],
+    "node_modules/websocket-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/websocket-stream/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
-      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
-      "cpu": [
-        "ppc64"
-      ],
+    "node_modules/websocket-stream/node_modules/ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "async-limiter": "~1.0.0",
+        "safe-buffer": "~5.1.0",
+        "ultron": "~1.1.0"
       }
     },
-    "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
-      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
-      "cpu": [
-        "riscv64"
-      ],
+    "node_modules/whatwg-encoding": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+      "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
       "dev": true,
       "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/linux-s390x": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
-      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/linux-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
-      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/sunos-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
-      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/win32-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
-      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/win32-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
-      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/@esbuild/win32-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
-      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite-node/node_modules/esbuild": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
-      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.20.2",
-        "@esbuild/android-arm": "0.20.2",
-        "@esbuild/android-arm64": "0.20.2",
-        "@esbuild/android-x64": "0.20.2",
-        "@esbuild/darwin-arm64": "0.20.2",
-        "@esbuild/darwin-x64": "0.20.2",
-        "@esbuild/freebsd-arm64": "0.20.2",
-        "@esbuild/freebsd-x64": "0.20.2",
-        "@esbuild/linux-arm": "0.20.2",
-        "@esbuild/linux-arm64": "0.20.2",
-        "@esbuild/linux-ia32": "0.20.2",
-        "@esbuild/linux-loong64": "0.20.2",
-        "@esbuild/linux-mips64el": "0.20.2",
-        "@esbuild/linux-ppc64": "0.20.2",
-        "@esbuild/linux-riscv64": "0.20.2",
-        "@esbuild/linux-s390x": "0.20.2",
-        "@esbuild/linux-x64": "0.20.2",
-        "@esbuild/netbsd-x64": "0.20.2",
-        "@esbuild/openbsd-x64": "0.20.2",
-        "@esbuild/sunos-x64": "0.20.2",
-        "@esbuild/win32-arm64": "0.20.2",
-        "@esbuild/win32-ia32": "0.20.2",
-        "@esbuild/win32-x64": "0.20.2"
-      }
-    },
-    "node_modules/vite-node/node_modules/vite": {
-      "version": "5.2.8",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
-      "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
-      "dev": true,
-      "dependencies": {
-        "esbuild": "^0.20.1",
-        "postcss": "^8.4.38",
-        "rollup": "^4.13.0"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
-      "engines": {
-        "node": "^18.0.0 || >=20.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/vitejs/vite?sponsor=1"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.3"
-      },
-      "peerDependencies": {
-        "@types/node": "^18.0.0 || >=20.0.0",
-        "less": "*",
-        "lightningcss": "^1.21.0",
-        "sass": "*",
-        "stylus": "*",
-        "sugarss": "*",
-        "terser": "^5.4.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/node": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "lightningcss": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        },
-        "sugarss": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vite-plugin-ngrok": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-ngrok/-/vite-plugin-ngrok-1.0.0.tgz",
-      "integrity": "sha512-HhLjCXrxEF0XSKu/Wws3kHgcY49ZT0rqeZk/TL44XvaAFb4U7sI9HNyH6j2hCjiu0KLrXAz7ta1eAwJMDWHjBQ==",
-      "dev": true,
-      "dependencies": {
-        "@ngrok/ngrok": "^0.9.1",
-        "picocolors": "^1.0.0"
-      },
-      "peerDependencies": {
-        "vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
-      }
-    },
-    "node_modules/vite/node_modules/rollup": {
-      "version": "3.29.4",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
-      "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
-      "dev": true,
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
-      "engines": {
-        "node": ">=14.18.0",
-        "npm": ">=8.0.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/vitest": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
-      "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
-      "dev": true,
-      "dependencies": {
-        "@vitest/expect": "1.4.0",
-        "@vitest/runner": "1.4.0",
-        "@vitest/snapshot": "1.4.0",
-        "@vitest/spy": "1.4.0",
-        "@vitest/utils": "1.4.0",
-        "acorn-walk": "^8.3.2",
-        "chai": "^4.3.10",
-        "debug": "^4.3.4",
-        "execa": "^8.0.1",
-        "local-pkg": "^0.5.0",
-        "magic-string": "^0.30.5",
-        "pathe": "^1.1.1",
-        "picocolors": "^1.0.0",
-        "std-env": "^3.5.0",
-        "strip-literal": "^2.0.0",
-        "tinybench": "^2.5.1",
-        "tinypool": "^0.8.2",
-        "vite": "^5.0.0",
-        "vite-node": "1.4.0",
-        "why-is-node-running": "^2.2.2"
-      },
-      "bin": {
-        "vitest": "vitest.mjs"
-      },
-      "engines": {
-        "node": "^18.0.0 || >=20.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/vitest"
-      },
-      "peerDependencies": {
-        "@edge-runtime/vm": "*",
-        "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "1.4.0",
-        "@vitest/ui": "1.4.0",
-        "happy-dom": "*",
-        "jsdom": "*"
-      },
-      "peerDependenciesMeta": {
-        "@edge-runtime/vm": {
-          "optional": true
-        },
-        "@types/node": {
-          "optional": true
-        },
-        "@vitest/browser": {
-          "optional": true
-        },
-        "@vitest/ui": {
-          "optional": true
-        },
-        "happy-dom": {
-          "optional": true
-        },
-        "jsdom": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vitest-mock-extended": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/vitest-mock-extended/-/vitest-mock-extended-1.3.1.tgz",
-      "integrity": "sha512-OpghYjh4BDuQ/Mzs3lFMQ1QRk9D8/2O9T47MLUA5eLn7K4RWIy+MfIivYOWEyxjTENjsBnzgMihDjyNalN/K0Q==",
-      "dev": true,
-      "dependencies": {
-        "ts-essentials": "^9.3.2"
-      },
-      "peerDependencies": {
-        "typescript": "3.x || 4.x || 5.x",
-        "vitest": ">=0.31.1"
-      }
-    },
-    "node_modules/vitest-mock-extended/node_modules/ts-essentials": {
-      "version": "9.4.1",
-      "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz",
-      "integrity": "sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==",
-      "dev": true,
-      "peerDependencies": {
-        "typescript": ">=4.1.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
-      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/android-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
-      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/android-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
-      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/android-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
-      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
-      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
-      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
-      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
-      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-arm": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
-      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
-      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
-      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
-      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
-      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
-      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
-      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
-      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/linux-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
-      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
-      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
-      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
-      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
-      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/@esbuild/win32-x64": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
-      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vitest/node_modules/esbuild": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
-      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.20.2",
-        "@esbuild/android-arm": "0.20.2",
-        "@esbuild/android-arm64": "0.20.2",
-        "@esbuild/android-x64": "0.20.2",
-        "@esbuild/darwin-arm64": "0.20.2",
-        "@esbuild/darwin-x64": "0.20.2",
-        "@esbuild/freebsd-arm64": "0.20.2",
-        "@esbuild/freebsd-x64": "0.20.2",
-        "@esbuild/linux-arm": "0.20.2",
-        "@esbuild/linux-arm64": "0.20.2",
-        "@esbuild/linux-ia32": "0.20.2",
-        "@esbuild/linux-loong64": "0.20.2",
-        "@esbuild/linux-mips64el": "0.20.2",
-        "@esbuild/linux-ppc64": "0.20.2",
-        "@esbuild/linux-riscv64": "0.20.2",
-        "@esbuild/linux-s390x": "0.20.2",
-        "@esbuild/linux-x64": "0.20.2",
-        "@esbuild/netbsd-x64": "0.20.2",
-        "@esbuild/openbsd-x64": "0.20.2",
-        "@esbuild/sunos-x64": "0.20.2",
-        "@esbuild/win32-arm64": "0.20.2",
-        "@esbuild/win32-ia32": "0.20.2",
-        "@esbuild/win32-x64": "0.20.2"
-      }
-    },
-    "node_modules/vitest/node_modules/execa": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
-      "dev": true,
-      "dependencies": {
-        "cross-spawn": "^7.0.3",
-        "get-stream": "^8.0.1",
-        "human-signals": "^5.0.0",
-        "is-stream": "^3.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^5.1.0",
-        "onetime": "^6.0.0",
-        "signal-exit": "^4.1.0",
-        "strip-final-newline": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=16.17"
-      },
-      "funding": {
-        "url": "https://github.com/sindresorhus/execa?sponsor=1"
-      }
-    },
-    "node_modules/vitest/node_modules/get-stream": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-      "dev": true,
-      "engines": {
-        "node": ">=16"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/human-signals": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=16.17.0"
-      }
-    },
-    "node_modules/vitest/node_modules/is-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/mimic-fn": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/npm-run-path": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^4.0.0"
-      },
-      "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/onetime": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-      "dev": true,
-      "dependencies": {
-        "mimic-fn": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/path-key": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/signal-exit": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
-      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-      "dev": true,
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/vitest/node_modules/strip-final-newline": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/vitest/node_modules/vite": {
-      "version": "5.2.8",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
-      "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
-      "dev": true,
-      "dependencies": {
-        "esbuild": "^0.20.1",
-        "postcss": "^8.4.38",
-        "rollup": "^4.13.0"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
-      "engines": {
-        "node": "^18.0.0 || >=20.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/vitejs/vite?sponsor=1"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.3"
-      },
-      "peerDependencies": {
-        "@types/node": "^18.0.0 || >=20.0.0",
-        "less": "*",
-        "lightningcss": "^1.21.0",
-        "sass": "*",
-        "stylus": "*",
-        "sugarss": "*",
-        "terser": "^5.4.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/node": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "lightningcss": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        },
-        "sugarss": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/w3c-xmlserializer": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
-      "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "xml-name-validator": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/walker": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
-      "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
-      "dev": true,
-      "dependencies": {
-        "makeerror": "1.0.12"
-      }
-    },
-    "node_modules/watchpack": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
-      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
-      "dev": true,
-      "dependencies": {
-        "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.1.2"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/wcwidth": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
-      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
-      "dev": true,
-      "dependencies": {
-        "defaults": "^1.0.3"
-      }
-    },
-    "node_modules/web-streams-polyfill": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
-      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/webcrypto-core": {
-      "version": "1.7.8",
-      "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.8.tgz",
-      "integrity": "sha512-eBR98r9nQXTqXt/yDRtInszPMjTaSAMJAFDg2AHsgrnczawT1asx9YNBX6k5p+MekbPF4+s/UJJrr88zsTqkSg==",
-      "dev": true,
-      "dependencies": {
-        "@peculiar/asn1-schema": "^2.3.8",
-        "@peculiar/json-schema": "^1.1.12",
-        "asn1js": "^3.0.1",
-        "pvtsutils": "^1.3.5",
-        "tslib": "^2.6.2"
-      }
-    },
-    "node_modules/webidl-conversions": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
-      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/webpack": {
-      "version": "5.90.3",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
-      "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.11.5",
-        "@webassemblyjs/wasm-edit": "^1.11.5",
-        "@webassemblyjs/wasm-parser": "^1.11.5",
-        "acorn": "^8.7.1",
-        "acorn-import-assertions": "^1.9.0",
-        "browserslist": "^4.21.10",
-        "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.15.0",
-        "es-module-lexer": "^1.2.1",
-        "eslint-scope": "5.1.1",
-        "events": "^3.2.0",
-        "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.2.9",
-        "json-parse-even-better-errors": "^2.3.1",
-        "loader-runner": "^4.2.0",
-        "mime-types": "^2.1.27",
-        "neo-async": "^2.6.2",
-        "schema-utils": "^3.2.0",
-        "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.3.10",
-        "watchpack": "^2.4.0",
-        "webpack-sources": "^3.2.3"
-      },
-      "bin": {
-        "webpack": "bin/webpack.js"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/webpack"
-      },
-      "peerDependenciesMeta": {
-        "webpack-cli": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/webpack-sources": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
-      "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/webpack-virtual-modules": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
-      "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==",
-      "dev": true
-    },
-    "node_modules/webpack/node_modules/es-module-lexer": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.2.tgz",
-      "integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==",
-      "dev": true,
-      "peer": true
-    },
-    "node_modules/webpack/node_modules/schema-utils": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "@types/json-schema": "^7.0.8",
-        "ajv": "^6.12.5",
-        "ajv-keywords": "^3.5.2"
-      },
-      "engines": {
-        "node": ">= 10.13.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/webpack"
-      }
-    },
-    "node_modules/websocket-stream": {
-      "version": "5.5.2",
-      "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.2.tgz",
-      "integrity": "sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==",
-      "dev": true,
-      "dependencies": {
-        "duplexify": "^3.5.1",
-        "inherits": "^2.0.1",
-        "readable-stream": "^2.3.3",
-        "safe-buffer": "^5.1.2",
-        "ws": "^3.2.0",
-        "xtend": "^4.0.0"
-      }
-    },
-    "node_modules/websocket-stream/node_modules/isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
-      "dev": true
-    },
-    "node_modules/websocket-stream/node_modules/readable-stream": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-      "dev": true,
-      "dependencies": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "node_modules/websocket-stream/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-      "dev": true
-    },
-    "node_modules/websocket-stream/node_modules/string_decoder": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "dev": true,
-      "dependencies": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "node_modules/websocket-stream/node_modules/ws": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
-      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
-      "dev": true,
-      "dependencies": {
-        "async-limiter": "~1.0.0",
-        "safe-buffer": "~5.1.0",
-        "ultron": "~1.1.0"
-      }
-    },
-    "node_modules/whatwg-encoding": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
-      "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "iconv-lite": "0.6.3"
-      },
+      "peer": true,
+      "dependencies": {
+        "iconv-lite": "0.6.3"
+      },
       "engines": {
         "node": ">=12"
       }
@@ -32973,9 +31727,9 @@
       }
     },
     "node_modules/why-is-node-running": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz",
-      "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
       "dev": true,
       "dependencies": {
         "siginfo": "^2.0.0",
@@ -33282,18 +32036,6 @@
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
       "dev": true
     },
-    "node_modules/yaml": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
-      "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
-      "dev": true,
-      "bin": {
-        "yaml": "bin.mjs"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/yargs": {
       "version": "17.7.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -33562,29 +32304,28 @@
         "@noble/curves": "^1.4.0",
         "base64url": "^3.0.1",
         "blakejs": "^1.2.1",
-        "bowser": "^2.11.0",
         "buffer": "^6.0.3",
         "immer": "^10.0.4",
         "lit": "^3.1.2",
         "lit-html": "^3.1.2",
-        "neverthrow": "^6.1.0",
+        "neverthrow": "^8.0.0",
         "rxjs": "^7.8.1",
         "tslog": ">=4.8.0",
         "uuid": "^10.0.0",
-        "valibot": "0.30.0"
+        "valibot": "0.42.1"
       },
       "devDependencies": {
         "@radixdlt/connect-button": "*",
         "@saithodev/semantic-release-backmerge": "^3.2.1",
         "@semantic-release/exec": "^6.0.3",
+        "@vitest/coverage-v8": "^2.1.3",
         "radix-connect-common": "*",
         "semantic-release": "^23.0.0",
         "semantic-release-replace-plugin": "^1.2.7",
-        "tsup": "^8.0.2",
+        "tsup": "^8.3.0",
         "typescript": "^5.4.4",
         "vite-plugin-singlefile": "^2.0.2",
-        "vitest": "^1.4.0",
-        "vitest-mock-extended": "^1.3.1"
+        "vitest": "^2.1.3"
       },
       "engines": {
         "node": ">=18"
@@ -33602,7 +32343,6 @@
       "os": [
         "aix"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33619,7 +32359,6 @@
       "os": [
         "android"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33636,7 +32375,6 @@
       "os": [
         "android"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33653,7 +32391,6 @@
       "os": [
         "android"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33670,7 +32407,6 @@
       "os": [
         "darwin"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33687,7 +32423,6 @@
       "os": [
         "darwin"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33704,7 +32439,6 @@
       "os": [
         "freebsd"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33721,7 +32455,6 @@
       "os": [
         "freebsd"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33738,7 +32471,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33755,7 +32487,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33772,7 +32503,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33789,7 +32519,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33806,7 +32535,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33823,7 +32551,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33840,7 +32567,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33857,7 +32583,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33874,7 +32599,6 @@
       "os": [
         "linux"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33891,7 +32615,6 @@
       "os": [
         "netbsd"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33908,7 +32631,6 @@
       "os": [
         "openbsd"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33925,7 +32647,6 @@
       "os": [
         "sunos"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33942,7 +32663,6 @@
       "os": [
         "win32"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33959,7 +32679,6 @@
       "os": [
         "win32"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -33976,7 +32695,6 @@
       "os": [
         "win32"
       ],
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -34256,6 +32974,133 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/dapp-toolkit/node_modules/@vitest/coverage-v8": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.3.tgz",
+      "integrity": "sha512-2OJ3c7UPoFSmBZwqD2VEkUw6A/tzPF0LmW0ZZhhB8PFxuc+9IBG/FaSM+RLEenc7ljzFvGN+G0nGQoZnh7sy2A==",
+      "dev": true,
+      "dependencies": {
+        "@ampproject/remapping": "^2.3.0",
+        "@bcoe/v8-coverage": "^0.2.3",
+        "debug": "^4.3.6",
+        "istanbul-lib-coverage": "^3.2.2",
+        "istanbul-lib-report": "^3.0.1",
+        "istanbul-lib-source-maps": "^5.0.6",
+        "istanbul-reports": "^3.1.7",
+        "magic-string": "^0.30.11",
+        "magicast": "^0.3.4",
+        "std-env": "^3.7.0",
+        "test-exclude": "^7.0.1",
+        "tinyrainbow": "^1.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@vitest/browser": "2.1.3",
+        "vitest": "2.1.3"
+      },
+      "peerDependenciesMeta": {
+        "@vitest/browser": {
+          "optional": true
+        }
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/expect": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz",
+      "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/spy": "2.1.3",
+        "@vitest/utils": "2.1.3",
+        "chai": "^5.1.1",
+        "tinyrainbow": "^1.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/mocker": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz",
+      "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/spy": "2.1.3",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.11"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@vitest/spy": "2.1.3",
+        "msw": "^2.3.5",
+        "vite": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/runner": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz",
+      "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/utils": "2.1.3",
+        "pathe": "^1.1.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/snapshot": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz",
+      "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/pretty-format": "2.1.3",
+        "magic-string": "^0.30.11",
+        "pathe": "^1.1.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/spy": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz",
+      "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==",
+      "dev": true,
+      "dependencies": {
+        "tinyspy": "^3.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "packages/dapp-toolkit/node_modules/@vitest/utils": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz",
+      "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/pretty-format": "2.1.3",
+        "loupe": "^3.1.1",
+        "tinyrainbow": "^1.2.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "packages/dapp-toolkit/node_modules/aggregate-error": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz",
@@ -34272,6 +33117,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/dapp-toolkit/node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "packages/dapp-toolkit/node_modules/before-after-hook": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
@@ -34301,6 +33155,22 @@
         "ieee754": "^1.2.1"
       }
     },
+    "packages/dapp-toolkit/node_modules/chai": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
+      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "dev": true,
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "packages/dapp-toolkit/node_modules/chalk": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
@@ -34313,6 +33183,15 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "packages/dapp-toolkit/node_modules/check-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 16"
+      }
+    },
     "packages/dapp-toolkit/node_modules/clean-stack": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz",
@@ -34413,6 +33292,15 @@
         }
       }
     },
+    "packages/dapp-toolkit/node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "packages/dapp-toolkit/node_modules/env-ci": {
       "version": "11.0.0",
       "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz",
@@ -34432,7 +33320,6 @@
       "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
       "dev": true,
       "hasInstallScript": true,
-      "peer": true,
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -34672,6 +33559,12 @@
         "@types/trusted-types": "^2.0.2"
       }
     },
+    "packages/dapp-toolkit/node_modules/loupe": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
+      "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
+      "dev": true
+    },
     "packages/dapp-toolkit/node_modules/lru-cache": {
       "version": "10.2.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
@@ -34737,6 +33630,14 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/dapp-toolkit/node_modules/neverthrow": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.0.0.tgz",
+      "integrity": "sha512-SX2Z50+U27I+CF3NwHE9J8MB6+bYRRub3U+1nAKxnL6c+2vW2l/WsYEC0e3Wqg8DwiJvrquqE0YhxlVTzGJGsg==",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "packages/dapp-toolkit/node_modules/node-emoji": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz",
@@ -37622,6 +36523,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/dapp-toolkit/node_modules/pathval": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
     "packages/dapp-toolkit/node_modules/read-pkg": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
@@ -37798,6 +36708,20 @@
         "node": ">=14.18"
       }
     },
+    "packages/dapp-toolkit/node_modules/test-exclude": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+      "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+      "dev": true,
+      "dependencies": {
+        "@istanbuljs/schema": "^0.1.2",
+        "glob": "^10.4.1",
+        "minimatch": "^9.0.4"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "packages/dapp-toolkit/node_modules/text-extensions": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz",
@@ -37810,6 +36734,24 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/dapp-toolkit/node_modules/tinypool": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
+      "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      }
+    },
+    "packages/dapp-toolkit/node_modules/tinyspy": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "packages/dapp-toolkit/node_modules/tslog": {
       "version": "4.8.0",
       "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.8.0.tgz",
@@ -37844,7 +36786,6 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
       "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",
@@ -37899,6 +36840,27 @@
         }
       }
     },
+    "packages/dapp-toolkit/node_modules/vite-node": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz",
+      "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==",
+      "dev": true,
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.3.6",
+        "pathe": "^1.1.2",
+        "vite": "^5.0.0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "packages/dapp-toolkit/node_modules/vite-plugin-singlefile": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.0.2.tgz",
@@ -37915,6 +36877,70 @@
         "vite": "^5.3.1"
       }
     },
+    "packages/dapp-toolkit/node_modules/vitest": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz",
+      "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/expect": "2.1.3",
+        "@vitest/mocker": "2.1.3",
+        "@vitest/pretty-format": "^2.1.3",
+        "@vitest/runner": "2.1.3",
+        "@vitest/snapshot": "2.1.3",
+        "@vitest/spy": "2.1.3",
+        "@vitest/utils": "2.1.3",
+        "chai": "^5.1.1",
+        "debug": "^4.3.6",
+        "magic-string": "^0.30.11",
+        "pathe": "^1.1.2",
+        "std-env": "^3.7.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.0",
+        "tinypool": "^1.0.0",
+        "tinyrainbow": "^1.2.0",
+        "vite": "^5.0.0",
+        "vite-node": "2.1.3",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "@vitest/browser": "2.1.3",
+        "@vitest/ui": "2.1.3",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
     "packages/eslint-config": {
       "name": "@repo/eslint-config",
       "version": "0.0.0",
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 17b89fa3..4dc56bdb 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -14,6 +14,7 @@ export const RadixButtonTheme = {
   black: 'black',
   'white-with-outline': 'white-with-outline',
   white: 'white',
+  custom: 'custom',
 } as const
 
 export type RadixButtonTheme = keyof typeof RadixButtonTheme
@@ -28,17 +29,24 @@ export type RadixButtonMode = keyof typeof RadixButtonMode
 export type PersonaData = { field: string; value: string }
 
 export const RequestStatus = {
+  fail: 'fail',
+  ignored: 'ignored',
   pending: 'pending',
   success: 'success',
-  fail: 'fail',
+  timedOut: 'timedOut',
   cancelled: 'cancelled',
-  ignored: 'ignored',
+  /**
+   * Pending commit status is for preauthorization which was signed but not yet successfully committed to the network
+   */
+  pendingCommit: 'pendingCommit',
 } as const
 
 export const RequestItemType = {
-  loginRequest: 'loginRequest',
   dataRequest: 'dataRequest',
+  proofRequest: 'proofRequest',
+  loginRequest: 'loginRequest',
   sendTransaction: 'sendTransaction',
+  preAuthorizationRequest: 'preAuthorizationRequest',
 } as const
 
 export type RequestItemType = typeof RequestItemType
@@ -47,6 +55,10 @@ export type RequestItemTypes = keyof typeof RequestItemType
 
 export type RequestStatusTypes = keyof typeof RequestStatus
 
+/**
+ * Not used in the codebase. Will be removed in the next major release future.
+ * @deprecated
+ */
 export type WalletRequest<
   RequestType extends RequestItemTypes,
   Status extends RequestStatusTypes,
@@ -58,6 +70,7 @@ export type WalletRequest<
   timestamp: number
   showCancel?: boolean
   transactionIntentHash?: string
+  transactionStatus?: string
   walletInteraction: any
   walletResponse?: any
   metadata: Record
@@ -75,4 +88,7 @@ export type RequestItem = {
   walletResponse?: any
   sentToWallet?: boolean
   isOneTimeRequest?: boolean
+  metadata?: Record
+  walletData?: any
 }
+
diff --git a/packages/connect-button/.storybook/preview-head.html b/packages/connect-button/.storybook/preview-head.html
index 7100922b..dfe59ea4 100644
--- a/packages/connect-button/.storybook/preview-head.html
+++ b/packages/connect-button/.storybook/preview-head.html
@@ -1,3 +1,13 @@
 
+
+
diff --git a/packages/connect-button/README.md b/packages/connect-button/README.md
index d8d2db1f..a74dbced 100644
--- a/packages/connect-button/README.md
+++ b/packages/connect-button/README.md
@@ -1,18 +1,6 @@
-# Radix Logo √ Connect Button
+# What is √ Connect Button?
 
-[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
-
-The √ Connect Button is a framework agnostic web component to help developers connect users and their Radix Wallet to their dApps.
-
-It appears as a consistent, Radix-branded UI element that helps users identify your dApp website as a Radix dApp. When used with [Radix dApp Toolkit](https://github.com/radixdlt/radix-dapp-toolkit) it is compatible with the Radix Wallet – and it automatically provides a consistent user experience for users to connect with their wallet and see the current status of the connection between dApp and Radix Wallet.
-
-- [ √ Connect Button](#--connect-button)
-- [Usage](#usage)
-  - [Getting started](#getting-started)
-  - [Setting properties programmatically](#setting-properties-programmatically)
-  - [Events](#events)
-- [Playground](#playground)
-- [License](#license)
+The **√ Connect Button** is a framework agnostic [custom element](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) which appears as a consistent, Radix-branded UI that helps users identify your dApp website as a Radix dApp. It communicates with outer world through attributes and DOM events.
 
 # Usage
 
@@ -112,6 +100,9 @@ type ConnectButtonEvents = {
 
 You can play around with different settings using our storybook instance. Visit [connect-button-storybook.radixdlt.com](https://connect-button-storybook.radixdlt.com/) to easily experiment with Connect Button.
 
+> [!IMPORTANT]
+> Placing Connect Button inside container which uses `backdrop-filter` can cause troubles. Please check ["header with blur"](https://connect-button-storybook.radixdlt.com/?path=/docs/radix-header-with-blur--docs) section inside storybook to check for workaround solution
+
 # License
 
 The √ Connect Button binaries are licensed under the [Radix Software EULA](http://www.radixdlt.com/terms/genericEULA)
diff --git a/packages/connect-button/src/components/account/account.stories.ts b/packages/connect-button/src/components/account/account.stories.ts
index 8c73cc71..e25f4331 100644
--- a/packages/connect-button/src/components/account/account.stories.ts
+++ b/packages/connect-button/src/components/account/account.stories.ts
@@ -15,22 +15,6 @@ const accounts = [
     'Main',
     'account_tdx_21_12x4zx09f8962a9wesfqvxaue0qn6m39r3cpysrjd6dtqppzhrkjrsr',
   ],
-  [
-    'Saving',
-    'account_tdx_21_12xdjp5dq7haph4c75mst99mc26gkm8mys70v6qlyz0fz86f9ucy0ru',
-  ],
-  [
-    'Degen',
-    'account_tdx_21_1298kg54s9r9evc5tgglj2wrqsatuflwxg5s3m845uut6t3jtyh6cyy',
-  ],
-  [
-    'Gaming',
-    'account_tdx_21_12y78nedvqg9svp49fjs4f9y5kreweqxt6vszaprnfq8kjhralku6fz',
-  ],
-  [
-    'Trading',
-    'account_tdx_21_128pncqprt3gfew04aefqy549ecvfp0a99mxjpa6wcpl2n2ymqr8gj3',
-  ],
   [
     'Staking',
     'account_tdx_21_12yccemy8vx37qkctmpkgdtatxe8mdmwl9mndv5dx69mj7tg45d4q88',
@@ -39,31 +23,40 @@ const accounts = [
     'Professional',
     'account_tdx_21_129tr5q2g6eh7zxwzl6tj0ndq87zzuqynqt56xpe3v2pf5k9wp67ju6',
   ],
-  [
-    'Fun',
-    'account_tdx_21_12xgzze2krhmw95r07y4pccssgyjxzwgem86hndy8cujfzhkggdpt7s',
-  ],
   [
     'Travel',
     'account_tdx_21_129q44nllnywkm8pscgqfq5wkpcfxtq2xffyca745c3fau3swhkhrjw',
   ],
-  [
-    'Alpha',
-    'account_tdx_21_12yc8neefcqfum2u4r5xtgder57va8ahdjm3qr9eatyhmdec62ya6m4',
-  ],
-  [
-    'Beta',
-    'account_tdx_21_12yg7c2752f4uwy6ayljg3g5pvj36xxdy690hj7fpllsed53jsgczz4',
-  ],
   [
     'VeryLongAccountNameVeryLongAccountNameVeryLongAccountName',
     'account_tdx_21_129vzduy6q5ufxxekf66eqdjy2vrm6ezdl0sh5kjhgrped9p5k6t9nf',
   ],
 ]
 
-export const Primary: Story = {
+export const Single: Story = {
+  render: (args) => html`
+    
+  `,
+  args: {
+    address:
+      'account_tdx_21_129tr5q2g6eh7zxwzl6tj0ndq87zzuqynqt56xpe3v2pf5k9wp67ju6',
+    label: 'Radix Account',
+    appearanceId: 0,
+  },
+}
+
+export const Multiple: Story = {
   render: () => html`
-    
+ +
${accounts.map( ([label, address], index) => html` +
+ ${createRow({ ...args, theme: 'custom', connected: false })} +
${createRow({ ...args, theme: 'white', connected: false })}
diff --git a/packages/connect-button/src/components/button/button.ts b/packages/connect-button/src/components/button/button.ts index 10fae7a6..ddd4f373 100644 --- a/packages/connect-button/src/components/button/button.ts +++ b/packages/connect-button/src/components/button/button.ts @@ -122,7 +122,6 @@ export class RadixButton extends LitElement { justify-content: flex-end; container-type: inline-size; user-select: none; - --radix-connect-button-text-color: var(--color-light); } :host([full-width]) > button { diff --git a/packages/connect-button/src/components/card/card.stories.ts b/packages/connect-button/src/components/card/card.stories.ts index 3e19f7e5..b32c0224 100644 --- a/packages/connect-button/src/components/card/card.stories.ts +++ b/packages/connect-button/src/components/card/card.stories.ts @@ -97,7 +97,7 @@ export const Requests: Story = { console.log('onIgnoreTransactionItem', event) }} ?showCancel="${args.showCancel}" - transactionIntentHash="${args.transactionIntentHash}" + hash="${args.transactionIntentHash}" > `, diff --git a/packages/connect-button/src/components/card/request-card.ts b/packages/connect-button/src/components/card/request-card.ts index a33287e2..fbab9f8c 100644 --- a/packages/connect-button/src/components/card/request-card.ts +++ b/packages/connect-button/src/components/card/request-card.ts @@ -45,7 +45,7 @@ export class RadixRequestCard extends LitElement { @property({ type: String, }) - transactionIntentHash: string = '' + hash: string = '' render() { const icon = this.getIconFromStatus() @@ -57,8 +57,10 @@ export class RadixRequestCard extends LitElement { cancelled: 'Transaction Cancelled', ignored: 'Transaction Ignored', success: 'Send transaction', + pendingCommit: '', + timedOut: '', content: html` - ${this.renderTxIntentHash()} + ${this.renderHash()} ${this.status === 'pending' ? html`
Open your Radix Wallet app to review the transaction @@ -73,11 +75,41 @@ export class RadixRequestCard extends LitElement { : ''} `, }, + preAuthorizationRequest: { + pending: 'Preauthorization Pending', + fail: 'Preauthorization Failed', + cancelled: 'Preauthorization Cancelled', + success: 'Preauthorization Request', + ignored: 'Preauthorization Ignored', + pendingCommit: 'Preauthorization Lookup', + timedOut: 'Preauthorization Timed Out', + content: html` + ${this.renderHash()} + ${this.status === 'pending' + ? html`
+ Open your Radix Wallet app to review the preauthorization + ${this.showCancel + ? html`
+ Cancel +
` + : html`
+ Ignore +
`} +
` + : this.status === RequestStatus.pendingCommit + ? html`
+
Ignore
+
` + : ''} + `, + }, dataRequest: { pending: 'Data Request Pending', fail: 'Data Request Rejected', cancelled: 'Data Request Rejected', ignored: '', + pendingCommit: '', + timedOut: '', success: 'Data Request', content: this.getRequestContentTemplate( 'Open Your Radix Wallet App to complete the request', @@ -89,6 +121,20 @@ export class RadixRequestCard extends LitElement { cancelled: 'Login Request Rejected', success: 'Login Request', ignored: '', + pendingCommit: '', + timedOut: '', + content: this.getRequestContentTemplate( + 'Open Your Radix Wallet App to complete the request', + ), + }, + proofRequest: { + pending: 'Proof Request Pending', + fail: 'Proof Request Rejected', + cancelled: 'Proof Request Rejected', + success: 'Proof Request', + ignored: '', + pendingCommit: '', + timedOut: '', content: this.getRequestContentTemplate( 'Open Your Radix Wallet App to complete the request', ), @@ -116,7 +162,7 @@ export class RadixRequestCard extends LitElement { : '' } - private isErrorStatus(status: RequestStatusTypes) { + private hasErrorIcon(status: RequestStatusTypes) { return ( [ RequestStatus.cancelled, @@ -126,19 +172,31 @@ export class RadixRequestCard extends LitElement { ).includes(status) } + private hasPendingIcon(status: RequestStatusTypes) { + return ([RequestStatus.pending, RequestStatus.pendingCommit] as string[]).includes( + status, + ) + } + + private hasIgnoredIcon(status: RequestStatusTypes) { + return ( + [RequestStatus.ignored, RequestStatus.timedOut] as string[] + ).includes(status) + } + private getIconFromStatus() { - return this.status === RequestStatus.pending + return this.hasPendingIcon(this.status) ? 'pending' - : this.status === RequestStatus.ignored + : this.hasIgnoredIcon(this.status) ? 'ignored' - : this.isErrorStatus(this.status) + : this.hasErrorIcon(this.status) ? 'error' : 'checked' } private getStylingFromStatus() { return classMap({ - dimmed: this.isErrorStatus(this.status), + dimmed: this.hasErrorIcon(this.status), inverted: this.status === 'pending', }) } @@ -169,12 +227,12 @@ export class RadixRequestCard extends LitElement { ) } - private renderTxIntentHash() { - return this.transactionIntentHash + private renderHash() { + return this.hash ? html`
ID: { event.preventDefault() this.dispatchEvent( @@ -182,8 +240,11 @@ export class RadixRequestCard extends LitElement { bubbles: true, composed: true, detail: { - type: 'transaction', - data: this.transactionIntentHash, + type: + this.type === 'sendTransaction' + ? 'transaction' + : 'subintent', + data: this.hash, }, }), ) diff --git a/packages/connect-button/src/components/connect-button.ts b/packages/connect-button/src/components/connect-button.ts index 13b3dc61..88fcdae8 100644 --- a/packages/connect-button/src/components/connect-button.ts +++ b/packages/connect-button/src/components/connect-button.ts @@ -195,7 +195,6 @@ export class ConnectButton extends LitElement { private toggleParentBackdropFilter() { const OPACITY_TRANSITION_DURATION = 180 - if (!this.isMobile) return if (!this.showPopoverMenu && this.parentElementWithBackdropFilter) { setTimeout(() => { diff --git a/packages/connect-button/src/components/pages/requests.ts b/packages/connect-button/src/components/pages/requests.ts index 16c36e29..98f09ba8 100644 --- a/packages/connect-button/src/components/pages/requests.ts +++ b/packages/connect-button/src/components/pages/requests.ts @@ -36,7 +36,7 @@ export class RadixRequestsPage extends LitElement { type="${requestItem.type}" status="${requestItem.status}" id="${requestItem.interactionId}" - transactionIntentHash="${requestItem.transactionIntentHash || ''}" + hash="${requestItem.transactionIntentHash || ''}" ?showCancel="${requestItem.showCancel}" timestamp=${requestItem.createdAt} >`, diff --git a/packages/connect-button/src/components/pages/sharing.ts b/packages/connect-button/src/components/pages/sharing.ts index 0b8ad186..ab8c5716 100644 --- a/packages/connect-button/src/components/pages/sharing.ts +++ b/packages/connect-button/src/components/pages/sharing.ts @@ -81,7 +81,7 @@ export class RadixSharingPage extends LitElement { @@ -89,7 +89,7 @@ export class RadixSharingPage extends LitElement { class=${classMap({ icon: true, 'update-data': true, - disabled: this.accounts.length === 0, + disabled: this.accounts?.length === 0, })} >
Update Account Sharing diff --git a/packages/connect-button/src/stories/blur-header.stories.ts b/packages/connect-button/src/stories/blur-header.stories.ts new file mode 100644 index 00000000..c14d294d --- /dev/null +++ b/packages/connect-button/src/stories/blur-header.stories.ts @@ -0,0 +1,264 @@ +import { StoryObj, Meta } from '@storybook/web-components' +import { html } from 'lit-html' + +export default { + title: 'Radix/Header with blur', +} as Meta + +export const Example: StoryObj = { + render: () => html` + +
+ +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sit amet + ante bibendum mauris porttitor vehicula non varius quam. Integer aliquam + nibh in condimentum eleifend. Ut quis suscipit dui. Quisque vitae varius + nisi, porttitor dapibus mauris. Integer porta turpis in egestas + vestibulum. Aliquam pellentesque massa neque, et interdum augue facilisis + nec. Suspendisse accumsan non sem vel ultrices. Integer sodales tincidunt + ex sed mollis. Proin sit amet magna ut ipsum efficitur placerat. Quisque + justo purus, lacinia efficitur lacus nec, ornare vulputate purus. + Suspendisse vel aliquet mauris. Aliquam auctor ipsum nisl, in commodo + velit aliquet vitae. Aliquam sodales, leo ut laoreet porta, justo lectus + facilisis orci, vel efficitur nisl leo quis tortor. Donec vitae fermentum + mi. Fusce congue tincidunt sagittis. Nunc posuere posuere mauris at + lacinia. Pellentesque quam magna, pulvinar eget vestibulum eget, luctus a + felis. Nulla facilisi. Orci varius natoque penatibus et magnis dis + parturient montes, nascetur ridiculus mus. Curabitur auctor egestas + auctor. Orci varius natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. Donec eu interdum diam. Ut mattis diam id risus + molestie viverra. Praesent vehicula massa eu turpis rutrum bibendum. In + euismod vulputate mi. Duis non tempus eros. Quisque ut efficitur dui. + Maecenas molestie auctor tincidunt. Sed finibus eu lacus commodo dapibus. + Aliquam tincidunt mauris nibh, eget laoreet orci lacinia nec. Duis tempor + neque sed orci maximus, at rhoncus mi tempus. Mauris ante arcu, dapibus at + tellus non, accumsan facilisis nunc. Nullam ac convallis ex. Nam vitae + diam volutpat, fermentum augue sed, fermentum mi. Mauris vestibulum + accumsan turpis, ac tempor mauris hendrerit ut. Quisque hendrerit feugiat + enim sit amet blandit. Sed efficitur ultrices quam viverra accumsan. Donec + vehicula hendrerit purus at laoreet. Cras et ultrices justo, sed hendrerit + tellus. Ut efficitur dolor nec magna tincidunt mollis eu eget nibh. Mauris + sit amet interdum mauris, quis fringilla ex. Duis augue enim, gravida ac + lectus fermentum, semper egestas magna. Sed at metus non magna tempus + suscipit nec et massa. Pellentesque ut sem ut nunc gravida vehicula sit + amet at felis. Suspendisse suscipit pulvinar ipsum, non eleifend velit + ultrices eu. Maecenas vitae semper quam. Suspendisse molestie pulvinar + lorem eu iaculis. Sed ut ligula nisl. Pellentesque habitant morbi + tristique senectus et netus et malesuada fames ac turpis egestas. Vivamus + in rhoncus sem. Etiam dignissim ex non efficitur ultricies. Vestibulum + ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; In finibus, orci et feugiat maximus, sem massa finibus nisi, ut + viverra risus lacus vel est. Vestibulum semper blandit aliquam. Sed a orci + dolor. Vivamus consequat eros urna, vel volutpat erat egestas quis. + Integer accumsan lorem sit amet tristique maximus. Aenean pharetra commodo + dolor. Suspendisse dapibus nibh eget volutpat tempus. Interdum et + malesuada fames ac ante ipsum primis in faucibus. In feugiat scelerisque + risus pharetra vestibulum. Praesent suscipit eget velit id laoreet. Morbi + quis arcu gravida augue placerat pharetra vitae id dui. Nunc congue tempus + eleifend. Ut sed vulputate sem. Vivamus dignissim est ut turpis consequat + dignissim. Aenean eget facilisis dui, vitae rutrum eros. Nullam dignissim + non ipsum a aliquet. Etiam dolor augue, sodales ac magna in, sagittis + tempus leo. Maecenas eu sapien bibendum, consectetur nibh quis, ornare + felis. Quisque ornare luctus lacus sed condimentum. Praesent lorem ex, + efficitur sed orci eget, tincidunt egestas mi. Vivamus blandit sapien sit + amet nunc consectetur feugiat. Cras tempor sagittis est, vitae mollis + libero auctor at. Integer eget enim mollis, elementum arcu a, maximus + diam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec + mi lectus, suscipit non dolor commodo, tempor blandit nunc. Interdum et + malesuada fames ac ante ipsum primis in faucibus. Nulla pellentesque + elementum risus scelerisque posuere. Nullam gravida lectus eget eros + molestie bibendum. Suspendisse at elit porttitor, commodo tellus et, + pharetra nisl. Ut fringilla, massa vel sagittis consectetur, tortor purus + ornare risus, in molestie sem nibh in eros. Phasellus ac odio vel tellus + posuere volutpat eu vitae augue. Maecenas finibus ultricies odio, sed + gravida urna vulputate feugiat. Donec nulla quam, tempor vitae bibendum + sed, ultricies ut magna. Sed varius condimentum metus eu ultricies. + Phasellus lacus justo, faucibus ac efficitur a, lobortis suscipit odio. + Sed sed blandit ipsum, ut pellentesque mauris. Duis viverra vehicula + fringilla. Mauris nec tempus nulla. Nulla at tortor at est malesuada + pharetra non ac neque. Integer vel scelerisque leo. Nam non nibh id nibh + placerat semper ullamcorper et justo. Sed elementum euismod interdum. Cras + a auctor purus. Etiam rhoncus pulvinar metus, sit amet mollis justo + suscipit sed. Donec id tristique ante. Ut ut augue cursus, finibus nisi + vel, egestas lectus. Donec eu dolor in justo hendrerit porttitor nec quis + felis. Vestibulum ut fringilla arcu. Donec bibendum ex nec viverra + imperdiet. Pellentesque sed elementum lorem. Integer rutrum neque diam, id + vehicula sapien malesuada at. Morbi efficitur elit vel tempor congue. Nam + a pulvinar odio. Sed nec lobortis diam. Proin fermentum lorem ligula, non + sodales urna tempor vitae. Phasellus nec tincidunt nibh, ac vehicula + purus. Mauris quis dolor ut ante egestas volutpat in ut libero. Integer eu + nibh convallis nulla sodales mattis quis sit amet purus. Duis sed metus + augue. Nam ac quam dignissim, tincidunt mauris feugiat, porttitor risus. + Maecenas quis urna quis magna vulputate porta. Praesent a erat nisi. Donec + pharetra imperdiet tellus, non lobortis nisi tincidunt et. Pellentesque + laoreet, arcu in fermentum egestas, metus tellus feugiat erat, non pretium + ex nibh in dolor. Sed in bibendum diam. Phasellus ante neque, dapibus a + nunc eget, commodo laoreet tortor. Nullam aliquam vitae nisi eu tincidunt. + Pellentesque tristique magna sed nulla rutrum porta. Donec tellus nulla, + auctor et hendrerit posuere, consectetur vel turpis. Phasellus ac interdum + diam. Suspendisse ullamcorper metus eu lacus varius euismod. Suspendisse + est magna, sagittis ac erat quis, interdum interdum tortor. Maecenas + sollicitudin est id condimentum eleifend. Suspendisse potenti. Vivamus + hendrerit nulla quis viverra molestie. Suspendisse ut auctor tortor. + Aliquam sed risus arcu. Donec pulvinar massa id felis malesuada fermentum. + Ut eget diam nec ipsum mollis condimentum. Duis posuere in lectus non + commodo. Curabitur tincidunt tristique scelerisque. Quisque porta diam + risus, vitae blandit purus posuere id. Sed venenatis nisl ac risus laoreet + ornare. Mauris vel justo dignissim, tristique nibh nec, fermentum felis. + Suspendisse egestas massa congue metus mollis, eget venenatis urna + eleifend. Fusce velit lorem, varius ac lectus porta, bibendum egestas + tellus. Donec pellentesque justo sed fringilla finibus. Donec bibendum id + diam sit amet facilisis. Etiam eu diam id libero ultrices ornare non eu + enim. Vivamus et viverra urna. Etiam a tincidunt elit, a lobortis nisl. Ut + consectetur tempor tellus sed egestas. Proin elementum quam ac elit +
+ `, +} + +export const Example2: StoryObj = { + render: () => html` + +
+
+
+ +
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sit amet + ante bibendum mauris porttitor vehicula non varius quam. Integer aliquam + nibh in condimentum eleifend. Ut quis suscipit dui. Quisque vitae varius + nisi, porttitor dapibus mauris. Integer porta turpis in egestas + vestibulum. Aliquam pellentesque massa neque, et interdum augue facilisis + nec. Suspendisse accumsan non sem vel ultrices. Integer sodales tincidunt + ex sed mollis. Proin sit amet magna ut ipsum efficitur placerat. Quisque + justo purus, lacinia efficitur lacus nec, ornare vulputate purus. + Suspendisse vel aliquet mauris. Aliquam auctor ipsum nisl, in commodo + velit aliquet vitae. Aliquam sodales, leo ut laoreet porta, justo lectus + facilisis orci, vel efficitur nisl leo quis tortor. Donec vitae fermentum + mi. Fusce congue tincidunt sagittis. Nunc posuere posuere mauris at + lacinia. Pellentesque quam magna, pulvinar eget vestibulum eget, luctus a + felis. Nulla facilisi. Orci varius natoque penatibus et magnis dis + parturient montes, nascetur ridiculus mus. Curabitur auctor egestas + auctor. Orci varius natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. Donec eu interdum diam. Ut mattis diam id risus + molestie viverra. Praesent vehicula massa eu turpis rutrum bibendum. In + euismod vulputate mi. Duis non tempus eros. Quisque ut efficitur dui. + Maecenas molestie auctor tincidunt. Sed finibus eu lacus commodo dapibus. + Aliquam tincidunt mauris nibh, eget laoreet orci lacinia nec. Duis tempor + neque sed orci maximus, at rhoncus mi tempus. Mauris ante arcu, dapibus at + tellus non, accumsan facilisis nunc. Nullam ac convallis ex. Nam vitae + diam volutpat, fermentum augue sed, fermentum mi. Mauris vestibulum + accumsan turpis, ac tempor mauris hendrerit ut. Quisque hendrerit feugiat + enim sit amet blandit. Sed efficitur ultrices quam viverra accumsan. Donec + vehicula hendrerit purus at laoreet. Cras et ultrices justo, sed hendrerit + tellus. Ut efficitur dolor nec magna tincidunt mollis eu eget nibh. Mauris + sit amet interdum mauris, quis fringilla ex. Duis augue enim, gravida ac + lectus fermentum, semper egestas magna. Sed at metus non magna tempus + suscipit nec et massa. Pellentesque ut sem ut nunc gravida vehicula sit + amet at felis. Suspendisse suscipit pulvinar ipsum, non eleifend velit + ultrices eu. Maecenas vitae semper quam. Suspendisse molestie pulvinar + lorem eu iaculis. Sed ut ligula nisl. Pellentesque habitant morbi + tristique senectus et netus et malesuada fames ac turpis egestas. Vivamus + in rhoncus sem. Etiam dignissim ex non efficitur ultricies. Vestibulum + ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; In finibus, orci et feugiat maximus, sem massa finibus nisi, ut + viverra risus lacus vel est. Vestibulum semper blandit aliquam. Sed a orci + dolor. Vivamus consequat eros urna, vel volutpat erat egestas quis. + Integer accumsan lorem sit amet tristique maximus. Aenean pharetra commodo + dolor. Suspendisse dapibus nibh eget volutpat tempus. Interdum et + malesuada fames ac ante ipsum primis in faucibus. In feugiat scelerisque + risus pharetra vestibulum. Praesent suscipit eget velit id laoreet. Morbi + quis arcu gravida augue placerat pharetra vitae id dui. Nunc congue tempus + eleifend. Ut sed vulputate sem. Vivamus dignissim est ut turpis consequat + dignissim. Aenean eget facilisis dui, vitae rutrum eros. Nullam dignissim + non ipsum a aliquet. Etiam dolor augue, sodales ac magna in, sagittis + tempus leo. Maecenas eu sapien bibendum, consectetur nibh quis, ornare + felis. Quisque ornare luctus lacus sed condimentum. Praesent lorem ex, + efficitur sed orci eget, tincidunt egestas mi. Vivamus blandit sapien sit + amet nunc consectetur feugiat. Cras tempor sagittis est, vitae mollis + libero auctor at. Integer eget enim mollis, elementum arcu a, maximus + diam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec + mi lectus, suscipit non dolor commodo, tempor blandit nunc. Interdum et + malesuada fames ac ante ipsum primis in faucibus. Nulla pellentesque + elementum risus scelerisque posuere. Nullam gravida lectus eget eros + molestie bibendum. Suspendisse at elit porttitor, commodo tellus et, + pharetra nisl. Ut fringilla, massa vel sagittis consectetur, tortor purus + ornare risus, in molestie sem nibh in eros. Phasellus ac odio vel tellus + posuere volutpat eu vitae augue. Maecenas finibus ultricies odio, sed + gravida urna vulputate feugiat. Donec nulla quam, tempor vitae bibendum + sed, ultricies ut magna. Sed varius condimentum metus eu ultricies. + Phasellus lacus justo, faucibus ac efficitur a, lobortis suscipit odio. + Sed sed blandit ipsum, ut pellentesque mauris. Duis viverra vehicula + fringilla. Mauris nec tempus nulla. Nulla at tortor at est malesuada + pharetra non ac neque. Integer vel scelerisque leo. Nam non nibh id nibh + placerat semper ullamcorper et justo. Sed elementum euismod interdum. Cras + a auctor purus. Etiam rhoncus pulvinar metus, sit amet mollis justo + suscipit sed. Donec id tristique ante. Ut ut augue cursus, finibus nisi + vel, egestas lectus. Donec eu dolor in justo hendrerit porttitor nec quis + felis. Vestibulum ut fringilla arcu. Donec bibendum ex nec viverra + imperdiet. Pellentesque sed elementum lorem. Integer rutrum neque diam, id + vehicula sapien malesuada at. Morbi efficitur elit vel tempor congue. Nam + a pulvinar odio. Sed nec lobortis diam. Proin fermentum lorem ligula, non + sodales urna tempor vitae. Phasellus nec tincidunt nibh, ac vehicula + purus. Mauris quis dolor ut ante egestas volutpat in ut libero. Integer eu + nibh convallis nulla sodales mattis quis sit amet purus. Duis sed metus + augue. Nam ac quam dignissim, tincidunt mauris feugiat, porttitor risus. + Maecenas quis urna quis magna vulputate porta. Praesent a erat nisi. Donec + pharetra imperdiet tellus, non lobortis nisi tincidunt et. Pellentesque + laoreet, arcu in fermentum egestas, metus tellus feugiat erat, non pretium + ex nibh in dolor. Sed in bibendum diam. Phasellus ante neque, dapibus a + nunc eget, commodo laoreet tortor. Nullam aliquam vitae nisi eu tincidunt. + Pellentesque tristique magna sed nulla rutrum porta. Donec tellus nulla, + auctor et hendrerit posuere, consectetur vel turpis. Phasellus ac interdum + diam. Suspendisse ullamcorper metus eu lacus varius euismod. Suspendisse + est magna, sagittis ac erat quis, interdum interdum tortor. Maecenas + sollicitudin est id condimentum eleifend. Suspendisse potenti. Vivamus + hendrerit nulla quis viverra molestie. Suspendisse ut auctor tortor. + Aliquam sed risus arcu. Donec pulvinar massa id felis malesuada fermentum. + Ut eget diam nec ipsum mollis condimentum. Duis posuere in lectus non + commodo. Curabitur tincidunt tristique scelerisque. Quisque porta diam + risus, vitae blandit purus posuere id. Sed venenatis nisl ac risus laoreet + ornare. Mauris vel justo dignissim, tristique nibh nec, fermentum felis. + Suspendisse egestas massa congue metus mollis, eget venenatis urna + eleifend. Fusce velit lorem, varius ac lectus porta, bibendum egestas + tellus. Donec pellentesque justo sed fringilla finibus. Donec bibendum id + diam sit amet facilisis. Etiam eu diam id libero ultrices ornare non eu + enim. Vivamus et viverra urna. Etiam a tincidunt elit, a lobortis nisl. Ut + consectetur tempor tellus sed egestas. Proin elementum quam ac elit +
+ `, +} diff --git a/packages/connect-button/src/components/connect-button.stories.css b/packages/connect-button/src/stories/connect-button.stories.css similarity index 100% rename from packages/connect-button/src/components/connect-button.stories.css rename to packages/connect-button/src/stories/connect-button.stories.css diff --git a/packages/connect-button/src/components/connect-button.stories.ts b/packages/connect-button/src/stories/connect-button.stories.ts similarity index 99% rename from packages/connect-button/src/components/connect-button.stories.ts rename to packages/connect-button/src/stories/connect-button.stories.ts index 5f83b236..ba8186b6 100644 --- a/packages/connect-button/src/components/connect-button.stories.ts +++ b/packages/connect-button/src/stories/connect-button.stories.ts @@ -6,8 +6,8 @@ import { RadixButtonStatus, RequestItem, } from 'radix-connect-common' -import './connect-button' -import { ConnectButton } from './connect-button' +import '../components/connect-button' +import { ConnectButton } from '../components/connect-button' import './connect-button.stories.css' import { BUTTON_MIN_WIDTH } from '../constants' diff --git a/packages/dapp-toolkit/.releaserc b/packages/dapp-toolkit/.releaserc index d0ef9b29..2ec1f5df 100644 --- a/packages/dapp-toolkit/.releaserc +++ b/packages/dapp-toolkit/.releaserc @@ -1,27 +1,105 @@ { - "branches": [ - "main", - "next", + "branches": [ + "main", + "next", + { + "name": "develop", + "channel": "dev", + "prerelease": "dev" + }, + { + "name": "release/([a-z0-9-.]+)", + "channel": "${name.replace(/^release\\//g, \"\")}", + "prerelease": "${name.replace(/^release\\//g, \"\")}" + } + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", { - "name": "develop", - "channel": "dev", - "prerelease": "dev" - }, - { - "name": "release/([a-z0-9-.]+)", - "channel": "${name.replace(/^release\\//g, \"\")}", - "prerelease": "${name.replace(/^release\\//g, \"\")}" + "preset": "conventionalcommits", + "releaseRules": [ + { + "type": "refactor", + "release": "patch" + }, + { + "type": "major", + "release": "major" + }, + { + "type": "docs", + "scope": "README", + "release": "patch" + }, + { + "type": "test", + "release": false + }, + { + "type": "style", + "release": "patch" + }, + { + "type": "perf", + "release": "patch" + }, + { + "type": "ci", + "release": false + }, + { + "type": "build", + "release": false + }, + { + "type": "chore", + "release": "patch" + }, + { + "type": "code", + "release": "patch" + }, + { + "type": "no-release", + "release": false + } + ], + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES" + ] + } } ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES" + ] + }, + "writerOpts": { + "commitsSort": [ + "subject", + "scope" + ] + }, + "presetConfig": { + "types": [ { - "type": "refactor", - "release": "patch" + "type": "feat", + "section": ":sparkles: Features", + "hidden": false + }, + { + "type": "fix", + "section": ":bug: Fixes", + "hidden": false }, { "type": "major", @@ -29,158 +107,84 @@ }, { "type": "docs", - "scope": "README", - "release": "patch" + "section": ":memo: Documentation", + "hidden": false }, { - "type": "test", - "release": false + "type": "style", + "section": ":barber: Code-style", + "hidden": false }, { - "type": "style", - "release": "patch" + "type": "refactor", + "section": ":zap: Refactor", + "hidden": false }, { "type": "perf", - "release": "patch" + "section": ":fast_forward: Performance", + "hidden": false }, { - "type": "ci", - "release": false + "type": "test", + "section": ":white_check_mark: Tests", + "hidden": false }, { - "type": "build", - "release": false + "type": "ci", + "section": ":repeat: CI", + "hidden": false }, { "type": "chore", - "release": "patch" + "section": ":repeat: Chore", + "hidden": false }, { - "type": "no-release", - "release": false - } - ], - "parserOpts": { - "noteKeywords": [ - "BREAKING CHANGE", - "BREAKING CHANGES" - ] - } - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "parserOpts": { - "noteKeywords": [ - "BREAKING CHANGE", - "BREAKING CHANGES" - ] - }, - "writerOpts": { - "commitsSort": [ - "subject", - "scope" - ] - }, - "presetConfig": { - "types": [ - { - "type": "feat", - "section": ":sparkles: Features", - "hidden": false - }, - { - "type": "fix", - "section": ":bug: Fixes", - "hidden": false - }, - { - "type": "major", - "release": "major" - }, - { - "type": "docs", - "section": ":memo: Documentation", - "hidden": false - }, - { - "type": "style", - "section": ":barber: Code-style", - "hidden": false - }, - { - "type": "refactor", - "section": ":zap: Refactor", - "hidden": false - }, - { - "type": "perf", - "section": ":fast_forward: Performance", - "hidden": false - }, - { - "type": "test", - "section": ":white_check_mark: Tests", - "hidden": false - }, - { - "type": "ci", - "section": ":repeat: CI", - "hidden": false - }, - { - "type": "chore", - "section": ":repeat: Chore", - "hidden": false - }, - { - "type": "build", - "section": ":wrench: Build", - "hidden": false - } - ] - } - } - ], - [ - "semantic-release-replace-plugin", - { - "replacements": [ - { - "files": [ - "src/version.ts" - ], - "from": "export const __VERSION__ = '2.0.0'", - "to": "export const __VERSION__ = '${nextRelease.version}'", - "countMatches": true + "type": "build", + "section": ":wrench: Build", + "hidden": false } ] } - ], - [ - "@semantic-release/exec", - { - "prepareCmd": "rm -rf dist && npm run build" - } - ], - "@semantic-release/npm", - "@semantic-release/github", - [ - "@saithodev/semantic-release-backmerge", - { - "backmergeBranches": [ - { - "from": "main", - "to": "develop" - } - ], - "backmergeStrategy": "merge", - "clearWorkspace": true, - "fastForwardMode": "ff" - } - ] + } + ], + [ + "semantic-release-replace-plugin", + { + "replacements": [ + { + "files": [ + "src/version.ts" + ], + "from": "export const __VERSION__ = '2.0.0'", + "to": "export const __VERSION__ = '${nextRelease.version}'", + "countMatches": true + } + ] + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "rm -rf dist && npm run build" + } + ], + "@semantic-release/npm", + "@semantic-release/github", + [ + "@saithodev/semantic-release-backmerge", + { + "backmergeBranches": [ + { + "from": "main", + "to": "develop" + } + ], + "backmergeStrategy": "merge", + "clearWorkspace": true, + "fastForwardMode": "ff" + } ] - } \ No newline at end of file + ] +} diff --git a/packages/dapp-toolkit/README.md b/packages/dapp-toolkit/README.md index 6e529825..8e2e0974 100644 --- a/packages/dapp-toolkit/README.md +++ b/packages/dapp-toolkit/README.md @@ -6,7 +6,7 @@ RDT supports both desktop and mobile browser web apps. For desktop browsers, it **RDT is composed of:** -- **√ Connect Button** – A framework agnostic web component that keeps a minimal internal state and have properties are pushed to it. +- **√ Connect Button** – A framework agnostic [custom element](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) that serves as user interface for RDT ([readme](../connect-button/README.md)) - **Tools** – Abstractions over lower level APIs for developers to build their radix dApps at lightning speed. @@ -34,7 +34,7 @@ Add following code to head section of your page. See example usage inside `examp ## Using `create-radix-dapp` -Use our [CLI tool](https://github.com/radixdlt/create-radix-dapp) to scaffold a new project. Just paste following command into your terminal and it will walk you through all required steps! +Use our [CLI tool](https://github.com/radixdlt/create-radix-dapp) to scaffold a new project. Just paste following command into your terminal and it will walk you through all required steps. ```bash npx create-radix-dapp@latest @@ -69,6 +69,9 @@ const rdt = RadixDappToolkit({ - **requires** networkId - Target radix network ID (for development use `RadixNetwork.Stokenet`). - _optional_ applicationName - Your dApp name. It's only used for statistics purposes on gateway side - _optional_ applicationVersion - Your dApp version. It's only used for statistics purposes on gateway side +- _optional_ logger - Configure and provide `Logger` instance if you want to deep dive into what's happening in RDT + +There are more configuration options which are not described here. Please look up [`OptionalRadixDappToolkitOptions`](https://github.com/radixdlt/radix-dapp-toolkit/blob/c65fa2ad016b22e3b5a5410a0a1adc24bbee86fe/packages/dapp-toolkit/src/_types.ts#L48) to learn more. ## Login requests @@ -120,7 +123,7 @@ In order to request a persona or account with proof of ownership a challenge is A challenge is a random 32 bytes hex encoded string that looks something like: `4ccb0555d6b4faad0d7f5ed40bf4e4f0665c8ba35929c638e232e09775d0fa0e` -If you're using JS for your backend you can use `generateRolaChallenge` function from Radix dApp Toolkit which will generate valid ROLA challenge for you. +If you're using JS for your backend you can use `generateRolaChallenge` function from Radix dApp Toolkit which will generate valid ROLA challenge for you. **Why do we need a challenge?** @@ -131,20 +134,11 @@ The challenge plays an important role in the authentication flow, namely prevent In order to request a proof, it is required to provide a function to RDT that produces a challenge. ```typescript -// const requestChallengeFromDappBackendFn = (): Promise => +// const requestChallengeFromDappBackendFn = (): Promise => // http.get('/api/auth/challenge') rdt.walletApi.provideChallengeGenerator(requestChallengeFromDappBackendFn) - rdt.walletApi.setRequestData(DataRequestBuilder.persona.withProof()) - -// handle the wallet response -rdt.walletApi.dataRequestControl(async (walletData) => { - const personaProof = walletData.proofs.find( - (proof) => proof.type === 'persona', - ) - if (personaProof) await handleLogin(personaProof) -}) ``` ### Handle user authentication @@ -172,6 +166,30 @@ rdt.walletApi.dataRequestControl(async (walletData) => { See [ROLA example](https://github.com/radixdlt/rola-examples) for an end-to-end implementation. +### Authenticate specific account or persona + +Sometimes you want to restrict access to some parts of the system. For example you have admin part of your dApp which only people with specific identities can access. On the other hand, you don't want every user to go through ROLA process every time they login. Here's where ["one-time proof of ownership"](#one-time-data-request) request comes handy. Radix dApp Toolkit gives you you a way to ask Radix Wallet about **specific account addresses and identity**. + +**Example:** + +```typescript + // const verifyProofInBackend = (proof: SignedChallenge): ResultAsync => { ... } + + rdt.walletApi.sendOneTimeRequest( + OneTimeDataRequestBuilder.proofOfOwnership().identity( + 'identity_tdx_2_12g3f29r62450l03ejucc2cf0pz52uawkwwm4um3chqxjjl2ffhq6f8', + ), + ).andThen((response) => { + const proof = response.proofs.find((proof) => proof.address ==='identity_tdx_2_12g3f29r62450l03ejucc2cf0pz52uawkwwm4um3chqxjjl2ffhq6f8') + return verifyProofInBackend(proof) + }) +``` + +> [!IMPORTANT] +> If you want to use that, you need to configure challenge generator with `provideChallengeGenerator` + + + ### User authentication management After a successful ROLA verification it is up to the dApp's business logic to handle user authentication session in order to keep the user logged-in between requests. Although RDT is persisting state between page reloads, it is not aware of user authentication. The dApp logic needs to control the login state and sign out a user when needed. @@ -317,6 +335,24 @@ rdt.walletApi.sendOneTimeRequest( ) ``` +#### `OneTimeDataRequestBuilderItem.proofOfOwnership()` + +```typescript +accounts: (value: string[]) => ProofOfOwnershipRequestBuilder +identity: (value: string) => ProofOfOwnershipRequestBuilder +``` + +Example: Prove that user who is trying access admin page right is owner of given identity + +```typescript +// const currentUserState = { .... } +rdt.walletApi.sendOneTimeRequest( + OneTimeDataRequestBuilder.proofOfOwnership().identity( + currentUserState.identity + ), +) +``` + #### `DataRequestBuilder.config(input: DataRequestState)` Use this method if you prefer to provide a raw data request object. @@ -346,7 +382,7 @@ rdt.walletApi.provideConnectResponseCallback((result) => { ### One Time Data Request -One-time data requests do not have a Persona context, and so will always result in the Radix Wallet asking the user to select where to draw personal data from. The wallet response from a one time data request is meant to be discarded after usage. A typical use case would be to populate a web-form with user data. +Sometimes you want to get some data from the Radix Wallet based on various user actions like custom button click, page event, route change etc. One-time data requests are perfect way of doing that. One time data requests neither need any "auth" context nor they keep any state. The wallet response from a one time data request is meant to be discarded after usage. A typical use case would be to populate a web-form with user data, choose account, prove identity etc. ```typescript const result = rdt.walletApi.sendOneTimeRequest( @@ -413,7 +449,9 @@ Radix transactions are built using "transaction manifests", that use a simple sy It is important to note that what your dApp sends to the Radix Wallet is actually a "transaction manifest stub". It is completed before submission by the Radix Wallet. For example, the Radix Wallet will automatically add a command to lock the necessary amount of network fees from one of the user's accounts. It may also add "assert" commands to the manifest according to user desires for expected returns. -**NOTE:** Information will be provided soon on a ["comforming" transaction manifest stub format](https://docs.radixdlt.com/docs/conforming-transaction-manifest-types) that ensures clear presentation and handling in the Radix Wallet. +> [!NOTE] +> Some of the manifests will have a nice presentation in the Radix Wallet, others will be displayed as raw text. Read more on ["comforming" transaction manifest stub format](https://docs.radixdlt.com/docs/conforming-transaction-manifest-types). + ### Build transaction manifest @@ -421,7 +459,7 @@ We recommend using template strings for constructing simpler transaction manifes ### sendTransaction -This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed. +This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed. `sendTransaction` promise will only be resolved after transaction has been committed to the network (either successfuly or rejected/failure). If you want to do your own logic as soon as transaction id is available, please use `onTransactionId` callback. It will be called immediately after RDT receives response from the Radix Wallet. ```typescript type SendTransactionInput = { @@ -457,6 +495,28 @@ const transactionIntentHash = result.value.transactionIntentHash +## Preauthorization Requests + +It is very similar to a transaction request, but it describes only a part of a final transaction – specifically the part that the user cares about, such as a swap they wish to perform within certain acceptable bounds. The pre-authorization is signed and returned to the dApp, which can then include it in a full transaction. A time bound is put on the pre-authorization, so the user knows for how long their pre-authorization is usable. + +Creation of preauthorization request object is abstracted away into `SubintentRequestBuilder`. You can set exipration date in two modes: +- delay in **seconds after preauthorization is signed** by using `.setExpiration('afterDelay', 3600)` +- provided **exact unix timestamp** to function call `.setExpiration('atTime', 1234567890)` + +**Example:** +```typescript + const result = await dAppToolkit.walletApi.sendPreAuthorizationRequest( + SubintentRequestBuilder() + .manifest(subintentManifest) + .setExpiration( + 'afterDelay', + 3600, + ) + // .addBlobs('blob1', 'blob2') + .message('This is a message') + ) +``` + # √ Connect Button Radix dApp Toolkit provides a consistent and delightful user experience between radix dApps thanks to `` [custom element](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements). Although complex by itself, RDT is off-loading the developer burden of having to handle the logic of all its internal states. @@ -480,12 +540,21 @@ Play around with the different configurations using the [√ Connect Button stor Connect Button Themes
-There are four themes you can choose from by default: `radix-blue` (default), `black`, `white-with-outline`, `white`. In order to do that, call following function after RDT instantiation +There are four themes you can choose from by default: `radix-blue` (default), `black`, `white-with-outline`, `white` and a special one called `custom`. In order to do that, call following function after RDT instantiation ```typescript rdt.buttonApi.setTheme('black') ``` +Using `custom` theme will let you override additional CSS variables. With that you can completely change the UI for not connected connect button. + +```css +body { + --radix-connect-button-background: red; + --radix-connect-button-text-color: black; + --radix-connect-button-border-color: yellow; + --radix-connect-button-background-hover: green; +} ### Modes
diff --git a/packages/dapp-toolkit/package.json b/packages/dapp-toolkit/package.json index 0950c18d..735e78e2 100644 --- a/packages/dapp-toolkit/package.json +++ b/packages/dapp-toolkit/package.json @@ -55,37 +55,36 @@ "scripts": { "dev": "tsup --watch", "build": "tsup && npm run build:single", - "build:single": "vite build --config vite-single-file.config.ts && cp dist/radix-dapp-toolkit.bundle.umd.cjs ../../examples/cdn", - "test": "vitest", + "build:single": "vite build --config vite-single-file.config.ts && cp dist/radix-dapp-toolkit.bundle.umd.js ../../examples/cdn", + "test": "vitest run --coverage", "test:watch": "vitest --watch" }, "dependencies": { "@noble/curves": "^1.4.0", "base64url": "^3.0.1", "blakejs": "^1.2.1", - "bowser": "^2.11.0", "buffer": "^6.0.3", "immer": "^10.0.4", "lit": "^3.1.2", "lit-html": "^3.1.2", - "neverthrow": "^6.1.0", + "neverthrow": "^8.0.0", "rxjs": "^7.8.1", "tslog": ">=4.8.0", "uuid": "^10.0.0", - "valibot": "0.30.0" + "valibot": "0.42.1" }, "devDependencies": { "@radixdlt/connect-button": "*", "@saithodev/semantic-release-backmerge": "^3.2.1", "@semantic-release/exec": "^6.0.3", + "@vitest/coverage-v8": "^2.1.3", "radix-connect-common": "*", "semantic-release": "^23.0.0", "semantic-release-replace-plugin": "^1.2.7", - "tsup": "^8.0.2", + "tsup": "^8.3.0", "typescript": "^5.4.4", "vite-plugin-singlefile": "^2.0.2", - "vitest": "^1.4.0", - "vitest-mock-extended": "^1.3.1" + "vitest": "^2.1.3" }, "repository": { "type": "git", @@ -95,4 +94,4 @@ "publishConfig": { "registry": "https://registry.npmjs.org" } -} \ No newline at end of file +} diff --git a/packages/dapp-toolkit/src/_types.ts b/packages/dapp-toolkit/src/_types.ts index fd316f43..1450ebd4 100644 --- a/packages/dapp-toolkit/src/_types.ts +++ b/packages/dapp-toolkit/src/_types.ts @@ -7,6 +7,7 @@ import { Persona, PersonaDataName, WalletInteraction, + WalletInteractionResponse, } from './schemas' import type { Logger } from './helpers' import type { SdkError } from './error' @@ -23,23 +24,29 @@ import type { GatewayModule, WalletRequestModule, ConnectButtonModule, + EnvironmentModule, } from './modules' +import { BuildableSubintentRequest } from './modules/wallet-request/pre-authorization-request/subintent-builder' export type Providers = { connectButtonModule: ConnectButtonModule gatewayModule: GatewayModule stateModule: StateModule storageModule: StorageModule + environmentModule: EnvironmentModule walletRequestModule: WalletRequestModule } export type ExplorerConfig = { baseUrl: string transactionPath: string + subintentPath: string accountsPath: string } -export type WalletDataRequest = Parameters[0] +export type WalletDataRequest = Parameters< + WalletRequestSdk['sendInteraction'] +>[0] export type WalletRequest = | { type: 'sendTransaction'; payload: WalletInteraction } @@ -96,6 +103,8 @@ export type SendTransactionInput = { onTransactionId?: (transactionId: string) => void } +export type SendPreAuthorizationRequestInput = BuildableSubintentRequest + export type ButtonApi = { setMode: (value: 'light' | 'dark') => void setTheme: (value: RadixButtonTheme) => void @@ -128,6 +137,9 @@ export type WalletApi = { dataRequestControl: (fn: (walletResponse: WalletData) => Promise) => void updateSharedAccounts: () => WalletDataRequestResult sendTransaction: (input: SendTransactionInput) => SendTransactionResult + sendPreAuthorizationRequest: ( + input: SendPreAuthorizationRequestInput, + ) => ResultAsync<{ signedPartialTransaction: string }, SdkError> setRequestData: (...dataRequestBuilderItem: DataRequestBuilderItem[]) => void sendRequest: () => WalletDataRequestResult sendOneTimeRequest: ( @@ -166,7 +178,7 @@ export type TransportProvider = { send: ( walletInteraction: WalletInteraction, callbackFns: Partial, - ) => ResultAsync + ) => ResultAsync disconnect: () => void destroy: () => void } diff --git a/packages/dapp-toolkit/src/helpers/exponential-backoff.spec.ts b/packages/dapp-toolkit/src/helpers/exponential-backoff.spec.ts new file mode 100644 index 00000000..4033ca5b --- /dev/null +++ b/packages/dapp-toolkit/src/helpers/exponential-backoff.spec.ts @@ -0,0 +1,79 @@ +import { afterAll, describe, expect, it, vi } from 'vitest' +import { ExponentialBackoff } from './exponential-backoff' +import { delayAsync } from '../test-helpers/delay-async' +import { Subscription } from 'rxjs' + +describe('exponential backoff', () => { + const subscription = new Subscription() + + it('should emit withBackoff$ observable', async () => { + const backoff = ExponentialBackoff({ + maxDelayTime: 2000, + multiplier: 2, + interval: 1000, + }) + + const spy = vi.fn() + + subscription.add( + backoff.withBackoff$.subscribe(() => { + spy() + backoff.trigger.next() + }), + ) + + await delayAsync(4500) + + expect(spy).toHaveBeenCalledTimes(3) + }) + + it('should emit error after timeout', async () => { + const backoff = ExponentialBackoff({ + maxDelayTime: 2000, + multiplier: 2, + interval: 2000, + timeout: new Date(Date.now() + 1000), + }) + const spy = vi.fn() + + subscription.add( + backoff.withBackoff$.subscribe((res) => { + spy(res.isOk() ? res.value : res.error) + + backoff.trigger.next() + }), + ) + + await delayAsync(2000) + + expect(spy).toHaveBeenCalledWith(0) + expect(spy).toHaveBeenCalledWith({ + error: 'timeout', + }) + }) + + it('should emit error after stop', async () => { + const backoff = ExponentialBackoff({}) + const spy = vi.fn() + + subscription.add( + backoff.withBackoff$.subscribe((res) => { + spy(res.isOk() ? res.value : res.error) + if (res.isOk()) { + backoff.trigger.next() + } + }), + ) + + await delayAsync(2000) + backoff.stop() + expect(spy).toHaveBeenCalledWith(0) + expect(spy).toHaveBeenCalledWith({ + error: 'stopped', + }) + }) + + afterAll(() => { + subscription.unsubscribe() + }) +}) diff --git a/packages/dapp-toolkit/src/helpers/exponential-backoff.ts b/packages/dapp-toolkit/src/helpers/exponential-backoff.ts index a2a42f37..7ffc8aef 100644 --- a/packages/dapp-toolkit/src/helpers/exponential-backoff.ts +++ b/packages/dapp-toolkit/src/helpers/exponential-backoff.ts @@ -6,7 +6,7 @@ import { map, merge, of, Subject, switchMap, timer } from 'rxjs' export type ExponentialBackoffInput = { multiplier?: number maxDelayTime?: number - timeout?: number + timeout?: number | Date interval?: number } export type ExponentialBackoff = typeof ExponentialBackoff @@ -17,6 +17,7 @@ export const ExponentialBackoff = ({ interval = 2_000, }: ExponentialBackoffInput = {}) => { const trigger = new Subject() + const stop = new Subject() let numberOfRetries = 0 const backoff$ = merge( @@ -36,12 +37,19 @@ export const ExponentialBackoff = ({ ) const withBackoffAndTimeout$: Observable> = - timeout - ? merge( - backoff$, - timer(timeout).pipe(map(() => err({ error: 'timeout' }))), - ) - : backoff$ + merge( + stop.asObservable().pipe(map(() => err({ error: 'stopped' }))), + timeout + ? merge( + backoff$, + timer(timeout).pipe(map(() => err({ error: 'timeout' }))), + ) + : backoff$, + ) - return { trigger, withBackoff$: withBackoffAndTimeout$ } + return { + trigger, + withBackoff$: withBackoffAndTimeout$, + stop: () => stop.next(), + } } diff --git a/packages/dapp-toolkit/src/helpers/generate-rola-challenge.spec.ts b/packages/dapp-toolkit/src/helpers/generate-rola-challenge.spec.ts new file mode 100644 index 00000000..aee907d5 --- /dev/null +++ b/packages/dapp-toolkit/src/helpers/generate-rola-challenge.spec.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest' +import { generateRolaChallenge } from './generate-rola-challenge' + +describe('generateRolaChallenge', () => { + it('should generate valid string', () => { + const challenge = generateRolaChallenge() + expect(challenge).toMatch(/^[0-9a-f]{64}$/) + }) +}) diff --git a/packages/dapp-toolkit/src/helpers/index.ts b/packages/dapp-toolkit/src/helpers/index.ts index 840c47ae..c6f679d5 100644 --- a/packages/dapp-toolkit/src/helpers/index.ts +++ b/packages/dapp-toolkit/src/helpers/index.ts @@ -1,6 +1,5 @@ export * from './exponential-backoff' export * from './fetch-wrapper' -export * from './is-mobile' export * from './logger' export * from './parse-json' export * from './remove-undefined' diff --git a/packages/dapp-toolkit/src/helpers/is-browser.ts b/packages/dapp-toolkit/src/helpers/is-browser.ts deleted file mode 100644 index 88de98f5..00000000 --- a/packages/dapp-toolkit/src/helpers/is-browser.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const isBrowser = () => - ![typeof window, typeof document].includes('undefined') diff --git a/packages/dapp-toolkit/src/helpers/is-mobile.spec.ts b/packages/dapp-toolkit/src/helpers/is-mobile.spec.ts deleted file mode 100644 index 555dc582..00000000 --- a/packages/dapp-toolkit/src/helpers/is-mobile.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { isMobile } from './is-mobile' - -describe('isMobile', () => { - it('should return true if userAgent is mobile', () => { - ;[ - 'Mozilla/5.0 (iPad; CPU OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.0.0 Mobile/15E148 Safari/604.1', - ].forEach((userAgent) => { - expect(isMobile(userAgent)).toBe(true) - }) - }) -}) diff --git a/packages/dapp-toolkit/src/helpers/is-mobile.ts b/packages/dapp-toolkit/src/helpers/is-mobile.ts deleted file mode 100644 index 2fed9ea2..00000000 --- a/packages/dapp-toolkit/src/helpers/is-mobile.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Bowser from 'bowser' - -export const isMobile = (userAgent: string = window.navigator.userAgent) => { - const parsed = Bowser.parse(userAgent) - return parsed.platform.type === 'mobile' || parsed.platform.type === 'tablet' -} diff --git a/packages/dapp-toolkit/src/helpers/parse-json.ts b/packages/dapp-toolkit/src/helpers/parse-json.ts index 37c03c7b..7b3ba89d 100644 --- a/packages/dapp-toolkit/src/helpers/parse-json.ts +++ b/packages/dapp-toolkit/src/helpers/parse-json.ts @@ -1,6 +1,5 @@ import type { Result } from 'neverthrow' import { err, ok } from 'neverthrow' -import { typedError } from './typed-error' export const parseJSON = >( text: string, @@ -8,6 +7,6 @@ export const parseJSON = >( try { return ok(JSON.parse(text)) } catch (error) { - return err(typedError(error)) + return err(error as Error) } } diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts index 2aee72f5..a858b6bf 100644 --- a/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts +++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { validateWalletResponse } from './validate-wallet-response' describe('validateWalletResponse', () => { - it('should parse valid response', async () => { + it('should parse valid response', () => { const walletResponse = { discriminator: 'success', interactionId: 'ab0f0190-1ae1-424b-a2c5-c36838f5b136', @@ -38,7 +38,7 @@ describe('validateWalletResponse', () => { }, } - const result = await validateWalletResponse(walletResponse) + const result = validateWalletResponse(walletResponse) expect(result.isOk() && result.value).toEqual(walletResponse) }) @@ -48,26 +48,11 @@ describe('validateWalletResponse', () => { const result = await validateWalletResponse(walletResponse) - expect(result.isErr() && result.error).toEqual({ - error: 'walletResponseValidation', - interactionId: '', - message: 'Invalid input', - }) - }) - - it('should return error valid failure response', async () => { - const walletResponse = { - discriminator: 'failure', - interactionId: '8cefec84-542d-40af-8782-b89df05db8ac', - error: 'rejectedByUser', - } - - const result = await validateWalletResponse(walletResponse) - - expect(result.isErr() && result.error).toEqual({ - discriminator: 'failure', - interactionId: '8cefec84-542d-40af-8782-b89df05db8ac', - error: 'rejectedByUser', - }) + expect(result.isErr() && result.error).toEqual( + expect.objectContaining({ + error: 'walletResponseValidation', + message: 'Invalid input', + }), + ) }) }) diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts index e9f8f072..a99b12a2 100644 --- a/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts +++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts @@ -1,27 +1,22 @@ -import { Result, ResultAsync, errAsync, okAsync } from 'neverthrow' -import { - WalletInteractionResponse, - WalletInteractionSuccessResponse, -} from '../schemas' +import { Result } from 'neverthrow' +import { WalletInteractionResponse } from '../schemas' import { SdkError } from '../error' -import { ValiError, parse } from 'valibot' +import { parse } from 'valibot' export const validateWalletResponse = ( walletResponse: unknown, -): ResultAsync => { +): Result => { const fn = Result.fromThrowable( (_) => parse(WalletInteractionResponse, _), - (error) => error as ValiError, + (error) => error, ) - const result = fn(walletResponse) - if (result.isErr()) { - return errAsync(SdkError('walletResponseValidation', '', 'Invalid input')) - } else if (result.isOk()) { - return result.value.discriminator === 'success' - ? okAsync(result.value) - : errAsync(result.value as any) - } - - return errAsync(SdkError('walletResponseValidation', '')) + return fn(walletResponse).mapErr((response) => + SdkError( + 'walletResponseValidation', + (walletResponse as any)?.interactionId, + 'Invalid input', + response, + ), + ) } diff --git a/packages/dapp-toolkit/src/modules/connect-button/connect-button.module.ts b/packages/dapp-toolkit/src/modules/connect-button/connect-button.module.ts index 001d3c19..a5511166 100644 --- a/packages/dapp-toolkit/src/modules/connect-button/connect-button.module.ts +++ b/packages/dapp-toolkit/src/modules/connect-button/connect-button.module.ts @@ -1,7 +1,10 @@ import { + concatMap, + delay, filter, finalize, first, + from, fromEvent, map, merge, @@ -10,17 +13,15 @@ import { Subscription, switchMap, tap, - timer, } from 'rxjs' import { ConnectButton } from '@radixdlt/connect-button' import type { Account, - RadixButtonStatus, RadixButtonTheme, RequestItem, } from 'radix-connect-common' import { ConnectButtonSubjects } from './subjects' -import { isMobile, type Logger } from '../../helpers' +import { type Logger } from '../../helpers' import { ExplorerConfig } from '../../_types' import { transformWalletDataToConnectButton, @@ -28,10 +29,9 @@ import { } from '../wallet-request' import { GatewayModule, RadixNetworkConfigById } from '../gateway' import { StateModule } from '../state' -import { StorageModule } from '../storage' -import { ConnectButtonModuleOutput, ConnectButtonStatus } from './types' -import { isBrowser } from '../../helpers/is-browser' +import { ConnectButtonModuleOutput } from './types' import { ConnectButtonNoopModule } from './connect-button-noop.module' +import { EnvironmentModule } from '../environment' export type ConnectButtonModule = ReturnType @@ -47,30 +47,33 @@ export type ConnectButtonModuleInput = { providers: { stateModule: StateModule gatewayModule: GatewayModule + environmentModule: EnvironmentModule walletRequestModule: WalletRequestModule - storageModule: StorageModule<{ - status: ConnectButtonStatus - }> } } export const ConnectButtonModule = ( input: ConnectButtonModuleInput, ): ConnectButtonModuleOutput => { - if (!isBrowser()) { + if (!input.providers.environmentModule.isBrowser()) { return ConnectButtonNoopModule() } import('@radixdlt/connect-button') const logger = input?.logger?.getSubLogger({ name: 'ConnectButtonModule' }) - const subjects = input.subjects || ConnectButtonSubjects() + const subjects = + input.subjects || + ConnectButtonSubjects({ + providers: { environmentModule: input.providers.environmentModule }, + }) const dAppDefinitionAddress = input.dAppDefinitionAddress - const { baseUrl, accountsPath, transactionPath } = input.explorer ?? { - baseUrl: RadixNetworkConfigById[input.networkId].dashboardUrl, - transactionPath: '/transaction/', - accountsPath: '/account/', - } - const statusStorage = input.providers.storageModule + const { baseUrl, accountsPath, transactionPath, subintentPath } = + input.explorer ?? { + baseUrl: RadixNetworkConfigById[input.networkId].dashboardUrl, + transactionPath: '/transaction/', + subintentPath: '/subintent/', + accountsPath: '/account/', + } const stateModule = input.providers.stateModule const gatewayModule = input.providers.gatewayModule @@ -88,7 +91,7 @@ export const ConnectButtonModule = ( const subscriptions = new Subscription() - const onConnectButtonRender$ = fromEvent(window, 'onConnectButtonRender') + const onConnectButtonRender$ = fromEvent(input.providers.environmentModule.globalThis, 'onConnectButtonRender') subscriptions.add( onConnectButtonRender$ @@ -286,11 +289,15 @@ export const ConnectButtonModule = ( subjects.onLinkClick .pipe( tap(({ type, data }) => { - if (['account', 'transaction'].includes(type)) { + if (['account', 'transaction', 'subintent'].includes(type)) { if (!baseUrl || !window) return const url = `${baseUrl}${ - type === 'transaction' ? transactionPath : accountsPath + type === 'transaction' + ? transactionPath + : type === 'subintent' + ? subintentPath + : accountsPath }${data}` window.open(url) @@ -314,7 +321,6 @@ export const ConnectButtonModule = ( onCancelRequestItem$: subjects.onCancelRequestItem.asObservable(), onIgnoreTransactionItem$: subjects.onIgnoreTransactionItem.asObservable(), onLinkClick$: subjects.onLinkClick.asObservable(), - setStatus: (value: RadixButtonStatus) => subjects.status.next(value), setTheme: (value: RadixButtonTheme) => subjects.theme.next(value), setMode: (value: 'light' | 'dark') => subjects.mode.next(value), setActiveTab: (value: 'sharing' | 'requests') => @@ -385,12 +391,6 @@ export const ConnectButtonModule = ( walletRequestModule.requestItems$ .pipe( tap((items) => { - const hasPendingItem = items.find((item) => item.status === 'pending') - - if (hasPendingItem) { - connectButtonApi.setStatus('pending') - } - connectButtonApi.setRequestItems([...items].reverse()) }), ) @@ -423,7 +423,11 @@ export const ConnectButtonModule = ( oneTime: false, }), ) - .map(() => isMobile() && subjects.showPopoverMenu.next(false)), + .map( + () => + input.providers.environmentModule.isMobile() && + subjects.showPopoverMenu.next(false), + ), ), ) .subscribe(), @@ -448,43 +452,36 @@ export const ConnectButtonModule = ( .subscribe(), ) - subscriptions.add( - statusStorage.storage$ - .pipe( - switchMap(() => - statusStorage.getState().map((state) => { - if (state?.status) { - subjects.status.next(state.status) - } - }), - ), + const setPendingOrDefault = () => + walletRequestModule + .getPendingRequests() + .andTee((items) => + subjects.status.next(items.length ? 'pending' : 'default'), ) - .subscribe(), - ) subscriptions.add( walletRequestModule.interactionStatusChange$ .pipe( - mergeMap((newStatus) => { - statusStorage.setState({ - status: newStatus === 'success' ? 'success' : 'error', - }) - - return timer(2000).pipe( - tap(() => { - const result = walletRequestModule.getPendingRequests() - result.map((pendingItems) => { - statusStorage.setState({ - status: pendingItems.length ? 'pending' : 'default', - }) - }) - }), - ) - }), + mergeMap((newStatus) => + of( + subjects.status.next( + newStatus === 'success' + ? 'success' + : newStatus === 'fail' + ? 'error' + : 'pending', + ), + ).pipe( + delay(2000), + concatMap(() => setPendingOrDefault()), + ), + ), ) .subscribe(), ) + setPendingOrDefault() + if (dAppDefinitionAddress) { gatewayModule.gatewayApi .getEntityMetadataPage(dAppDefinitionAddress) diff --git a/packages/dapp-toolkit/src/modules/connect-button/subjects.ts b/packages/dapp-toolkit/src/modules/connect-button/subjects.ts index 1f842b19..8f26d07f 100644 --- a/packages/dapp-toolkit/src/modules/connect-button/subjects.ts +++ b/packages/dapp-toolkit/src/modules/connect-button/subjects.ts @@ -1,10 +1,10 @@ -import type { Account, RequestItem } from 'radix-connect-common' +import type { Account, RadixButtonTheme, RequestItem } from 'radix-connect-common' import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs' -import { isMobile } from '../../helpers' import { ConnectButtonStatus } from './types' +import { EnvironmentModule } from '../environment' export type ConnectButtonSubjects = ReturnType -export const ConnectButtonSubjects = () => ({ +export const ConnectButtonSubjects = (input: { providers: { environmentModule: EnvironmentModule }}) => ({ onConnect: new Subject<{ challenge: string } | undefined>(), onDisconnect: new Subject(), onUpdateSharedAccounts: new Subject(), @@ -16,22 +16,20 @@ export const ConnectButtonSubjects = () => ({ onShowPopover: new Subject(), status: new BehaviorSubject('default'), loggedInTimestamp: new BehaviorSubject(''), - isMobile: new BehaviorSubject(isMobile()), + isMobile: new BehaviorSubject(input.providers.environmentModule.isMobile()), isWalletLinked: new BehaviorSubject(false), showPopoverMenu: new BehaviorSubject(false), isExtensionAvailable: new BehaviorSubject(false), fullWidth: new BehaviorSubject(false), activeTab: new BehaviorSubject<'sharing' | 'requests'>('sharing'), mode: new BehaviorSubject<'light' | 'dark'>('light'), - theme: new BehaviorSubject< - 'radix-blue' | 'black' | 'white' | 'white-with-outline' - >('radix-blue'), + theme: new BehaviorSubject('radix-blue'), avatarUrl: new BehaviorSubject(''), personaLabel: new BehaviorSubject(''), personaData: new BehaviorSubject<{ value: string; field: string }[]>([]), dAppName: new BehaviorSubject(''), onLinkClick: new Subject<{ - type: 'account' | 'transaction' | 'setupGuide' | 'showQrCode' | 'getWallet' + type: 'account' | 'transaction' | 'setupGuide' | 'showQrCode' | 'getWallet' | 'subintent' data: string }>(), }) diff --git a/packages/dapp-toolkit/src/modules/connect-button/types.ts b/packages/dapp-toolkit/src/modules/connect-button/types.ts index 043f463c..93bf8cc9 100644 --- a/packages/dapp-toolkit/src/modules/connect-button/types.ts +++ b/packages/dapp-toolkit/src/modules/connect-button/types.ts @@ -23,10 +23,9 @@ export type ConnectButtonModuleOutput = { onShowPopover$: Observable onCancelRequestItem$: Observable onLinkClick$: Observable<{ - type: 'account' | 'transaction' | 'showQrCode' | 'setupGuide' | 'getWallet' + type: 'account' | 'transaction' | 'showQrCode' | 'setupGuide' | 'getWallet' | 'subintent' data: string }> - setStatus: (value: RadixButtonStatus) => void setMode: (value: 'light' | 'dark') => void setTheme: (value: RadixButtonTheme) => void setActiveTab: (value: 'sharing' | 'requests') => void diff --git a/packages/dapp-toolkit/src/modules/environment/environment.module.spec.ts b/packages/dapp-toolkit/src/modules/environment/environment.module.spec.ts new file mode 100644 index 00000000..2a3a3d07 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/environment/environment.module.spec.ts @@ -0,0 +1,609 @@ +import { describe, expect, it } from 'vitest' +import { EnvironmentModule } from './environment.module' + +describe('EnvironmentModule', () => { + const environmentModule = EnvironmentModule() + const isMobile = environmentModule.isMobile + describe('isMobile', () => { + it('should return true if userAgent is mobile', () => { + ;[ + 'Mozilla/5.0 (iPad; CPU OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.0.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/132.0 Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPad; CPU OS 17_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Linux; Android 10; ONEPLUS A6003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.102 Mobile Safari/537.36 EdgA/130.0.2849.68', + 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.104 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 15; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.104 Mobile Safari/537.36', + 'Mozilla/5.0 (Android 13; Mobile; rv:133.0) Gecko/133.0 Firefox/133.0', + 'Mozilla/5.0 (Android 14; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0', + 'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.78 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.92 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.92 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.6367.88 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/79.0.3945.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7.1 Mobile/20H30 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7 Mobile/15E148 Safari/604.1 OPT/5.0.7', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7.10 Mobile/20H350 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7.2 Mobile/20H115 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7.8 Mobile/20H343 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/21B74 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1 Ddg/17.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1 OPT/5.1.0', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/121.0.6167.138 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.6367.68 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 YaBrowser/24.7.7.783.10 SA/3 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/21E236 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.6478.153 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/127.0.6533.107 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/127.0.6533.77 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.34 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.92 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.37 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.78 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/333.0.671582647 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/336.0.679286625 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/339.0.686111475 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/342.0.693598186 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1 Ddg/17.5', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5.1 Mobile/21F90 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.109 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.144 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.6478.153 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/127.0.6533.107 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.37 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.78 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/128.0.2739.42 Version/17.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/130.0.2849.80 Version/17.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/333.0.671582647 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/335.0.676534794 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/338.1.685896509 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/340.3.689937600 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1 Ddg/17.6', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/130.1 Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/21G93 [FBAN/FBIOS;FBAV/482.1.0.47.106;FBBV/644413532;FBDV/iPhone14,7;FBMD/iPhone;FBSN/iOS;FBSV/17.6.1;FBSS/3;FBID/phone;FBLC/pl_PL;FBOP/5;FBRV/646501326;IABMV/1]', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1 Ddg/17.6', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1 OPT/5.0.7', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/15E148 Safari/605.1.15 (Ecosia ios@10.1.2.1933)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/15E148 Safari/605.1.15 (Ecosia ios@10.1.3.1951)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/21G101 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/21G93 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/116.0.5845.177 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Mobile/21H16 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.8 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.8.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/112.0.5615.167 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.144 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/126.0.6478.161 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.69 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.37 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/130.1 Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/331.0.665236494 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/22A3354 Instagram 351.0.1.35.98 (iPhone13,4; iOS 18_0; fr_FR; fr-FR; scale=3.00; 1284x2778; 647722782; IABMV/1)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/22A3354 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/22A3370 Instagram 355.0.0.34.91 (iPhone15,3; iOS 18_0_1; hr_HR; hr; scale=3.00; 1290x2796; 656616803; IABMV/1)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 Ddg/18.0', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 OPT/5.0.0', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 OPT/5.1.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/22A3370 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/128.0.6613.98 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/129.0.6668.46 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.90 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/130.0.2849.80 Version/18.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/132.0 Mobile/15E148 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/22B5054e Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/22B83 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/131.0.6778.73 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1 Ddg/18.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Mobile/22B91 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.6723.78 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/130.0.2849.68 Version/18.0 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Linux; Android 10; 5030D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.79 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; CLT-L29 Build/HUAWEICLT-L29; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.14 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/485.2.0.68.111;]', + 'Mozilla/5.0 (Linux; Android 10; CPH1931 Build/QKQ1.200209.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; HRY-LX1T Build/HONORHRY-LX1T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36 Presearch (Tempest)', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36 XiaoMi/MiuiBrowser/14.17.0-gn', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36 XiaoMi/MiuiBrowser/14.18.0-gn', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36 XiaoMi/MiuiBrowser/14.19.0-gn', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 AlohaBrowser/6.3.2', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 AlohaBrowser/6.4.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 EdgA/126.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 OPR/83.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36 Veera/1.3.5', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36 EdgA/127.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36 OPR/84.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 EdgA/127.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/84.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36 EdgA/128.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36 EdgA/129.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36 OPR/85.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.54 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36 EdgA/130.0.0.0', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.60 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/25.0 Chrome/121.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/122.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; M2007J20CG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.40 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; moto z4 Build/QPFS30.130-15-11-23) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; moto z4 Build/QPFS30.130-15-11-23) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; Nokia 5.1 Plus) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; Redmi Note 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; Redmi Note 9S Build/QKQ1.191215.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.46 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/490.0.0.63.82;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 10; Redmi Y3 Build/QKQ1.191008.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.148 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; Redmi Y3 Build/QKQ1.191008.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.102 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; SM-A015F Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.43 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; SM-A015F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.99 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; STK-L21 Build/HUAWEISTK-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; STK-L21 Build/HUAWEISTK-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; STK-L21 Build/HUAWEISTK-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; TECNO KD7 Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.99 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; YAL-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; 220333QAG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; CPH1937 Build/RKQ1.200903.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; CPH1989 Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.58 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; CPH2113) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Infinix X6812 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/475.0.0.60.109;]', + 'Mozilla/5.0 (Linux; Android 11; Infinix X6812 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.54 Mobile Safari/537.36[FBAN/EMA;FBLC/en_US;FBAV/407.0.0.12.116;]', + 'Mozilla/5.0 (Linux; Android 11; Infinix X6812 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.108 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/485.2.0.68.111;]', + 'Mozilla/5.0 (Linux; Android 11; Infinix X688B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.82 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Infinix X689B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Infinix X695) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.133 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.70 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.39 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Stargon/6.1.5 Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) Build/RZBS31.Q2-143-27-25) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.70 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) play Build/RPXS31.Q2-58-17-7-3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) power Build/RPES31.Q4U-47-35-12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; K) power Build/RPES31.Q4U-47-35-12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Lenovo TB-J716F Build/RKQ1.201112.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.20 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; LM-K510 Build/RKQ1.210420.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.88 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/452.0.0.45.110;]', + 'Mozilla/5.0 (Linux; Android 11; M2003J15SC Build/RP1A.200720.011;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; M2003J15SC) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.85 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; M2007J20CG Build/RKQ1.200826.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.108 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; M2101K7BI Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; M2101K7BI Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; M2102J20SI Build/RKQ1.200826.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; moto e20 Build/RONS31.267-94-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; moto e20 Build/RONS31.267-94-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; moto g(20)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; moto g(9) play Build/RPXS31.Q2-58-17-7-3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/479.1.0.76.109;]', + 'Mozilla/5.0 (Linux; Android 11; Motorola Defy) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.85 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Redmi Note 7 Build/RQ3A.211001.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; SM-A125F Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.88 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/480.0.0.54.88;]', + 'Mozilla/5.0 (Linux; Android 11; TECNO KF6k) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.98 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; V2043 Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/473.0.0.35.110;]', + 'Mozilla/5.0 (Linux; Android 11; V2043 Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/474.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 11; V2146 Build/RP1A.200720.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.14 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; vivo 1901) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; vivo 1904; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36 VivoBrowser/12.9.0.4', + 'Mozilla/5.0 (Linux; Android 11; vivo 1918 Build/RP1A.200720.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11; vivo 1920 Build/RP1A.200720.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile DuckDuckGo/5 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; CPH2473) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; Infinix X6516 Build/SP1A.210812.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/474.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 12; itel S665L Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.186 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; K) Build/S2RI32.32-20-9-9-2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.128 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2007J17C Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2007J20CG Build/SKQ1.211019.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.66 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/483.0.0.51.72;]', + 'Mozilla/5.0 (Linux; Android 12; M2007J20CG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2007J20CI Build/SKQ1.211019.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2007J3SG Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.24 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/486.0.0.66.70;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 12; M2010J19SG Build/SKQ1.211202.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2010J19SI Build/SKQ1.211202.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2012K11AG Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2101K7AG Build/SKQ1.210908.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2101K7BNY) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2102J20SG Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; M2102J20SG Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; Mi 10 Pro Build/XiaomiMi 10 Pro;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; Redmi Note 9 Build/SQ3A.220705.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; Redmi Note 9 Pro Build/SKQ1.211019.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.143 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; RMX2155 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/475.0.0.60.109;]', + 'Mozilla/5.0 (Linux; Android 12; RMX2155 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.66 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/483.0.0.51.72;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 12; RMX2155 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.24 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/486.0.0.66.70;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 12; RMX2161 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; RMX2161 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.38 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-A115M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SM-A315G Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.128 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SM-A715F Build/HUAWEISM-A715F;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SM-G780G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SM-G970F Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.148 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/476.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 12; SM-M317F Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; SM-N970F Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.143 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/466.0.0.39.109;]', + 'Mozilla/5.0 (Linux; Android 12; TECNO KI5k Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12; V2027 Build/SP1A.210812.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.105 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/473.1.0.45.110;]', + 'Mozilla/5.0 (Linux; Android 12; V2027 Build/SP1A.210812.003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; 2201116PI Build/TKQ1.221114.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; 2201117PG Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.24 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/486.0.0.66.70;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 13; 2209116AG Build/TKQ1.221114.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; 23108RN04Y Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/478.0.0.47.115;]', + 'Mozilla/5.0 (Linux; Android 13; 23122PCD1G Build/TKQ1.221114.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.108 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; 23124RA7EO Build/TKQ1.221114.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.148 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/476.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 13; Android_Device) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; CPH2159 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; CPH2159 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.86 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; CPH2197 Build/TP1A.220905.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.51 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/482.0.0.51.80;]', + 'Mozilla/5.0 (Linux; Android 13; CPH2473 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; CPH2591 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; en; Infinix X6525B Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.24.1.3 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; en; Infinix X6711 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.23.1.4 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; en; Infinix X678B Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.23.1.4 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; Infinix X6525 Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.134 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/481.0.0.61.80;]', + 'Mozilla/5.0 (Linux; Android 13; Infinix X6731B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.70 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; Infinix X6739 Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; Infinix X6815D Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; itel A666LN Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.116 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.70 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.39 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.38 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; M2101K6G Build/TKQ1.221013.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; M2101K6G Build/TKQ1.221013.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; M2101K6P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.159 Mobile Safari/537.36 OPR/82.2.4342.79546', + 'Mozilla/5.0 (Linux; Android 13; M2101K7BNY Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.58 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; M2101K9G Build/TKQ1.220829.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; M2103K19G Build/TP1A.220624.014;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; moto e13 Build/TLAS33.105-285-4; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/482.0.0.51.80;]', + 'Mozilla/5.0 (Linux; Android 13; moto e13 Build/TLAS33.105-285-4; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.24 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/486.0.0.66.70;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; RMX3363) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36 Carbon', + 'Mozilla/5.0 (Linux; Android 13; RMX3624 Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.82 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; RMX3630 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; RMX3760 Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/479.1.0.76.109;]', + 'Mozilla/5.0 (Linux; Android 13; RMX3830 Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.123 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/480.0.0.54.88;]', + 'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A032M Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 SamsungBrowser/7.4 Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A127F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A325F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; SM-A037F Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.70 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; SM-A326B Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/475.0.0.60.109;]', + 'Mozilla/5.0 (Linux; Android 13; SM-A515F Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/477.0.0.50.99;]', + 'Mozilla/5.0 (Linux; Android 13; SM-G781B Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/477.0.0.50.99;]', + 'Mozilla/5.0 (Linux; Android 13; SM-G781B Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; TECNO LH8n Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; V2025) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; V2037 Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; V2037 Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.82 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 13; V2146 Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.107 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/483.0.0.51.72;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 14; 22041211AC Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.20 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041211AC Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.38 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041211AC Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.5 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041211AC Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.2 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041211AC Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.14 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041216C Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 22041219NY Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/473.1.0.45.110;]', + 'Mozilla/5.0 (Linux; Android 14; 22071212AG Build/UP1A.230905.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.82 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 2211133G Build/UKQ1.230705.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 23021RAA2Y Build/UKQ1.230917.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.134 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/481.0.0.61.80;]', + 'Mozilla/5.0 (Linux; Android 14; 23021RAA2Y Build/UKQ1.230917.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/484.0.0.63.83;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 14; 23049PCD8G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.192 Mobile Safari/537.36 OPR/74.2.3922.71802', + 'Mozilla/5.0 (Linux; Android 14; 23076PC4BI Build/UKQ1.230917.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 23090RA98C Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 2311DRK48G Build/UP1A.230905.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 2311DRK48G Build/UP1A.230905.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 23124RA7EO Build/UKQ1.231207.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.97 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/485.0.0.70.77;IABMV/1;]', + 'Mozilla/5.0 (Linux; Android 14; 2312DRA50G Build/UKQ1.231003.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.148 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/477.0.0.50.99;]', + 'Mozilla/5.0 (Linux; Android 14; 2312DRA50G Build/UKQ1.231003.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; 24066PC95I Build/UKQ1.240116.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; ASUS_AI2401_D Build/UKQ1.231003.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.88 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; CPH2581 Build/UKQ1.230924.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/475.0.0.60.109;]', + 'Mozilla/5.0 (Linux; Android 14; CPH2591 Build/UP1A.230620.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; en; A065 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.23.1.4 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; en; Infinix X6833B Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.23.1.4 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; en; Infinix X6853 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.22.3.5 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; en; RMX3710 Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.129 HiBrowser/v2.23.1.4 UWS/ Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2214 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.128 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2214 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.102 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2216 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2219 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2219 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2220 Build/UP1A.231005.007_IN) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2220 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; I2220 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; Infinix X678B Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; Infinix X6833B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; Infinix X6833B Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; Infinix X6833B Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; Infinix X6850 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.58 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.186 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.128 Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.38 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.5 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.39 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; motorola edge 30 neo Build/U1SSMS34.31-64-4-6; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 Instagram 349.0.0.39.104 Android (34/14; 420dpi; 1080x2174; motorola; motorola edge 30 neo; miami; qcom; pt_BR; 642842387)', + 'Mozilla/5.0 (Linux; Android 14; MT2111 Build/UKQ1.230924.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.102 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; RMX3393 Build/UKQ1.230924.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.51 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/482.0.0.51.80;]', + 'Mozilla/5.0 (Linux; Android 14; RMX3710 Build/UKQ1.230924.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; RMX3780 Build/UKQ1.230924.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.134 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/481.0.0.61.80;]', + 'Mozilla/5.0 (Linux; Android 14; RMX3998 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; SAMSUNG SM-A055F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; SM-A245F Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.108 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; SM-A256B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.81 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/478.0.0.47.115;]', + 'Mozilla/5.0 (Linux; Android 14; SM-A525F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36 Carbon', + 'Mozilla/5.0 (Linux; Android 14; SM-M236B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36 Carbon', + 'Mozilla/5.0 (Linux; Android 14; SM-S911B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.148 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/476.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 14; TECNO KJ7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; TECNO LH8n) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.56 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; TrebleDroid with GApps Build/UQ1A.240205.004) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.164 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 14; V2310 Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.1.0; CPH1909) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.1.0; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.1.0; OPPO R11; Build/OPM1.171019.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.4280.141 Mobile Safari/537.36 Firefox-KiToBrowser/115.0', + 'Mozilla/5.0 (Linux; Android 9; AMN-LX9 Build/HUAWEIAMN-LX9; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/474.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 9; ASUS_X01AD Build/WW_Phone-202011271133) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.127 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; CPH2083 Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.84 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; Infinix X650C Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.110 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.105 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; LG-H870 Build/PKQ1.190522.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/476.1.0.47.109;]', + 'Mozilla/5.0 (Linux; Android 9; Redmi 6A Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.81 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; Redmi 8A Pro Build/PKQ1.190319.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; Redmi Y2 Build/PKQ1.181203.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; vivo 1902 Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.146 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; vivo 1904) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.66 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 9; VIVO Y17 Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.73 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 13; M2012K11AG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.165 YaBrowser/24.7.8.165.00 SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 13; SM-A515F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 YaBrowser/24.7.9.61.00 SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.142 YaBrowser/24.7.4.142.00 SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 14; SM-A057F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.124 YaBrowser/24.7.3.124.00 SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 14; SM-A057F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.22 YaBrowser/24.7.8.22.00 (beta) SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; arm_64; Android 14; SM-A057F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.46 YaBrowser/24.10.2.46.00 (beta) SA/3 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; U; Android 10; id-id; Redmi Note 9 Pro Build/QQ3A.200805.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.128 Mobile Safari/537.36 XiaoMi/Mint Browser/3.9.3', + 'Mozilla/5.0 (Linux; U; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Safari/537.36 OPR/84.0.2254.73823', + 'Mozilla/5.0 (Linux; U; Android 12; en-gb; SM-A125F Build/SP1A.210812.016) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.61 Mobile Safari/537.36 PHX/5.6', + 'Mozilla/5.0 (Linux; U; Android 12; M2102J20SG Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.128 Mobile Safari/537.36 OPR/84.0.2254.73823', + 'Mozilla/5.0 (Linux; U; Android 12; Redmi Note 9 Pro Max Build/SKQ1.211019.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.146 Mobile Safari/537.36 OPR/84.0.2254.73823', + 'Mozilla/5.0 (Linux; U; Android 12; SM-M115F Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.81 Mobile Safari/537.36 OPR/83.1.2254.73239', + 'Mozilla/5.0 (Linux; U; Android 13; en-us; Redmi 10 Build/TP1A.220624.014) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.25.2.2-gn', + 'Mozilla/5.0 (Linux; U; Android 13; pt-pt; Redmi Note 11 Build/TKQ1.221114.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/112.0.5615.136 Mobile Safari/537.36 XiaoMi/MiuiBrowser/14.10.1.2-gn', + 'Mozilla/5.0 (Linux; U; Android 14; en-us; 23046PNC9C Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.119 Mobile Safari/537.36 XiaoMi/MiuiBrowser/18.6.60929', + 'Mozilla/5.0 (Linux; U; Android 8.1.0; SM-G610F Build/M1AJQ; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36 OPR/47.2.2254.147957', + 'Mozilla/5.0 (Linux; U; Android 9; in-id; Infinix X650C Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36 PHX/10.6', + ].forEach((userAgent) => { + expect(isMobile(userAgent)).toBe(true) + }) + }) + + it('should return false if user agent is not mobile', () => { + ;[ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6308.196 Safari/537.36 Edg/126.0.2282.70', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 OPR/112.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 YaBrowser/24.7.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Decentr Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Norton/127.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/113.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/113.0.0.0 (Edition std-1)', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0 (Edition std-2)', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 YaBrowser/24.10.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.644 YaBrowser/24.10.4.644 (beta) Yowser/2.5 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 AVG/130.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0', + 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0', + 'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6301.219 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.140', + 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Safari/605.1.15', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 YaBrowser/24.10.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0', + 'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0', + 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + ].forEach((userAgent) => { + expect(isMobile(userAgent)).toBe(false) + }) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/environment/environment.module.ts b/packages/dapp-toolkit/src/modules/environment/environment.module.ts new file mode 100644 index 00000000..d520c097 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/environment/environment.module.ts @@ -0,0 +1,35 @@ +export type EnvironmentModule = ReturnType +export const EnvironmentModule = () => { + const isMobile = (userAgent: string) => { + const ua = userAgent.toLowerCase() + + return /(mobi|ipod|phone|blackberry|opera mini|fennec|minimo|symbian|psp|nintendo ds|archos|skyfire|puffin|blazer|bolt|gobrowser|iris|maemo|semc|teashark|uzard|ipad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test( + ua, + ) + } + + const getNavigator = () => { + return typeof navigator !== 'undefined' ? navigator : undefined + } + + /** + * Checks if the provided object is a Telegram Mobile App (TMA) global object. + * + * @param maybeTgGlobalObject - The object to check. + * @returns `true` if the object has WebView initialization parameters, otherwise `false`. + */ + const isTMA = () => + Object.keys((globalThis as any)?.Telegram?.WebView?.initParams || {}) + .length > 0 + + return { + get globalThis() { + return globalThis + }, + isMobile: (userAgent?: string) => { + return isMobile(userAgent ?? getNavigator()?.userAgent ?? '') + }, + isTMA, + isBrowser: () => ![typeof window, typeof document].includes('undefined'), + } +} diff --git a/packages/dapp-toolkit/src/modules/environment/index.ts b/packages/dapp-toolkit/src/modules/environment/index.ts new file mode 100644 index 00000000..edd92c80 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/environment/index.ts @@ -0,0 +1 @@ +export * from './environment.module' \ No newline at end of file diff --git a/packages/dapp-toolkit/src/modules/gateway/gateway.module.ts b/packages/dapp-toolkit/src/modules/gateway/gateway.module.ts index a6f2ae8f..639673b3 100644 --- a/packages/dapp-toolkit/src/modules/gateway/gateway.module.ts +++ b/packages/dapp-toolkit/src/modules/gateway/gateway.module.ts @@ -8,7 +8,7 @@ import { ExponentialBackoff, } from '../../helpers' import { SdkError } from '../../error' -import { TransactionStatus, TransactionStatusResponse } from './types' +import { SubintentStatus, TransactionStatus } from './types' import { GatewayApiClientConfig } from '../../_types' export type GatewayModule = ReturnType @@ -25,67 +25,50 @@ export const GatewayModule = (input: { const gatewayApi = input?.providers?.gatewayApiService ?? GatewayApiService(input.clientConfig) - const pollTransactionStatus = ( - transactionIntentHash: string, - ): ResultAsync => { - const retry = ExponentialBackoff(input.retryConfig) - - const completedTransactionStatus = new Set([ - 'CommittedSuccess', - 'CommittedFailure', - 'Rejected', - ]) - + const poll = ( + hash: string, + apiCall: () => ResultAsync, + exponentialBackoff: ReturnType, + ): ResultAsync => { return ResultAsync.fromPromise( firstValueFrom( - retry.withBackoff$.pipe( + exponentialBackoff.withBackoff$.pipe( switchMap((result) => { if (result.isErr()) return [ err( - SdkError('failedToPollSubmittedTransaction', '', undefined, { + SdkError('failedToPoll', '', undefined, { error: result.error, - context: - 'GatewayModule.pollTransactionStatus.retry.withBackoff$', - transactionIntentHash, + context: 'GatewayModule.poll.retry.withBackoff$', + hash, }), ), ] - logger?.debug(`pollingTxStatus retry #${result.value + 1}`) + logger?.debug(`Polling ${hash} retry #${result.value + 1}`) - return gatewayApi - .getTransactionStatus(transactionIntentHash) - .map((response) => { - if (completedTransactionStatus.has(response.status)) - return response + return apiCall().orElse((response) => { + if (response.reason === 'FailedToFetch') { + logger?.debug({ + error: response, + context: 'unexpected error, retrying', + }) + exponentialBackoff.trigger.next() + return ok(undefined) + } - retry.trigger.next() - return - }) - .orElse((response) => { - if (response.reason === 'FailedToFetch') { - logger?.debug({ - error: response, - context: 'unexpected error, retrying', - }) - retry.trigger.next() - return ok(undefined) - } - - logger?.debug(response) - return err( - SdkError('failedToPollSubmittedTransaction', '', undefined, { - error: response, - transactionIntentHash, - context: - 'GatewayModule.pollTransactionStatus.getTransactionStatus', - }), - ) - }) + logger?.debug(response) + return err( + SdkError('failedToPoll', '', undefined, { + error: response, + hash, + context: 'GatewayModule.poll', + }), + ) + }) }), filter( - (result): result is Result => + (result): result is Result => (result.isOk() && !!result.value) || result.isErr(), ), first(), @@ -95,7 +78,71 @@ export const GatewayModule = (input: { ).andThen((result) => result) } + const pollTransactionStatus = ( + transactionIntentHash: string, + ): ResultAsync => { + const exponentialBackoff = ExponentialBackoff(input.retryConfig) + return poll( + transactionIntentHash, + () => + gatewayApi + .getTransactionStatus(transactionIntentHash) + .map(({ status }) => { + const completedStatus = new Set([ + 'CommittedSuccess', + 'CommittedFailure', + 'Rejected', + ]) + if (completedStatus.has(status)) return status + + exponentialBackoff.trigger.next() + return + }), + exponentialBackoff, + ) + } + + const pollSubintentStatus = ( + subintentHash: string, + expirationTimestamp: number, + ) => { + const exponentialBackoff = ExponentialBackoff({ + ...input.retryConfig, + maxDelayTime: 60_000, + timeout: new Date(expirationTimestamp * 1000), + }) + + return { + stop: exponentialBackoff.stop, + result: poll<{ + subintentStatus: SubintentStatus + transactionIntentHash: string + }>( + subintentHash, + () => + gatewayApi + .getSubintentStatus(subintentHash) + .map( + ({ subintent_status, finalized_at_transaction_intent_hash }) => { + if (subintent_status === 'CommittedSuccess') { + return { + subintentStatus: subintent_status, + transactionIntentHash: finalized_at_transaction_intent_hash, + } + } + + exponentialBackoff.trigger.next() + return + }, + ), + + exponentialBackoff, + ), + } + } + return { + pollSubintentStatus, pollTransactionStatus, gatewayApi, configuration: input.clientConfig, diff --git a/packages/dapp-toolkit/src/modules/gateway/gateway.service.ts b/packages/dapp-toolkit/src/modules/gateway/gateway.service.ts index 73b70dae..90acef14 100644 --- a/packages/dapp-toolkit/src/modules/gateway/gateway.service.ts +++ b/packages/dapp-toolkit/src/modules/gateway/gateway.service.ts @@ -1,4 +1,4 @@ -import { EntityMetadataItem, TransactionStatus } from './types' +import { EntityMetadataItem, SubintentStatus, TransactionStatus } from './types' import { fetchWrapper } from '../../helpers' import { __VERSION__ } from '../../version' import { GatewayApiClientConfig } from '../../_types' @@ -32,12 +32,21 @@ export const GatewayApiService = ({ intent_hash: transactionIntentHash, }) + const getSubintentStatus = (subintentHash: string) => + fetchWithHeaders<{ + subintent_status: SubintentStatus + finalized_at_transaction_intent_hash: string + }>('/transaction/subintent-status', { + subintent_hash: subintentHash, + }) + const getEntityMetadataPage = (address: string) => fetchWithHeaders<{ items: EntityMetadataItem[] }>('/state/entity/page/metadata', { address }) return { + getSubintentStatus, getTransactionStatus, getEntityMetadataPage, } diff --git a/packages/dapp-toolkit/src/modules/gateway/types.ts b/packages/dapp-toolkit/src/modules/gateway/types.ts index 2e7ce320..1439760f 100644 --- a/packages/dapp-toolkit/src/modules/gateway/types.ts +++ b/packages/dapp-toolkit/src/modules/gateway/types.ts @@ -8,6 +8,8 @@ export const TransactionStatus = { Rejected: 'Rejected', } as const +export type SubintentStatus = Extract + export type MetadataStringValue = { type: 'String' value: string diff --git a/packages/dapp-toolkit/src/modules/index.ts b/packages/dapp-toolkit/src/modules/index.ts index 075c0a7a..c7e2d8d4 100644 --- a/packages/dapp-toolkit/src/modules/index.ts +++ b/packages/dapp-toolkit/src/modules/index.ts @@ -1,5 +1,6 @@ export * from './connect-button' export * from './gateway' export * from './state' +export * from './environment' export * from './storage' export * from './wallet-request' diff --git a/packages/dapp-toolkit/src/modules/state/state.module.ts b/packages/dapp-toolkit/src/modules/state/state.module.ts index c6c2faba..42ecb8b2 100644 --- a/packages/dapp-toolkit/src/modules/state/state.module.ts +++ b/packages/dapp-toolkit/src/modules/state/state.module.ts @@ -2,7 +2,7 @@ import { BehaviorSubject, Subscription, filter } from 'rxjs' import { RdtState, WalletData, walletDataDefault } from './types' import { Logger } from '../../helpers' import { StorageModule } from '../storage' -import { ok, okAsync } from 'neverthrow' +import { ok, okAsync, ResultAsync } from 'neverthrow' export type StateModule = ReturnType @@ -13,20 +13,22 @@ export const StateModule = (input: { } }) => { const logger = input?.logger?.getSubLogger({ name: 'StateModule' }) - const storageModule = input.providers.storageModule + const storageModule: StorageModule = input.providers.storageModule const subscriptions = new Subscription() const setState = (state: RdtState) => storageModule.setState(state) - const getState = () => + const getState = (): ResultAsync => storageModule .getState() .orElse(() => okAsync(defaultState)) .andThen((state) => (state ? ok(state) : ok(defaultState))) - + const patchState = (state: Partial) => - getState().andThen((oldState) => setState({ ...oldState, ...state } as RdtState)) + getState().andThen((oldState) => + setState({ ...oldState, ...state } as RdtState), + ) const defaultState = { walletData: walletDataDefault, diff --git a/packages/dapp-toolkit/src/modules/state/types.ts b/packages/dapp-toolkit/src/modules/state/types.ts index 39b429e7..3278ca54 100644 --- a/packages/dapp-toolkit/src/modules/state/types.ts +++ b/packages/dapp-toolkit/src/modules/state/types.ts @@ -14,7 +14,7 @@ import { object, variant, string, - Output, + InferOutput, } from 'valibot' export const proofType = { @@ -22,7 +22,7 @@ export const proofType = { account: 'account', } as const -export type SignedChallengePersona = Output +export type SignedChallengePersona = InferOutput export const SignedChallengePersona = object({ challenge: string(), proof: Proof, @@ -30,7 +30,7 @@ export const SignedChallengePersona = object({ type: literal(proofType.persona), }) -export type SignedChallengeAccount = Output +export type SignedChallengeAccount = InferOutput export const SignedChallengeAccount = object({ challenge: string(), proof: Proof, @@ -38,7 +38,7 @@ export const SignedChallengeAccount = object({ type: literal(proofType.account), }) -export type SignedChallenge = Output +export type SignedChallenge = InferOutput export const SignedChallenge = variant('type', [ SignedChallengePersona, SignedChallengeAccount, @@ -59,14 +59,14 @@ export const WalletDataPersonaDataPhoneNumbersAddresses = object({ fields: array(string()), }) -export type WalletDataPersonaData = Output +export type WalletDataPersonaData = InferOutput export const WalletDataPersonaData = variant('entry', [ WalletDataPersonaDataFullName, WalletDataPersonaDataEmailAddresses, WalletDataPersonaDataPhoneNumbersAddresses, ]) -export type WalletData = Output +export type WalletData = InferOutput export const WalletData = object({ accounts: array(Account), personaData: array(WalletDataPersonaData), @@ -74,7 +74,7 @@ export const WalletData = object({ proofs: array(SignedChallenge), }) -export type SharedData = Output +export type SharedData = InferOutput export const SharedData = object({ persona: optional(object({ proof: boolean() })), ongoingAccounts: optional( @@ -86,7 +86,7 @@ export const SharedData = object({ ongoingPersonaData: optional(PersonaDataRequestItem), }) -export type RdtState = Output +export type RdtState = InferOutput export const RdtState = object({ loggedInTimestamp: string(), walletData: WalletData, diff --git a/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts b/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts index 23d5c36d..63534677 100644 --- a/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts +++ b/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts @@ -5,12 +5,17 @@ import { beforeEach, describe, expect, it } from 'vitest' import { LocalStorageModule, StorageModule } from './local-storage.module' import { ResultAsync } from 'neverthrow' +import { EnvironmentModule } from '../environment' describe('LocalStorageModule', () => { let storageModule: StorageModule beforeEach(() => { - storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`) + storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`, { + providers: { + environmentModule: EnvironmentModule(), + }, + }) }) it('should store and read data', async () => { diff --git a/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts b/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts index 888a8204..d3703ae6 100644 --- a/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts +++ b/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts @@ -1,6 +1,7 @@ import { err, Ok, ok, Result, ResultAsync } from 'neverthrow' import { typedError, parseJSON, stringify } from '../../helpers' import { filter, fromEvent, map, merge, mergeMap, of } from 'rxjs' +import { EnvironmentModule } from '../environment' type NetworkId = number type PartitionKey = @@ -8,7 +9,6 @@ type PartitionKey = | 'identities' | 'requests' | 'state' - | 'connectButton' | 'walletResponses' | 'connectorExtension' type dAppDefinitionAddress = string @@ -25,11 +25,18 @@ export type StorageModule = ReturnType< > export const LocalStorageModule = ( - key: `rdt:${dAppDefinitionAddress}:${NetworkId}`, - partitionKey?: PartitionKey, + storageKey: + | `rdt:${dAppDefinitionAddress}:${NetworkId}` + | `rdt:${dAppDefinitionAddress}:${NetworkId}${string}`, + { + providers, + }: { + providers: { + environmentModule: EnvironmentModule + } + }, ) => { - const storageKey = partitionKey ? `${key}:${partitionKey}` : key - + const _window = providers.environmentModule.globalThis const getDataAsync = (): Promise => new Promise((resolve, reject) => { try { @@ -72,7 +79,7 @@ export const LocalStorageModule = ( setDataAsync(serialized), typedError, ).map(() => { - window.dispatchEvent( + _window.dispatchEvent( new StorageEvent('storage', { key: storageKey, oldValue: JSON.stringify(items), @@ -98,7 +105,7 @@ export const LocalStorageModule = ( setDataAsync(serialized), typedError, ).map(() => { - window.dispatchEvent( + _window.dispatchEvent( new StorageEvent('storage', { key: storageKey, oldValue: JSON.stringify(data), @@ -121,7 +128,7 @@ export const LocalStorageModule = ( setDataAsync(serialized), typedError, ).map(() => { - window.dispatchEvent( + _window.dispatchEvent( new StorageEvent('storage', { key: storageKey, oldValue: JSON.stringify(oldValue), @@ -148,10 +155,10 @@ export const LocalStorageModule = ( ) const getPartition = (partitionKey: PartitionKey) => - LocalStorageModule(key, partitionKey) + LocalStorageModule(`${storageKey}:${partitionKey}`, { providers }) const storage$ = merge( - fromEvent(window, 'storage'), + fromEvent(providers.environmentModule.globalThis, 'storage'), of({ key: storageKey, newValue: null, oldValue: null }), ).pipe( filter((item) => item.key === storageKey), diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/accounts.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/accounts.ts index 4718b8fc..9406740e 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/accounts.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/accounts.ts @@ -1,5 +1,5 @@ import { produce } from 'immer' -import { boolean, object, Output, optional } from 'valibot' +import { boolean, object, InferOutput, optional } from 'valibot' import { NumberOfValues } from '../../../../schemas' export type AccountsRequestBuilder = { @@ -13,7 +13,7 @@ export type OneTimeAccountsRequestBuilder = { exactly: (n: number) => OneTimeAccountsRequestBuilder withProof: (value?: boolean) => OneTimeAccountsRequestBuilder } -export type AccountsDataRequest = Output +export type AccountsDataRequest = InferOutput export const AccountsDataRequestSchema = object({ numberOfAccounts: NumberOfValues, diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/index.ts index 9763d609..32e53b74 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/index.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/index.ts @@ -11,6 +11,7 @@ import { PersonaDataRequestBuilder, personaData, } from './persona-data' +import { proofOfOwnership, ProofOfOwnershipRequest, ProofOfOwnershipRequestBuilder } from './proof-of-ownership' export type DataRequestBuilderItem = | AccountsRequestBuilder @@ -21,12 +22,14 @@ export type DataRequestBuilderItem = export type OneTimeDataRequestBuilderItem = | OneTimeAccountsRequestBuilder | OneTimePersonaDataRequestBuilder + | ProofOfOwnershipRequestBuilder -export type DataRequestState = Partial< - { accounts: AccountsDataRequest } & { personaData: PersonaDataRequest } & { - persona: PersonaRequest - } -> +export type DataRequestState = Partial<{ + accounts: AccountsDataRequest + personaData: PersonaDataRequest + persona: PersonaRequest + proofOfOwnership: ProofOfOwnershipRequest +}> export type ConfigRequestBuilder = {} @@ -57,9 +60,11 @@ export const DataRequestBuilder: DataRequestBuilder = { export type OneTimeDataRequestBuilder = { accounts: () => OneTimeAccountsRequestBuilder personaData: (input?: PersonaDataRequest) => OneTimePersonaDataRequestBuilder + proofOfOwnership: () => ProofOfOwnershipRequestBuilder } export const OneTimeDataRequestBuilder: OneTimeDataRequestBuilder = { accounts, personaData, + proofOfOwnership, } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona-data.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona-data.ts index e9ca6f1f..85b1a07f 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona-data.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona-data.ts @@ -1,5 +1,5 @@ import { produce } from 'immer' -import { boolean, object, Output, partial } from 'valibot' +import { boolean, object, InferOutput, partial } from 'valibot' import { NumberOfValues } from '../../../../schemas' export type PersonaDataRequestBuilder = { @@ -13,7 +13,7 @@ export type OneTimePersonaDataRequestBuilder = { emailAddresses: (value?: boolean) => PersonaDataRequestBuilder phoneNumbers: (value?: boolean) => PersonaDataRequestBuilder } -export type PersonaDataRequest = Output +export type PersonaDataRequest = InferOutput export const PersonaDataRequestSchema = partial( object({ diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona.ts index 13247fe5..3279fd3d 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/persona.ts @@ -1,10 +1,10 @@ import { produce } from 'immer' -import { boolean, object, Output, optional } from 'valibot' +import { boolean, object, InferOutput, optional } from 'valibot' export type PersonaRequestBuilder = { withProof: (value?: boolean) => PersonaRequestBuilder } -export type PersonaRequest = Output +export type PersonaRequest = InferOutput const schema = object({ withProof: optional(boolean()), diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/proof-of-ownership.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/proof-of-ownership.ts new file mode 100644 index 00000000..884e4234 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/builders/proof-of-ownership.ts @@ -0,0 +1,43 @@ +import { produce } from 'immer' +import { object, InferOutput, string, array, optional } from 'valibot' + +export type ProofOfOwnershipRequestBuilder = { + accounts: (value: string[]) => ProofOfOwnershipRequestBuilder + identity: (value: string) => ProofOfOwnershipRequestBuilder +} +export type ProofOfOwnershipRequest = InferOutput + +const schema = object({ + accountAddresses: optional(array(string())), + identityAddress: optional(string()), +}) + +export const proofOfOwnership = (initialData: ProofOfOwnershipRequest = {}) => { + let data: ProofOfOwnershipRequest = produce(initialData, () => {}) + + const accounts = (value: string[]) => { + data = produce(data, (draft) => { + draft.accountAddresses = value + }) + return methods + } + + const identity = (value: string) => { + data = produce(data, (draft) => { + draft.identityAddress = value + }) + return methods + } + + const _toObject = (): { proofOfOwnership: ProofOfOwnershipRequest } => ({ + proofOfOwnership: data, + }) + + const methods = { + accounts, + identity, + _toObject, + } + + return methods +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/data-request-state.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/data-request-state.spec.ts index d3de0502..1e6854a9 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/data-request-state.spec.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/data-request-state.spec.ts @@ -1,3 +1,4 @@ +import { DataRequestBuilder, OneTimeDataRequestBuilder } from './builders' import { accounts } from './builders/accounts' import { persona } from './builders/persona' import { personaData } from './builders/persona-data' @@ -17,6 +18,71 @@ describe('DataRequestStateModule', () => { }) }) + describe('patchState then removeState', () => { + it('should add state then remove state', () => { + dataRequest.patchState( + DataRequestBuilder.persona().withProof(), + DataRequestBuilder.personaData().fullName(), + ) + + expect(dataRequest.getState()).toEqual({ + accounts: { + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + reset: false, + withProof: false, + }, + persona: { + withProof: true, + }, + personaData: { + fullName: true, + }, + }) + dataRequest.removeState('persona', 'accounts') + expect(dataRequest.getState()).toEqual({ + personaData: { + fullName: true, + }, + }) + }) + }) + + describe('reset', () => { + it('should set initial state', () => { + dataRequest.patchState(DataRequestBuilder.persona().withProof()) + dataRequest.reset() + expect(dataRequest.getState()).toEqual({ + accounts: { + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + reset: false, + withProof: false, + }, + }) + }) + }) + + describe('toDataRequestState', () => { + it('should consume one time data request builder proofOfOwnership', () => { + const state = dataRequest.toDataRequestState( + OneTimeDataRequestBuilder.proofOfOwnership() + .identity('identity_abc') + .accounts(['account_abc']), + ) + expect(state).toEqual({ + proofOfOwnership: { + identityAddress: 'identity_abc', + accountAddresses: ['account_abc'], + }, + }) + }) + }) + it('should be instantiated with default values', () => { expect(dataRequest.getState()).toEqual({ accounts: { diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.spec.ts new file mode 100644 index 00000000..9edf9747 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.spec.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest' +import { toWalletRequest } from './to-wallet-request' + +describe('toWalletRequest', () => { + it('should transform data request to wallet request', () => { + const testCases: [any, any][] = [ + [ + { + isConnect: true, + oneTime: false, + dataRequestState: {}, + walletData: { + accounts: [], + personaData: [], + proofs: [], + }, + }, + { + auth: { + discriminator: 'loginWithoutChallenge', + }, + discriminator: 'authorizedRequest', + reset: { + accounts: false, + personaData: false, + }, + }, + ], + [ + { + isConnect: false, + oneTime: true, + challenge: 'abc', + dataRequestState: { + proofOfOwnership: {}, + accounts: {}, + }, + walletData: { + persona: {}, + }, + }, + { + auth: { + discriminator: 'loginWithoutChallenge', + }, + discriminator: 'authorizedRequest', + oneTimeAccounts: { + challenge: undefined, + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + }, + proofOfOwnership: { + challenge: 'abc', + }, + reset: { + accounts: false, + personaData: false, + }, + }, + ], + [ + { + isConnect: true, + oneTime: false, + challenge: 'abc', + dataRequestState: { + accounts: { + withProof: true, + }, + personaData: { + reset: true, + }, + persona: { + withProof: true, + }, + }, + walletData: { + persona: {}, + }, + }, + { + auth: { + challenge: 'abc', + discriminator: 'loginWithChallenge', + }, + discriminator: 'authorizedRequest', + ongoingAccounts: { + challenge: 'abc', + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + }, + ongoingPersonaData: { + isRequestingName: undefined, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + reset: { + accounts: false, + personaData: false, + }, + }, + ], + ] + + testCases.forEach(([input, expected]) => { + const result = toWalletRequest(input) + expect(result.isOk() && result.value).toEqual(expected) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.ts index f6c8206c..ea9ada8b 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/helpers/to-wallet-request.ts @@ -20,6 +20,13 @@ export const toWalletRequest = ({ transformRdtDataRequestToWalletRequest( isConnect, produce({}, (draft: TransformRdtDataRequestToWalletRequestInput) => { + if (dataRequestState.proofOfOwnership) { + draft.proofOfOwnership = { + ...dataRequestState.proofOfOwnership, + challenge, + } + } + if (dataRequestState.accounts) { draft.accounts = { numberOfAccounts: dataRequestState.accounts.numberOfAccounts || { @@ -41,7 +48,7 @@ export const toWalletRequest = ({ oneTime, } - if (!oneTime) { + if (!oneTime || dataRequestState.proofOfOwnership) { const persona = walletData.persona if (walletData.persona) draft.persona = persona diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/fixtures/wallet-interactions.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/fixtures/wallet-interactions.ts new file mode 100644 index 00000000..0ac656cb --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/fixtures/wallet-interactions.ts @@ -0,0 +1,193 @@ +import { WalletDataRequestResponse } from '../wallet-to-rdt' + +export const walletSuccessResponseToAuthorizedRequest = { + discriminator: 'authorizedRequest', + auth: { + discriminator: 'loginWithChallenge', + persona: { + identityAddress: + 'identity_tdx_2_12twas58v4sthsmuky5653dup0drez3vcfwsfm6kp40qu9qyt8fgts6', + label: 'Usdudh', + }, + challenge: + '069ef236486d4cd5706b5e5b168e19f750ffd1b4876529a0a9de966d50a15ab7', + proof: { + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + curve: 'curve25519', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + }, + oneTimeAccounts: { + proofs: [ + { + proof: { + signature: + '90c33d0ded5db913edc3c86e1ddf553acaefe17f115419081b3d82ca160289bc938c5246f28384de28250fe658b08ac17b4f531c3c7c9346c1867c1ac67d1402', + publicKey: + 'eecf6843076e36faa896d13a95886dd2ace6c1ed84c173d4ca757c6674234e9f', + curve: 'curve25519', + }, + accountAddress: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + }, + ], + challenge: + 'fee6ba63de936007c22465a43e91283a222c9168fab12236bd131b1a0010b8a0', + accounts: [ + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + label: 'A', + appearanceId: 0, + }, + ], + }, + ongoingAccounts: { + accounts: [ + { + address: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + label: 'Spending Account', + appearanceId: 0, + }, + { + address: + 'account_tdx_2_128928hvf6pjr3rx2xvdw6ulf7pc8g88ya8ma3j8dtjmntckz09fr3n', + label: 'Savings Account', + appearanceId: 1, + }, + ], + challenge: + '069ef236486d4cd5706b5e5b168e19f750ffd1b4876529a0a9de966d50a15ab7', + proofs: [ + { + proof: { + publicKey: + '11b162e3343ce770b6e9ed8a29d125b5580d1272b0dc4e2bd0fcae33320d9566', + curve: 'curve25519', + signature: + 'e18617b527d4d33607a8adb6a040c26ca97642ec89dd8a6fe7a41fa724473e4cc69b0729c1df57aba77455801f2eef6f28848a5d206e3739de29ca2288957502', + }, + accountAddress: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + }, + { + proof: { + publicKey: + '5386353e4cc27e3d27d064d777d811e242a16ba7aefd425062ed46631739619d', + curve: 'curve25519', + signature: + '0143fd941d51f531c8265b0f6b24f4cfcdfd24b40aac47dee6fb3386ce0d400563c892e3894a33840d1c7af2dd43ecd0729fd209171003765d109a04d7485605', + }, + accountAddress: + 'account_tdx_2_128928hvf6pjr3rx2xvdw6ulf7pc8g88ya8ma3j8dtjmntckz09fr3n', + }, + ], + }, + ongoingPersonaData: { + name: { + variant: 'western', + familyName: 'Family', + givenNames: 'Given', + nickname: 'Nick', + }, + emailAddresses: ['some@gmail.com'], + phoneNumbers: ['071234579'], + }, +} satisfies WalletDataRequestResponse + +export const walletSuccessResponseToUnauthorizedRequest = { + oneTimeAccounts: { + accounts: [ + { + address: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + label: 'Spending Account', + appearanceId: 0, + }, + ], + }, + discriminator: 'unauthorizedRequest', + oneTimePersonaData: { + name: { + variant: 'western', + familyName: 'Family', + givenNames: 'Given', + nickname: 'Nick', + }, + emailAddresses: ['some@gmail.com'], + phoneNumbers: ['071234579'], + }, +} satisfies WalletDataRequestResponse + +export const walletSuccessResponseToProofOfOwnershipRequest = { + discriminator: 'authorizedRequest', + auth: { + discriminator: 'usePersona', + persona: { + identityAddress: + 'identity_tdx_2_12fat0nh0gymw9j4rqka5344p3h3r86x4z0hkw2v78r03pt0kfv0qva', + label: 'pao13', + }, + }, + proofOfOwnership: { + challenge: + 'e280cfa39e1499f2862e59759cc2fc990cce28b70a7989324fe91c47814d0630', + proofs: [ + { + accountAddress: + 'account_tdx_2_12ytkalad6hfxamsz4a7r8tevz7ahurfj58dlp4phl4nca5hs0hpu90', + proof: { + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + curve: 'curve25519', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + }, + { + identityAddress: + 'identity_tdx_2_12fat0nh0gymw9j4rqka5344p3h3r86x4z0hkw2v78r03pt0kfv0qva', + proof: { + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + curve: 'curve25519', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + }, + ], + }, +} satisfies WalletDataRequestResponse + +export const walletSuccessResponseToUnauthorizedRequestWithChallengeForOneTimeAccount = + { + discriminator: 'unauthorizedRequest', + oneTimeAccounts: { + proofs: [ + { + proof: { + signature: + '90c33d0ded5db913edc3c86e1ddf553acaefe17f115419081b3d82ca160289bc938c5246f28384de28250fe658b08ac17b4f531c3c7c9346c1867c1ac67d1402', + publicKey: + 'eecf6843076e36faa896d13a95886dd2ace6c1ed84c173d4ca757c6674234e9f', + curve: 'curve25519', + }, + accountAddress: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + }, + ], + challenge: + 'fee6ba63de936007c22465a43e91283a222c9168fab12236bd131b1a0010b8a0', + accounts: [ + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + label: 'A', + appearanceId: 0, + }, + ], + }, + } satisfies WalletDataRequestResponse diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.spec.ts new file mode 100644 index 00000000..c07e6e90 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.spec.ts @@ -0,0 +1,208 @@ +import { describe, expect, it } from 'vitest' +import { transformRdtDataRequestToWalletRequest } from './rdt-to-wallet' + +describe('transformRdtDataRequestToWalletRequest', () => { + describe('given is connect request', () => { + it('should transform RDT data request to wallet request', async () => { + const result = await transformRdtDataRequestToWalletRequest(true, { + proofOfOwnership: { + challenge: 'challenge', + accountAddresses: ['account_'], + identityAddress: 'identity_', + }, + accounts: { + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + challenge: 'challenge', + oneTime: false, + reset: true, + }, + personaData: { + fullName: true, + reset: true, + }, + }) + + expect(result.isOk() && result.value).toEqual({ + auth: { + discriminator: 'loginWithoutChallenge', + }, + discriminator: 'authorizedRequest', + ongoingAccounts: { + challenge: 'challenge', + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + }, + ongoingPersonaData: { + isRequestingName: true, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + proofOfOwnership: { + accountAddresses: ['account_'], + challenge: 'challenge', + identityAddress: 'identity_', + }, + reset: { + accounts: false, + personaData: false, + }, + }) + }) + + it('should produce correct result', async () => { + const result = await transformRdtDataRequestToWalletRequest(true, { + accounts: { + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + challenge: 'challenge', + oneTime: false, + reset: true, + }, + personaData: { + fullName: true, + reset: false, + }, + persona: { + identityAddress: 'identity_', + label: 'label', + }, + }) + + expect(result.isOk() && result.value).toEqual({ + auth: { + discriminator: 'usePersona', + identityAddress: 'identity_', + }, + discriminator: 'authorizedRequest', + ongoingAccounts: { + challenge: 'challenge', + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + }, + ongoingPersonaData: { + isRequestingName: true, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + reset: { + accounts: false, + personaData: false, + }, + }) + }) + + it('should produce correct result', async () => { + const result = await transformRdtDataRequestToWalletRequest(true, { + accounts: { + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + challenge: 'challenge', + oneTime: false, + reset: true, + }, + personaData: { + fullName: true, + reset: false, + }, + persona: { + identityAddress: 'identity_', + label: 'label', + }, + }) + + expect(result.isOk() && result.value).toEqual({ + auth: { + discriminator: 'usePersona', + identityAddress: 'identity_', + }, + discriminator: 'authorizedRequest', + ongoingAccounts: { + challenge: 'challenge', + numberOfAccounts: { + quantifier: 'atLeast', + quantity: 1, + }, + }, + ongoingPersonaData: { + isRequestingName: true, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + reset: { + accounts: false, + personaData: false, + }, + }) + }) + }) + + describe('given is not connect request', () => { + it('should transform RDT data request to wallet request', async () => { + const result = await transformRdtDataRequestToWalletRequest(false, { + persona: { + challenge: 'abc', + }, + }) + expect(result.isOk() && result.value).toEqual({ + auth: { + challenge: 'abc', + discriminator: 'loginWithChallenge', + }, + discriminator: 'authorizedRequest', + reset: { + accounts: false, + personaData: false, + }, + }) + }) + + it('should transform RDT data request to wallet request', async () => { + const result = await transformRdtDataRequestToWalletRequest(false, {}) + expect(result.isOk() && result.value).toEqual({ + discriminator: 'unauthorizedRequest', + }) + }) + + it('should transform RDT data request to wallet request', async () => { + const result = await transformRdtDataRequestToWalletRequest(false, { + personaData: { + oneTime: true, + fullName: true, + reset: true, + }, + }) + expect(result.isOk() && result.value).toEqual({ + discriminator: 'authorizedRequest', + auth: { + discriminator: 'loginWithoutChallenge', + }, + + oneTimePersonaData: { + isRequestingName: true, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + ongoingPersonaData: { + isRequestingName: true, + numberOfRequestedEmailAddresses: undefined, + numberOfRequestedPhoneNumbers: undefined, + }, + reset: { + accounts: false, + personaData: true, + }, + }) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.ts index 3e75be74..333d2321 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/rdt-to-wallet.ts @@ -9,12 +9,19 @@ import { NumberOfValues } from '../../../../schemas' import { produce } from 'immer' import type { Result } from 'neverthrow' import { ok } from 'neverthrow' -import { boolean, object, string, Output, optional } from 'valibot' +import { boolean, object, string, InferOutput, optional, array } from 'valibot' -export type TransformRdtDataRequestToWalletRequestInput = Output< +export type TransformRdtDataRequestToWalletRequestInput = InferOutput< typeof TransformRdtDataRequestToWalletRequestInput > export const TransformRdtDataRequestToWalletRequestInput = object({ + proofOfOwnership: optional( + object({ + challenge: optional(string()), + accountAddresses: optional(array(string())), + identityAddress: optional(string()), + }), + ), accounts: optional( object({ numberOfAccounts: NumberOfValues, @@ -44,7 +51,7 @@ export const TransformRdtDataRequestToWalletRequestInput = object({ const isAuthorized = ( input: TransformRdtDataRequestToWalletRequestInput, ): boolean => { - const { persona, accounts, personaData } = input + const { persona, accounts, personaData, proofOfOwnership } = input const isPersonaLogin = !!persona const shouldResetData = accounts?.reset || personaData?.reset @@ -55,8 +62,9 @@ const isAuthorized = ( shouldResetData || isOngoingAccountsRequest || isOngoingPersonaDataRequest || - isPersonaLogin - ) + isPersonaLogin || + proofOfOwnership + ) return isAuthorizedRequest } @@ -112,6 +120,36 @@ const withAccountRequestItem = return updatedRequestItems } +const withProofOfOwnershipRequestItem = + (input: TransformRdtDataRequestToWalletRequestInput) => + ( + requestItems: T, + ) => { + const updatedRequestItems = { ...requestItems } + + if (input.proofOfOwnership) { + const { challenge, accountAddresses, identityAddress } = + input.proofOfOwnership + + if (challenge && updatedRequestItems.discriminator === 'authorizedRequest') { + updatedRequestItems['proofOfOwnership'] = { + challenge, + } + if (accountAddresses) { + updatedRequestItems['proofOfOwnership'].accountAddresses = + accountAddresses + } + + if (identityAddress) { + updatedRequestItems['proofOfOwnership'].identityAddress = + identityAddress + } + } + } + + return updatedRequestItems + } + const withPersonaDataRequestItem = (input: TransformRdtDataRequestToWalletRequestInput) => ( @@ -170,6 +208,7 @@ const createUnauthorizedRequestItems = ( }) .map(withAccountRequestItem(input)) .map(withPersonaDataRequestItem(input)) + .map(withProofOfOwnershipRequestItem(input)) const createAuthorizedRequestItems = ( input: TransformRdtDataRequestToWalletRequestInput, @@ -181,6 +220,7 @@ const createAuthorizedRequestItems = ( .map(withAccountRequestItem(input)) .map(withPersonaDataRequestItem(input)) .map(withResetRequestItem(input)) + .map(withProofOfOwnershipRequestItem(input)) const transformConnectRequest = ( isConnect: boolean, diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.spec.ts new file mode 100644 index 00000000..3f08473c --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.spec.ts @@ -0,0 +1,249 @@ +import { describe, expect, it } from 'vitest' +import { transformWalletResponseToRdtWalletData } from './wallet-to-rdt' +import { + walletSuccessResponseToAuthorizedRequest, + walletSuccessResponseToProofOfOwnershipRequest, + walletSuccessResponseToUnauthorizedRequest, + walletSuccessResponseToUnauthorizedRequestWithChallengeForOneTimeAccount, +} from './fixtures/wallet-interactions' + +describe('transformWalletResponseToRdtWalletData', () => { + describe('given success response for login request and ongoing access to accounts and persona data', () => { + it('should produce correct wallet data', async () => { + const result = await transformWalletResponseToRdtWalletData( + walletSuccessResponseToAuthorizedRequest, + ) + + expect(result.isOk()).toBe(true) + expect(result.isOk() && result.value).toEqual({ + accounts: [ + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + appearanceId: 0, + label: 'A', + }, + { + address: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + appearanceId: 0, + label: 'Spending Account', + }, + { + address: + 'account_tdx_2_128928hvf6pjr3rx2xvdw6ulf7pc8g88ya8ma3j8dtjmntckz09fr3n', + appearanceId: 1, + label: 'Savings Account', + }, + ], + persona: { + identityAddress: + 'identity_tdx_2_12twas58v4sthsmuky5653dup0drez3vcfwsfm6kp40qu9qyt8fgts6', + label: 'Usdudh', + }, + personaData: [ + { + entry: 'fullName', + fields: { + familyName: 'Family', + givenNames: 'Given', + nickname: 'Nick', + variant: 'western', + }, + }, + { + entry: 'emailAddresses', + fields: ['some@gmail.com'], + }, + { + entry: 'phoneNumbers', + fields: ['071234579'], + }, + ], + proofs: [ + { + address: + 'identity_tdx_2_12twas58v4sthsmuky5653dup0drez3vcfwsfm6kp40qu9qyt8fgts6', + challenge: + '069ef236486d4cd5706b5e5b168e19f750ffd1b4876529a0a9de966d50a15ab7', + proof: { + curve: 'curve25519', + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + type: 'persona', + }, + { + address: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + challenge: + '069ef236486d4cd5706b5e5b168e19f750ffd1b4876529a0a9de966d50a15ab7', + proof: { + curve: 'curve25519', + publicKey: + '11b162e3343ce770b6e9ed8a29d125b5580d1272b0dc4e2bd0fcae33320d9566', + signature: + 'e18617b527d4d33607a8adb6a040c26ca97642ec89dd8a6fe7a41fa724473e4cc69b0729c1df57aba77455801f2eef6f28848a5d206e3739de29ca2288957502', + }, + type: 'account', + }, + { + address: + 'account_tdx_2_128928hvf6pjr3rx2xvdw6ulf7pc8g88ya8ma3j8dtjmntckz09fr3n', + challenge: + '069ef236486d4cd5706b5e5b168e19f750ffd1b4876529a0a9de966d50a15ab7', + proof: { + curve: 'curve25519', + publicKey: + '5386353e4cc27e3d27d064d777d811e242a16ba7aefd425062ed46631739619d', + signature: + '0143fd941d51f531c8265b0f6b24f4cfcdfd24b40aac47dee6fb3386ce0d400563c892e3894a33840d1c7af2dd43ecd0729fd209171003765d109a04d7485605', + }, + type: 'account', + }, + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + challenge: + 'fee6ba63de936007c22465a43e91283a222c9168fab12236bd131b1a0010b8a0', + proof: { + curve: 'curve25519', + publicKey: + 'eecf6843076e36faa896d13a95886dd2ace6c1ed84c173d4ca757c6674234e9f', + signature: + '90c33d0ded5db913edc3c86e1ddf553acaefe17f115419081b3d82ca160289bc938c5246f28384de28250fe658b08ac17b4f531c3c7c9346c1867c1ac67d1402', + }, + type: 'account', + }, + ], + }) + }) + }) + + describe('given success response for unathorized request', () => { + it('should produce correct wallet data', async () => { + const result = await transformWalletResponseToRdtWalletData( + walletSuccessResponseToUnauthorizedRequest, + ) + expect(result.isOk()).toBe(true) + expect(result.isOk() && result.value).toEqual({ + accounts: [ + { + address: + 'account_tdx_2_129qeystv8tufmkmjrry2g6kadhhfh4f7rd0x3t9yagcvfhspt62paz', + appearanceId: 0, + label: 'Spending Account', + }, + ], + persona: undefined, + personaData: [ + { + entry: 'fullName', + fields: { + familyName: 'Family', + givenNames: 'Given', + nickname: 'Nick', + variant: 'western', + }, + }, + { + entry: 'emailAddresses', + fields: ['some@gmail.com'], + }, + { + entry: 'phoneNumbers', + fields: ['071234579'], + }, + ], + proofs: [], + }) + }) + }) + + describe('given success response for authorized request with proof of ownership', () => { + it('should produce correct wallet data', async () => { + const result = await transformWalletResponseToRdtWalletData( + walletSuccessResponseToProofOfOwnershipRequest, + ) + expect(result.isOk()).toBe(true) + expect(result.isOk() && result.value).toEqual({ + accounts: [], + persona: { + identityAddress: + 'identity_tdx_2_12fat0nh0gymw9j4rqka5344p3h3r86x4z0hkw2v78r03pt0kfv0qva', + label: 'pao13', + }, + personaData: [], + proofs: [ + { + address: + 'account_tdx_2_12ytkalad6hfxamsz4a7r8tevz7ahurfj58dlp4phl4nca5hs0hpu90', + challenge: + 'e280cfa39e1499f2862e59759cc2fc990cce28b70a7989324fe91c47814d0630', + proof: { + curve: 'curve25519', + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + type: 'account', + }, + { + address: + 'identity_tdx_2_12fat0nh0gymw9j4rqka5344p3h3r86x4z0hkw2v78r03pt0kfv0qva', + challenge: + 'e280cfa39e1499f2862e59759cc2fc990cce28b70a7989324fe91c47814d0630', + proof: { + curve: 'curve25519', + publicKey: + 'ff8aee4c625738e35d837edb11e33b8abe0d6f40849ca1451edaba84d04d0699', + signature: + '10177ac7d486691777133ffe59d46d55529d86cb1c4ce66aa82f432372f33e24d803d8498f42e26fe113c030fce68c526aeacff94334ba5a7f7ef84c2936eb05', + }, + type: 'persona', + }, + ], + }) + }) + }) + + describe('given success response for unauthorized request with challenge for one time account', () => { + it('should produce correct wallet data', async () => { + const result = await transformWalletResponseToRdtWalletData( + walletSuccessResponseToUnauthorizedRequestWithChallengeForOneTimeAccount, + ) + expect(result.isOk()).toBe(true) + expect(result.isOk() && result.value).toEqual({ + accounts: [ + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + appearanceId: 0, + label: 'A', + }, + ], + persona: undefined, + personaData: [], + proofs: [ + { + address: + 'account_tdx_2_129qprkuarea8mrtklr06g5ghdzgd2z6um4x8cxgu94hkt28taeaqxf', + challenge: + 'fee6ba63de936007c22465a43e91283a222c9168fab12236bd131b1a0010b8a0', + proof: { + curve: 'curve25519', + publicKey: + 'eecf6843076e36faa896d13a95886dd2ace6c1ed84c173d4ca757c6674234e9f', + signature: + '90c33d0ded5db913edc3c86e1ddf553acaefe17f115419081b3d82ca160289bc938c5246f28384de28250fe658b08ac17b4f531c3c7c9346c1867c1ac67d1402', + }, + type: 'account', + }, + ], + }) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.ts b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.ts index cea15995..e2369883 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/data-request/transformations/wallet-to-rdt.ts @@ -2,11 +2,14 @@ import { produce } from 'immer' import { okAsync, type ResultAsync } from 'neverthrow' import type { Account, + AccountProof, PersonaDataRequestResponseItem, + PersonaProof, WalletAuthorizedRequestResponseItems, WalletUnauthorizedRequestResponseItems, } from '../../../../schemas' import { + SignedChallenge, type SignedChallengeAccount, type WalletData, proofType, @@ -62,6 +65,24 @@ const withPersonaDataEntries = ( return entries } +const convertOwnershipProofsToSignedChallenge = ( + challenge: string, + proofs: (PersonaProof | AccountProof)[], +): SignedChallenge[] => { + return proofs.map((proof) => { + const type = + 'identityAddress' in proof ? proofType.persona : proofType.account + const address = + 'identityAddress' in proof ? proof.identityAddress : proof.accountAddress + return { + type, + challenge, + address, + proof: proof.proof, + } + }) +} + const withPersonaData = (input: WalletDataRequestResponse) => (walletData: WalletData) => produce(walletData, (draft) => { @@ -129,6 +150,15 @@ const withProofs = ) draft.proofs.push(...accountProofs) } + + if (input.proofOfOwnership) { + draft.proofs.push( + ...convertOwnershipProofsToSignedChallenge( + input.proofOfOwnership.challenge, + input.proofOfOwnership.proofs, + ), + ) + } } if (input.discriminator === 'unauthorizedRequest') { if ( diff --git a/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.spec.ts new file mode 100644 index 00000000..e94e98af --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { EncryptionModule } from './encryption.module' + +describe('encryption module', () => { + const encryptionModule = EncryptionModule() + + it('should encrypt data', async () => { + const data = Buffer.from('data') + const encryptionKey = Buffer.from('12345678901234567890123456789012') + const result = await encryptionModule + .encrypt(data, encryptionKey, encryptionModule.createIV()) + .map((value) => { + expect(value).toBeDefined() + }) + + expect(result.isOk()).toBeTruthy() + }) + + it('should return error for invalid key length', async () => { + const data = Buffer.from('data') + const encryptionKey = Buffer.from('key') + const result = await encryptionModule.encrypt( + data, + encryptionKey, + encryptionModule.createIV(), + ) + expect(result.isErr()).toBeTruthy() + }) + + it('should return 12 bytes IV', () => { + const iv = encryptionModule.createIV() + expect(iv.length).toBe(12) + }) + + it('should decrypt previously encrypted data', async () => { + const data = Buffer.from('data') + const encryptionKey = Buffer.from('12345678901234567890123456789012') + const iv = encryptionModule.createIV() + const result = await encryptionModule + .encrypt(data, encryptionKey, iv) + .andThen((sealed) => + encryptionModule.decrypt(sealed.ciphertext, encryptionKey, iv), + ) + .map((decrypted) => + expect(decrypted.toString('hex')).toEqual(data.toString('hex')), + ) + expect(result.isOk()).toBeTruthy() + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.ts index c8a9f540..62e59689 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/encryption/encryption.module.ts @@ -39,9 +39,14 @@ export const EncryptionModule = () => { typedError, ) - const combineIVandCipherText = (iv: Buffer, ciphertext: Buffer): Buffer => - Buffer.concat([iv, ciphertext]) - + /** + * Decrypts data using AES-GCM algorithm + * + * @param {Buffer} data - payload to be decrypted + * @param {Buffer} encryptionKey - key used for decryption + * @param {Buffer} iv - initialization vector used when data was encrypted + * @returns decrypted data wrapped inside ResultAsync + */ const decrypt = ( data: Buffer, encryptionKey: Buffer, @@ -51,6 +56,14 @@ export const EncryptionModule = () => { cryptoDecrypt(data, cryptoKey, iv), ) + /** + * Encrypts data using AES-GCM algorithm + * + * @param {Buffer} data - payload to be encrypted + * @param {Buffer} encryptionKey - key used for encryption + * @param {Buffer=} iv - optional initialization vector + * @returns encrypted data wrapped inside ResultAsync + */ const encrypt = ( data: Buffer, encryptionKey: Buffer, @@ -62,11 +75,15 @@ export const EncryptionModule = () => { getKey(encryptionKey) .andThen((cryptoKey) => cryptoEncrypt(data, cryptoKey, iv)) .map((ciphertext) => ({ - combined: combineIVandCipherText(iv, ciphertext), + combined: Buffer.concat([iv, ciphertext]), iv, ciphertext, })) + /** + * Creates 96 bits (12 bytes) initialization vector for AES-GCM encryption + * @returns {Buffer} 12 randomly generated bytes + */ const createIV = () => Buffer.from(crypto.getRandomValues(new Uint8Array(12))) return { encrypt, decrypt, createIV } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.spec.ts new file mode 100644 index 00000000..b9f78a73 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { transformBufferToSealbox } from './sealbox' + +describe('transformBufferToSealbox', () => { + it('should transform buffer to sealbox', async () => { + const buf = Buffer.from( + 'aaaaaaaaaaaaaaaaaaaaaaaaccbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'hex', + ) + const result = await transformBufferToSealbox(buf) + + expect(result.isOk() && JSON.stringify(result.value)).toEqual( + JSON.stringify({ + iv: Buffer.from('aaaaaaaaaaaaaaaaaaaaaaaa', 'hex'), + ciphertext: Buffer.from('cc', 'hex'), + authTag: Buffer.from('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'hex'), + combined: buf, + ciphertextAndAuthTag: Buffer.from( + 'ccbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'hex', + ), + }), + ) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.ts b/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.ts index b21c8016..b2b300bf 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/encryption/helpers/sealbox.ts @@ -3,26 +3,13 @@ import { Result } from 'neverthrow' import { readBuffer } from './buffer-reader' export type SealedBoxProps = { - ciphertext: Buffer iv: Buffer authTag: Buffer combined: Buffer + ciphertext: Buffer ciphertextAndAuthTag: Buffer } -const combineSealboxToBuffer = ({ - iv, - ciphertext, - authTag, -}: Pick): Buffer => - Buffer.concat([iv, ciphertext, authTag]) - -const combineCiphertextAndAuthtag = ({ - ciphertext, - authTag, -}: Pick): Buffer => - Buffer.concat([ciphertext, authTag]) - export const transformBufferToSealbox = ( buffer: Buffer, ): Result => { @@ -39,10 +26,7 @@ export const transformBufferToSealbox = ( iv, ciphertext, authTag, - combined: combineSealboxToBuffer({ iv, ciphertext, authTag }), - ciphertextAndAuthTag: combineCiphertextAndAuthtag({ - ciphertext, - authTag, - }), + combined: Buffer.concat([iv, ciphertext, authTag]), + ciphertextAndAuthTag: Buffer.concat([ciphertext, authTag]), })) } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts b/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.spec.ts similarity index 100% rename from packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts rename to packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.spec.ts diff --git a/packages/dapp-toolkit/src/modules/wallet-request/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/index.ts index 2db1f238..9ba1455d 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/index.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/index.ts @@ -6,3 +6,4 @@ export * from './session/session.module' export * from './transport' export * from './wallet-request-sdk' export * from './wallet-request' +export * from './pre-authorization-request' diff --git a/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/index.ts new file mode 100644 index 00000000..7b48a2e5 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/index.ts @@ -0,0 +1 @@ +export * from './subintent-builder' diff --git a/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/preauthorization-polling-module.ts b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/preauthorization-polling-module.ts new file mode 100644 index 00000000..6038385a --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/preauthorization-polling-module.ts @@ -0,0 +1,110 @@ +import { ResultAsync } from 'neverthrow' +import { RequestItem, RequestStatus } from 'radix-connect-common' +import { Logger } from '../../../helpers' +import { GatewayModule } from '../../gateway' +import { RequestItemModule } from '../request-items' +import { Subject, Subscription, tap } from 'rxjs' + +type ActivePolling = ReturnType + +export type PreauthorizationPollingModuleInput = { + logger?: Logger + providers: { + gatewayModule: GatewayModule + requestItemModule: RequestItemModule + ignoreTransactionSubject: Subject + } +} +export type PreauthorizationPollingModule = ReturnType< + typeof PreauthorizationPollingModule +> +export const PreauthorizationPollingModule = ( + input: PreauthorizationPollingModuleInput, +) => { + const logger = input?.logger?.getSubLogger({ + name: 'PreauthorizationPolling', + }) + const { + providers: { requestItemModule, ignoreTransactionSubject }, + } = input + let shouldRun = true + const WAIT_TIME = 1_000 + const activePolling = new Map() + const subscriptions = new Subscription() + + subscriptions.add( + ignoreTransactionSubject + .pipe( + tap((id) => { + if (activePolling.has(id)) { + activePolling.get(id)?.stop() + activePolling.delete(id) + } + }), + ) + .subscribe(), + ) + + const preauthorizationPollingLoop = async () => { + await requestItemModule.getPendingCommit().andThen((lookedUpItems) => { + const timedOutItems: RequestItem[] = [] + const lookupItems: RequestItem[] = [] + + lookedUpItems.forEach((item) => { + if (Number(item.metadata?.expirationTimestamp) * 1000 < Date.now()) { + timedOutItems.push(item) + } else { + lookupItems.push(item) + } + }) + + lookupItems.forEach((item) => { + if (!activePolling.has(item.interactionId)) { + const polling = input.providers.gatewayModule.pollSubintentStatus( + item.transactionIntentHash!, + item.metadata?.expirationTimestamp as number, + ) + activePolling.set(item.interactionId, polling) + polling.result + .andTee((result) => + requestItemModule.updateStatus({ + id: item.interactionId, + status: RequestStatus.success, + metadata: { + parentTransactionIntentHash: result.transactionIntentHash, + }, + }), + ) + .mapErr(() => { + activePolling.delete(item.interactionId) + }) + } + }) + + return ResultAsync.combine( + timedOutItems.map((item) => { + if (activePolling.has(item.interactionId)) { + activePolling.get(item.interactionId)?.stop() + activePolling.delete(item.interactionId) + } + return requestItemModule.updateStatus({ + id: item.interactionId, + status: RequestStatus.timedOut, + }) + }), + ) + }) + + await new Promise((resolve) => setTimeout(resolve, WAIT_TIME)) + if (shouldRun) preauthorizationPollingLoop() + } + + preauthorizationPollingLoop() + + return { + destroy: () => { + shouldRun = false + subscriptions.unsubscribe() + }, + } +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.spec.ts new file mode 100644 index 00000000..f28b1709 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { SubintentRequestBuilder } from './subintent-builder' + +describe('SubintentRequestBuilder', () => { + it('should build a subintent request', () => { + const tx = SubintentRequestBuilder() + .manifest('...') + .setExpiration('afterDelay', 60) + .addBlobs('deadbeef', 'beefdead') + .message('hello') + .toRequestItem() + + expect(tx).toEqual({ + discriminator: 'subintent', + version: 1, + manifestVersion: 2, + subintentManifest: '...', + expiration: { + discriminator: 'expireAfterDelay', + expireAfterSeconds: 60, + }, + blobs: ['deadbeef', 'beefdead'], + message: 'hello', + }) + }) + + it('should build a subintent request with expiration at time', () => { + const tx = SubintentRequestBuilder() + .manifest('...') + .setExpiration('atTime', 1970) + .toRequestItem() + + expect(tx).toEqual({ + discriminator: 'subintent', + version: 1, + manifestVersion: 2, + subintentManifest: '...', + expiration: { + discriminator: 'expireAtTime', + unixTimestampSeconds: 1970, + }, + }) + }) + + it('should build a subintent request using raw object', () => { + const tx = SubintentRequestBuilder() + .rawConfig({ + version: 1, + manifestVersion: 2, + subintentManifest: '...', + expiration: { + discriminator: 'expireAfterDelay', + expireAfterSeconds: 60, + }, + blobs: ['deadbeef', 'beefdead'], + message: 'hello', + }) + .toRequestItem() + + expect(tx).toEqual({ + discriminator: 'subintent', + version: 1, + manifestVersion: 2, + subintentManifest: '...', + expiration: { + discriminator: 'expireAfterDelay', + expireAfterSeconds: 60, + }, + blobs: ['deadbeef', 'beefdead'], + message: 'hello', + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.ts b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.ts new file mode 100644 index 00000000..57c732da --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/pre-authorization-request/subintent-builder.ts @@ -0,0 +1,126 @@ +import { SubintentRequestItem } from '../../../schemas' + +export type BuildableSubintentRequest = { + toRequestItem: () => SubintentRequestItem + getOnSubmittedSuccessFn?: () => (transactionIntentHash: string) => void +} +/** + * A builder function for creating a SubintentRequest. + * + * @returns An object with methods to configure and build a SubintentRequestItem. + * + * @example + * const builder = SubintentRequestBuilder(); + * const requestItem = builder + * .manifest('some-manifest') + * .setExpiration('atTime', 1234567890) + * .addBlobs('blob1', 'blob2') + * .message('This is a message') + * .onSubmittedSuccess((transactionIntentHash) => console.log('Submitted successfully', transactionIntentHash)) + */ +export const SubintentRequestBuilder = () => { + let state: Partial = { + discriminator: 'subintent', + version: 1, + manifestVersion: 2, + } + let onSubmittedSuccessFn: (transactionIntentHash: string) => void + + /** + * Adds a callback to be called when the preauthorization is included in successfully committed on ledger transaction. + * This will be called with the committed transaction intent hash. + * + * @param callback - function to be called when the preauthorization is included in successfully committed on ledger transaction + * @returns The API object for chaining. + */ + const onSubmittedSuccess = ( + callback: (transactionIntentHash: string) => void, + ) => { + onSubmittedSuccessFn = callback + return api + } + + /** + * Sets the expiration for a request. + * + * @param type - The type of expiration. Can be 'atTime' for a specific time or 'afterDelay' for a duration after the signature. + * @param value - The value associated with the expiration type. For 'atTime', this is a timestamp. For 'afterDelay', this is the number of seconds. + * @returns The API object for chaining. + */ + const setExpiration = (type: 'atTime' | 'afterDelay', value: number) => { + state.expiration = + type === 'atTime' + ? { + discriminator: 'expireAtTime', + unixTimestampSeconds: value, + } + : { + discriminator: 'expireAfterDelay', + expireAfterSeconds: value, + } + return api + } + + /** + * Adds the provided blobs to the state. + * + * @param blobs - A list of blob strings to be added to the state. + * @returns The API object for chaining. + */ + const addBlobs = (...blobs: string[]) => { + state.blobs = blobs + return api + } + + /** + * Sets the message to be included in the subintent transaction. + * + * @param message - The message to be set in the state. + * @returns The API object for chaining further calls. + */ + const message = (message: string) => { + state.message = message + return api + } + + /** + * Sets the transaction manifest in the state and returns the API object. + * + * @param value - The transaction manifest to be set. + * @returns The API object for method chaining. + */ + const manifest = (value: string) => { + state.subintentManifest = value + return { setExpiration } + } + + /** + * Converts the current state to a SubintentRequestItem. + * + * @returns {SubintentRequestItem} The current state cast as a SubintentRequestItem. + */ + const toRequestItem = () => state as SubintentRequestItem + + /** + * Sets the raw configuration for the builder. + * + * @param rawConfig - The raw configuration to set. + * @returns The API object for method chaining. + */ + const rawConfig = ( + rawConfig: Omit, + ) => { + state = { ...rawConfig, discriminator: 'subintent' } + return { toRequestItem } + } + + const api = { + addBlobs, + message, + toRequestItem, + onSubmittedSuccess, + getOnSubmittedSuccessFn: () => onSubmittedSuccessFn, + } as const + + return { manifest, rawConfig } +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts index 33818a19..713dc6bb 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts @@ -1,7 +1,8 @@ +import { GatewayModule } from './../../gateway/gateway.module' import { RequestStatus, type RequestItem, - type RequestStatusTypes, + RequestStatusTypes, } from 'radix-connect-common' import { Subscription, filter, map, switchMap } from 'rxjs' import { Logger } from '../../../helpers' @@ -9,9 +10,13 @@ import { ErrorType } from '../../../error' import { WalletInteraction } from '../../../schemas' import type { StorageModule } from '../../storage' import { ResultAsync, errAsync } from 'neverthrow' +import { WalletData } from '../../state' export type RequestItemModuleInput = { logger?: Logger - providers: { storageModule: StorageModule } + providers: { + gatewayModule: GatewayModule + storageModule: StorageModule + } } export type RequestItemModule = ReturnType export const RequestItemModule = (input: RequestItemModuleInput) => { @@ -19,6 +24,8 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { const subscriptions = new Subscription() const storageModule = input.providers.storageModule + const signals = new Map void>() + const createItem = ({ type, walletInteraction, @@ -37,21 +44,36 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { isOneTimeRequest, }) - const add = (value: { - type: RequestItem['type'] - walletInteraction: WalletInteraction - isOneTimeRequest: boolean - }) => { + const add = ( + value: { + type: RequestItem['type'] + walletInteraction: WalletInteraction + isOneTimeRequest: boolean + }, + onSignal?: (signalValue: string) => void, + ) => { const item = createItem(value) logger?.debug({ method: 'addRequestItem', item, }) + if (onSignal) { + signals.set(item.interactionId, onSignal) + } + return storageModule .setItems({ [item.interactionId]: item }) .map(() => item) } + const getAndRemoveSignal = (interactionId: string) => { + if (signals.has(interactionId)) { + const signal = signals.get(interactionId) + signals.delete(interactionId) + return signal + } + } + const patch = (id: string, partialValue: Partial) => { logger?.debug({ method: 'patchRequestItemStatus', @@ -65,40 +87,57 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { return patch(id, { status: 'fail', error: ErrorType.canceledByUser }) } + const isWalletInteractionRequired = (status: RequestStatusTypes) => + ([RequestStatus.pending, RequestStatus.pendingCommit] as string[]).includes( + status, + ) + const updateStatus = ({ id, status, error, transactionIntentHash, + metadata = {}, + walletData, + walletResponse, }: { id: string status: RequestStatusTypes error?: string transactionIntentHash?: string + walletData?: WalletData + walletResponse?: any, + metadata?: Record }): ResultAsync => { return storageModule .getItemById(id) .mapErr(() => ({ reason: 'couldNotReadFromStore' })) .andThen((item) => { if (item) { + if (status === RequestStatus.ignored && signals.has(id)) { + signals.delete(id) + } + if (status === RequestStatus.success) { + const signal = getAndRemoveSignal(id) + signal?.(metadata?.parentTransactionIntentHash as string) + } const updated = { ...item, + walletData, + transactionIntentHash, + error, + walletResponse, status: item.status === RequestStatus.ignored ? item.status : status, + metadata: item.metadata + ? { ...item.metadata, ...metadata } + : metadata, } as RequestItem - if (updated.status === 'fail') { - updated.error = error! - } - if ( - updated.status === 'success' && - updated.type === 'sendTransaction' - ) { - updated.transactionIntentHash = transactionIntentHash! - } - if (['success', 'fail', 'ignored', 'cancelled'].includes(updated.status)) { + + if (!isWalletInteractionRequired(updated.status)) { delete updated.walletInteraction - delete updated.walletResponse } + logger?.debug({ method: 'updateRequestItemStatus', updated }) return storageModule .setItems({ [id]: updated }) @@ -108,6 +147,13 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { }) } + const getPendingCommit = () => + storageModule + .getItemList() + .map((items) => + items.filter((item) => item.status === RequestStatus.pendingCommit), + ) + const getPending = () => storageModule .getItemList() @@ -126,7 +172,9 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { cancel, updateStatus, patch, + getAndRemoveSignal, getById: (id: string) => storageModule.getItemById(id), + getPendingCommit, getPending, requests$, clear: storageModule.clear, diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/index.ts new file mode 100644 index 00000000..7ff8789d --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/index.ts @@ -0,0 +1,3 @@ +export * from './resolvers' +export * from './request-resolver.module' +export * from './type' diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts new file mode 100644 index 00000000..d2fe73d7 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts @@ -0,0 +1,186 @@ +import { err, ok, okAsync, Result, ResultAsync } from 'neverthrow' +import { validateWalletResponse, type Logger } from '../../../helpers' +import type { WalletInteractionResponse } from '../../../schemas' +import type { StorageModule } from '../../storage' +import type { RequestItemModule } from '../request-items' +import { SdkError } from '../../../error' +import { filter, firstValueFrom, map } from 'rxjs' +import { WalletResponseResolver } from './type' +import { RequestItem } from 'radix-connect-common' + +export type RequestResolverModule = ReturnType +export const RequestResolverModule = (input: { + logger?: Logger + providers: { + storageModule: StorageModule + requestItemModule: RequestItemModule + resolvers: WalletResponseResolver[] + } +}) => { + const WAIT_TIME = 1_000 + const { providers } = input + const { requestItemModule, storageModule, resolvers } = providers + const logger = input.logger?.getSubLogger({ name: 'RequestResolverModule' }) + + let shouldRun = true + + const walletResponses: StorageModule = + storageModule.getPartition('walletResponses') + + const getPendingRequests = () => + requestItemModule + .getPending() + .orElse((error) => { + logger?.error({ method: 'getPendingRequests', error }) + return ok([]) + }) + .andThen((pendingItems) => + pendingItems.length === 0 + ? err('PendingItemsNotFound') + : ok(pendingItems), + ) + + const getPendingRequestById = (interactionId: string) => + requestItemModule + .getById(interactionId) + .mapErr(() => SdkError('FailedToGetPendingItems', interactionId)) + .andThen((pendingItem) => + pendingItem?.status === 'pending' + ? ok(pendingItem) + : err(SdkError('PendingItemNotFound', interactionId)), + ) + + const getWalletResponseById = ( + interactionId: string, + ): ResultAsync => + requestItemModule + .getById(interactionId) + .mapErr(() => SdkError('FailedToGetWalletResponse', interactionId)) + .map((item) => item?.walletResponse) + + const markRequestAsSent = (interactionId: string) => + requestItemModule.patch(interactionId, { sentToWallet: true }) + + const addWalletResponses = (responses: WalletInteractionResponse[]) => + Result.combine(responses.map(validateWalletResponse)) + .asyncAndThen(() => + walletResponses.setItems( + responses.reduce>( + (acc, response) => { + acc[response.interactionId] = response + return acc + }, + {}, + ), + ), + ) + .mapErr((error) => logger?.error({ method: 'addWalletResponses', error })) + + const toRequestItemMap = (items: RequestItem[]) => + items.reduce>( + (acc, item) => ({ ...acc, [item.interactionId]: item }), + {}, + ) + + const matchRequestItemToResponses = ( + requestItems: Record, + ) => { + const ids = Object.keys(requestItems) + return walletResponses + .getItemList() + .map((responses) => + responses.filter((response) => ids.includes(response.interactionId)), + ) + .andThen((responses) => + responses.length ? ok(responses) : err('WalletResponsesNotFound'), + ) + .map((responses) => + responses.map((response) => ({ + walletInteractionResponse: response, + requestItem: requestItems[response.interactionId], + })), + ) + } + + const resolveRequests = ( + unresolvedRequests: { + walletInteractionResponse: WalletInteractionResponse + requestItem: RequestItem + }[], + ) => + ResultAsync.combine(unresolvedRequests.map(resolveRequest)).map( + () => unresolvedRequests, + ) + + // Remove data from RequestItem and WalletResponse that is no longer needed + const cleanup = (requestItems: RequestItem[]) => { + return okAsync(undefined) + } + + const resolveRequest = ({ + requestItem, + walletInteractionResponse, + }: { + walletInteractionResponse: WalletInteractionResponse + requestItem: RequestItem + }) => { + const { walletInteraction } = requestItem + + return ResultAsync.combine( + resolvers.map((resolver) => + resolver({ walletInteraction, walletInteractionResponse, requestItem }), + ), + ) + } + + const waitForWalletResponse = (interactionId: string) => + ResultAsync.fromSafePromise( + firstValueFrom( + requestItemModule.requests$.pipe( + filter((items) => + items.some( + (item) => + item.interactionId === interactionId && + item.status !== 'pending', + ), + ), + map( + (items) => + items.find((item) => item.interactionId === interactionId)!, + ), + ), + ), + ) + + const requestResolverLoop = async () => { + await getPendingRequests() + .map(toRequestItemMap) + .andThen(matchRequestItemToResponses) + .andThen(resolveRequests) + .map((unresolvedRequests) => + unresolvedRequests.map((item) => item.requestItem), + ) + .andThen(cleanup) + + await new Promise((resolve) => setTimeout(resolve, WAIT_TIME)) + + if (shouldRun) requestResolverLoop() + } + + requestResolverLoop() + + return { + waitForWalletResponse, + getPendingRequestById, + getPendingRequestIds: () => + getPendingRequests().map((items) => + items.map((item) => item.interactionId), + ), + markRequestAsSent, + addWalletResponses, + getWalletResponseById, + destroy: () => { + shouldRun = false + }, + } +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/data-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/data-response.ts new file mode 100644 index 00000000..3cd6b426 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/data-response.ts @@ -0,0 +1,128 @@ +import { err, okAsync, ResultAsync } from 'neverthrow' +import { + WalletInteraction, + WalletInteractionResponse, +} from '../../../../schemas' +import { + transformWalletRequestToSharedData, + transformWalletResponseToRdtWalletData, + WalletDataRequestResponse, +} from '../../data-request' +import { RequestItemModule } from '../../request-items' +import { StateModule, WalletData } from '../../../state' +import { SdkError } from '../../../../error' +import { UpdateConnectButtonStatus, WalletResponseResolver } from '../type' +import { RequestItem } from 'radix-connect-common' + +const matchResponse = ( + input: WalletInteractionResponse, +): WalletDataRequestResponse | undefined => { + if (input.discriminator === 'success') { + if ( + input.items.discriminator === 'authorizedRequest' || + input.items.discriminator === 'unauthorizedRequest' + ) { + return input.items + } + } +} + +type GetDataRequestController = () => + | undefined + | (( + walletData: WalletData, + ) => ResultAsync) + +const useDataRequestController = + (getDataRequestController: GetDataRequestController, interactionId: string) => + (walletData: WalletData) => { + const maybeDataRequestController = getDataRequestController() + + if (!maybeDataRequestController) return okAsync(walletData) + + return maybeDataRequestController(walletData) + .map(() => walletData) + .mapErr((error) => SdkError(error.error, interactionId, error.message)) + } + +const handleAuthorizedRequestResponse = ({ + requestItem, + walletInteraction, + walletData, + stateModule, +}: { + requestItem: RequestItem + walletInteraction: WalletInteraction + walletData: WalletData + stateModule: StateModule +}) => + stateModule + .getState() + .andThen((state) => + stateModule + .setState({ + loggedInTimestamp: + requestItem.type === 'loginRequest' + ? Date.now().toString() + : state!.loggedInTimestamp, + walletData, + sharedData: transformWalletRequestToSharedData( + walletInteraction, + state!.sharedData, + ), + }) + .andTee(() => stateModule.emitWalletData()), + ) + .orElse(() => + err(SdkError('FailedToUpdateRdtState', walletInteraction.interactionId)), + ) + +export const dataResponseResolver = + (dependencies: { + requestItemModule: RequestItemModule + getDataRequestController: GetDataRequestController + stateModule: StateModule + updateConnectButtonStatus: UpdateConnectButtonStatus + }): WalletResponseResolver => + ({ walletInteraction, walletInteractionResponse, requestItem }) => { + const dataResponse = matchResponse(walletInteractionResponse) + if (!dataResponse) return okAsync(undefined) + + const { requestItemModule, getDataRequestController, stateModule } = + dependencies + + const { interactionId } = walletInteraction + + return transformWalletResponseToRdtWalletData(dataResponse) + .andThen( + useDataRequestController(getDataRequestController, interactionId), + ) + .andThen((walletData) => + dataResponse.discriminator === 'authorizedRequest' + ? handleAuthorizedRequestResponse({ + requestItem, + walletInteraction, + walletData, + stateModule, + }).map(() => walletData) + : okAsync(walletData), + ) + .andThen((walletData) => + requestItemModule + .updateStatus({ + id: walletInteraction.interactionId, + status: 'success', + walletData, + walletResponse: walletInteractionResponse, + }) + .mapErr((error) => + SdkError(error.reason, walletInteraction.interactionId), + ), + ) + .andTee(() => dependencies.updateConnectButtonStatus('success')) + .orElse((error) => { + dependencies.updateConnectButtonStatus('fail') + return err(error) + }) + .map(() => undefined) + } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/failed-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/failed-response.ts new file mode 100644 index 00000000..4503f026 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/failed-response.ts @@ -0,0 +1,43 @@ +import { err, okAsync } from 'neverthrow' +import { + WalletInteractionFailureResponse, + WalletInteractionResponse, +} from '../../../../schemas' +import { RequestItemModule } from '../../request-items' +import { SdkError } from '../../../../error' +import { UpdateConnectButtonStatus, WalletResponseResolver } from '../type' + +const matchResponse = ( + input: WalletInteractionResponse, +): WalletInteractionFailureResponse | undefined => { + if (input.discriminator === 'failure') { + return input + } +} + +export const failedResponseResolver = + (dependencies: { + requestItemModule: RequestItemModule + updateConnectButtonStatus: UpdateConnectButtonStatus + }): WalletResponseResolver => + ({ walletInteraction, walletInteractionResponse }) => { + const failedResponse = matchResponse(walletInteractionResponse) + if (!failedResponse) return okAsync(undefined) + + const { interactionId } = walletInteraction + + const { requestItemModule } = dependencies + + return requestItemModule + .updateStatus({ + id: interactionId, + status: 'fail', + walletResponse: walletInteractionResponse, + }) + .orElse((error) => { + dependencies.updateConnectButtonStatus('fail') + return err(SdkError(error.reason, interactionId)) + }) + .andTee(() => dependencies.updateConnectButtonStatus('fail')) + .map(() => undefined) + } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/index.ts new file mode 100644 index 00000000..5ce32902 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/index.ts @@ -0,0 +1,4 @@ +export * from './data-response' +export * from './failed-response' +export * from './send-transaction-response' +export * from './pre-authorization-response' diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts new file mode 100644 index 00000000..b452dfa1 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts @@ -0,0 +1,55 @@ +import { err, okAsync } from 'neverthrow' +import { + SubintentResponseItem, + WalletInteractionResponse, +} from '../../../../schemas' +import { RequestItemModule } from '../../request-items' +import { SdkError } from '../../../../error' +import { UpdateConnectButtonStatus, WalletResponseResolver } from '../type' +import { RequestStatus } from 'radix-connect-common' + +const matchResponse = ( + input: WalletInteractionResponse, +): SubintentResponseItem | undefined => { + if ( + input.discriminator === 'success' && + input.items.discriminator === 'preAuthorizationResponse' + ) { + return input.items.response + } +} + +export const preAuthorizationResponseResolver = + (dependencies: { + requestItemModule: RequestItemModule + updateConnectButtonStatus: UpdateConnectButtonStatus + }): WalletResponseResolver => + ({ walletInteraction, walletInteractionResponse }) => { + const response = matchResponse(walletInteractionResponse) + if (!response) return okAsync(undefined) + const { signedPartialTransaction, expirationTimestamp, subintentHash } = + response + + const { interactionId } = walletInteraction + + const { requestItemModule } = dependencies + + return requestItemModule + .updateStatus({ + id: interactionId, + status: RequestStatus.pendingCommit, + transactionIntentHash: subintentHash, + walletResponse: walletInteractionResponse, + metadata: { + signedPartialTransaction, + expirationTimestamp, + subintentHash, + }, + }) + .orElse((error) => { + dependencies.updateConnectButtonStatus('fail') + return err(SdkError(error.reason, interactionId)) + }) + .andTee(() => dependencies.updateConnectButtonStatus('success')) + .map(() => undefined) + } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts new file mode 100644 index 00000000..46cef1c2 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts @@ -0,0 +1,81 @@ +import { err, okAsync } from 'neverthrow' +import { + WalletInteractionResponse, + WalletTransactionResponseItems, +} from '../../../../schemas' +import { GatewayModule, TransactionStatus } from '../../../gateway' +import { RequestItemModule } from '../../request-items' +import { SdkError } from '../../../../error' +import { UpdateConnectButtonStatus, WalletResponseResolver } from '../type' + +const matchResponse = ( + input: WalletInteractionResponse, +): WalletTransactionResponseItems | undefined => { + if ( + input.discriminator === 'success' && + input.items.discriminator === 'transaction' + ) { + return input.items + } +} + +const determineFailedTransaction = (status: TransactionStatus) => { + const failedTransactionStatus: TransactionStatus[] = [ + TransactionStatus.Rejected, + TransactionStatus.CommittedFailure, + ] + + return failedTransactionStatus.includes(status) +} + +export const sendTransactionResponseResolver = + (dependencies: { + gatewayModule: GatewayModule + requestItemModule: RequestItemModule + updateConnectButtonStatus: UpdateConnectButtonStatus + }): WalletResponseResolver => + ({ walletInteraction, walletInteractionResponse }) => { + const transactionResponse = matchResponse(walletInteractionResponse) + if (!transactionResponse) return okAsync(undefined) + + const { gatewayModule, requestItemModule, updateConnectButtonStatus } = + dependencies + const { interactionId } = walletInteraction + + const { + send: { transactionIntentHash }, + } = transactionResponse + + return requestItemModule + .getById(interactionId) + .mapErr(() => SdkError('FailedToGetItemWithInteractionId', interactionId)) + .andTee(() => + requestItemModule.getAndRemoveSignal(interactionId)?.( + transactionIntentHash, + ), + ) + .andThen(() => gatewayModule.pollTransactionStatus(transactionIntentHash)) + .andThen((status) => { + const isFailedTransaction = determineFailedTransaction(status) + const requestItemStatus = isFailedTransaction ? 'fail' : 'success' + + return requestItemModule + .updateStatus({ + id: interactionId, + status: requestItemStatus, + transactionIntentHash, + metadata: { transactionStatus: status }, + walletResponse: walletInteractionResponse, + }) + .orElse((error) => err(SdkError(error.reason, interactionId))) + .andThen(() => { + updateConnectButtonStatus(requestItemStatus) + return okAsync(undefined) + }) + .orElse((error) => { + updateConnectButtonStatus('fail') + return err(error) + }) + }) + .map(() => undefined) + } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/type.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/type.ts new file mode 100644 index 00000000..dec31a44 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/type.ts @@ -0,0 +1,16 @@ +import { ResultAsync } from 'neverthrow' +import { WalletInteraction, WalletInteractionResponse } from '../../../schemas' +import { SdkError } from '../../../error' +import { RequestItem } from 'radix-connect-common' + +export type WalletResponseResolverInput = { + walletInteraction: WalletInteraction + walletInteractionResponse: WalletInteractionResponse + requestItem: RequestItem +} + +export type WalletResponseResolver = ( + input: WalletResponseResolverInput, +) => ResultAsync + +export type UpdateConnectButtonStatus = (status: 'fail' | 'success') => void diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts index 0cceadfd..132712cf 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts @@ -7,6 +7,7 @@ import { filter, first, firstValueFrom, + from, map, merge, mergeMap, @@ -19,7 +20,7 @@ import { tap, timer, } from 'rxjs' -import { Logger, isMobile, unwrapObservable } from '../../../../helpers' +import { Logger, unwrapObservable } from '../../../../helpers' import { CallbackFns, IncomingMessage, @@ -29,11 +30,12 @@ import { WalletInteractionResponse, eventType, } from '../../../../schemas' -import { RequestItemModule } from '../../request-items' import { StorageModule } from '../../../storage' import { SdkError } from '../../../../error' import { TransportProvider } from '../../../../_types' import { v4 as uuidV4 } from 'uuid' +import type { RequestResolverModule } from '../../request-resolver/request-resolver.module' +import { EnvironmentModule } from '../../../environment' export type ConnectorExtensionModule = ReturnType< typeof ConnectorExtensionModule @@ -44,7 +46,8 @@ export const ConnectorExtensionModule = (input: { logger?: Logger extensionDetectionTime?: number providers: { - requestItemModule: RequestItemModule + environmentModule: EnvironmentModule + requestResolverModule: RequestResolverModule storageModule: StorageModule<{ sessionId?: string }> } }) => { @@ -56,7 +59,7 @@ export const ConnectorExtensionModule = (input: { const subjects = input?.subjects ?? ConnectorExtensionSubjects() const subscription = new Subscription() const extensionDetectionTime = input?.extensionDetectionTime ?? 200 - const requestItemModule = input.providers.requestItemModule + const requestResolverModule = input.providers.requestResolverModule const storage = input.providers.storageModule.getPartition('connectorExtension') @@ -77,6 +80,15 @@ export const ConnectorExtensionModule = (input: { ) .subscribe(), ) + subscription.add( + subjects.responseSubject + .pipe( + mergeMap((walletResponse) => + from(requestResolverModule.addWalletResponses([walletResponse])), + ), + ) + .subscribe(), + ) subscription.add( subjects.outgoingMessageSubject .pipe( @@ -85,7 +97,7 @@ export const ConnectorExtensionModule = (input: { method: 'outgoingMessageSubject', payload, }) - window.dispatchEvent( + input.providers.environmentModule.globalThis.dispatchEvent( new CustomEvent(eventType.outgoingMessage, { detail: payload, }), @@ -134,25 +146,21 @@ export const ConnectorExtensionModule = (input: { const sendWalletInteraction = ( walletInteraction: WalletInteraction, callbackFns: Partial, - ): ResultAsync => { + ): ResultAsync => { const cancelRequestSubject = new Subject>() + const maybeResolved$ = from( + requestResolverModule.getWalletResponseById( + walletInteraction.interactionId, + ), + ).pipe(filter((result) => result.isOk() && !!result.value)) + const walletResponse$ = subjects.responseSubject.pipe( filter( (response) => response.interactionId === walletInteraction.interactionId, ), - mergeMap( - (walletResponse): ResultAsync => - requestItemModule - .patch(walletResponse.interactionId, { - walletResponse, - }) - .mapErr(() => - SdkError('requestItemPatchError', walletResponse.interactionId), - ) - .map(() => walletResponse), - ), + map((walletResponse) => ok(walletResponse)), ) const cancelResponse$ = subjects.messageLifeCycleEventSubject.pipe( @@ -206,6 +214,7 @@ export const ConnectorExtensionModule = (input: { }) const walletResponseOrCancelRequest$ = merge( + maybeResolved$, walletResponse$, cancelRequestSubject, ).pipe(first()) @@ -304,7 +313,7 @@ export const ConnectorExtensionModule = (input: { return { id: 'connector-extension' as const, - isSupported: () => !isMobile(), + isSupported: () => !input.providers.environmentModule.isMobile(), send: sendWalletInteraction, isAvailable$: extensionStatus$.pipe( map(({ isExtensionAvailable }) => isExtensionAvailable), @@ -313,7 +322,7 @@ export const ConnectorExtensionModule = (input: { map(({ isWalletLinked }) => isWalletLinked), ), showQrCode: () => { - window.dispatchEvent( + input.providers.environmentModule.globalThis.dispatchEvent( new CustomEvent(eventType.outgoingMessage, { detail: { discriminator: 'openPopup' }, }), diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts index 6fba667b..8f64e027 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts @@ -1,24 +1,20 @@ import { ResultAsync } from 'neverthrow' import { errAsync, okAsync } from 'neverthrow' -import { Logger, isMobile } from '../../../../helpers' -import Bowser from 'bowser' +import { Logger } from '../../../../helpers' import { SdkError } from '../../../../error' +import { EnvironmentModule } from '../../../environment' export type DeepLinkModule = ReturnType export const DeepLinkModule = (input: { logger?: Logger walletUrl: string + providers: { + environmentModule: EnvironmentModule + } }) => { const { walletUrl } = input - const userAgent = Bowser.parse(window.navigator.userAgent) - const { platform } = userAgent const logger = input?.logger?.getSubLogger({ name: 'DeepLinkModule' }) - - logger?.debug({ - platform, - userAgent: window.navigator.userAgent, - userAgentParsed: userAgent, - }) + const isTelegramMiniApp = input.providers.environmentModule.isTMA() const deepLinkToWallet = ( values: Record, @@ -34,8 +30,14 @@ export const DeepLinkModule = (input: { data: { ...values }, }) - if (isMobile() && globalThis.location?.href) { - globalThis.location.href = outboundUrl.toString() + if (input.providers.environmentModule.isMobile()) { + const deepLink = outboundUrl.toString() + + // Telegram Mini App does not support deep linking by changing location.href value + if (isTelegramMiniApp) + input.providers.environmentModule.globalThis.open(deepLink, '_blank') + else if (input.providers.environmentModule.globalThis.location?.href) + input.providers.environmentModule.globalThis.location.href = deepLink return okAsync(undefined) } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/index.ts new file mode 100644 index 00000000..4a0ecfb1 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/index.ts @@ -0,0 +1 @@ +export * from './base64url' diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index b2ed636b..fe865ff6 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow' +import { ResultAsync, errAsync } from 'neverthrow' import { Subscription } from 'rxjs' import { EncryptionModule, transformBufferToSealbox } from '../../encryption' import { Session, SessionModule } from '../../session/session.module' @@ -7,11 +7,10 @@ import type { WalletInteraction, WalletInteractionResponse, } from '../../../../schemas' -import { Logger, isMobile, parseJSON } from '../../../../helpers' +import { Logger, parseJSON } from '../../../../helpers' import { SdkError } from '../../../../error' import { DeepLinkModule } from './deep-link.module' import { IdentityModule } from '../../identity/identity.module' -import { RequestItemModule } from '../../request-items/request-item.module' import { StorageModule } from '../../../storage' import { Curve25519 } from '../../crypto' import { @@ -20,6 +19,8 @@ import { } from './radix-connect-relay-api.service' import type { TransportProvider } from '../../../../_types' import { base64urlEncode } from './helpers/base64url' +import type { RequestResolverModule } from '../../request-resolver/request-resolver.module' +import { EnvironmentModule } from '../../../environment' export type RadixConnectRelayModule = ReturnType export const RadixConnectRelayModule = (input: { @@ -28,8 +29,9 @@ export const RadixConnectRelayModule = (input: { walletUrl: string dAppDefinitionAddress: string providers: { - requestItemModule: RequestItemModule storageModule: StorageModule + requestResolverModule: RequestResolverModule + environmentModule: EnvironmentModule encryptionModule?: EncryptionModule identityModule?: IdentityModule sessionModule?: SessionModule @@ -38,10 +40,7 @@ export const RadixConnectRelayModule = (input: { }): TransportProvider => { const logger = input.logger?.getSubLogger({ name: 'RadixConnectRelayModule' }) const { baseUrl, providers, walletUrl } = input - const { requestItemModule, storageModule } = providers - - const walletResponses: StorageModule = - storageModule.getPartition('walletResponses') + const { storageModule, requestResolverModule } = providers const encryptionModule = providers?.encryptionModule ?? EncryptionModule() @@ -50,6 +49,9 @@ export const RadixConnectRelayModule = (input: { DeepLinkModule({ logger, walletUrl, + providers: { + environmentModule: input.providers.environmentModule, + }, }) const identityModule = @@ -102,37 +104,21 @@ export const RadixConnectRelayModule = (input: { } const checkRelayLoop = async () => { - await requestItemModule.getPending().andThen((pendingItems) => { - if (pendingItems.length === 0) { - return okAsync(undefined) - } - - return sessionModule + await requestResolverModule.getPendingRequestIds().andThen(() => + sessionModule .getCurrentSession() - .andThen((session) => - radixConnectRelayApiService.getResponses(session.sessionId), - ) + .map((session) => session.sessionId) + .andThen(radixConnectRelayApiService.getResponses) .andThen((responses) => - ResultAsync.combine( - responses.map((response) => decryptWalletResponse(response)), - ).andThen((decryptedResponses) => { - return walletResponses.setItems( - decryptedResponses.reduce( - (acc, response) => { - acc[response.interactionId] = response - return acc - }, - {} as Record, - ), - ) - }), + ResultAsync.combine(responses.map(decryptWalletResponse)), ) - }) + .andThen(requestResolverModule.addWalletResponses), + ) await wait() checkRelayLoop() } - if (isMobile()) { + if (input.providers.environmentModule.isMobile()) { checkRelayLoop() } @@ -149,33 +135,24 @@ export const RadixConnectRelayModule = (input: { publicKey: string identity: string }) => - requestItemModule - .getById(walletInteraction.interactionId) - .mapErr(() => - SdkError('FailedToGetPendingItems', walletInteraction.interactionId), - ) - .andThen((pendingItem) => - pendingItem - ? ok(pendingItem) - : err( - SdkError('PendingItemNotFound', walletInteraction.interactionId), - ), + requestResolverModule + .getPendingRequestById(walletInteraction.interactionId) + .andThen(() => + requestResolverModule.markRequestAsSent( + walletInteraction.interactionId, + ), ) .andThen(() => - requestItemModule - .patch(walletInteraction.interactionId, { sentToWallet: true }) - .andThen(() => - deepLinkModule.deepLinkToWallet({ - sessionId: session.sessionId, - request: base64urlEncode(walletInteraction), - signature, - publicKey, - identity: identity, - origin: walletInteraction.metadata.origin, - dAppDefinitionAddress: - walletInteraction.metadata.dAppDefinitionAddress, - }), - ), + deepLinkModule.deepLinkToWallet({ + sessionId: session.sessionId, + request: base64urlEncode(walletInteraction), + signature, + publicKey, + identity, + origin: walletInteraction.metadata.origin, + dAppDefinitionAddress: + walletInteraction.metadata.dAppDefinitionAddress, + }), ) .mapErr(() => SdkError('FailedToSendDappRequest', walletInteraction.interactionId), @@ -217,7 +194,12 @@ export const RadixConnectRelayModule = (input: { publicKey: dAppIdentity.x25519.getPublicKey(), }), ) - .andThen(() => waitForWalletResponse(walletInteraction.interactionId)), + .andThen(() => + requestResolverModule.waitForWalletResponse( + walletInteraction.interactionId, + ), + ) + .map((requestItem) => requestItem.walletResponse), ) const decryptWalletResponseData = ( @@ -243,64 +225,9 @@ export const RadixConnectRelayModule = (input: { jsError: error, })) - const waitForWalletResponse = ( - interactionId: string, - ): ResultAsync => - ResultAsync.fromPromise( - new Promise(async (resolve, reject) => { - let response: WalletInteractionResponse | undefined - let error: SdkError | undefined - - logger?.debug({ - method: 'waitForWalletResponse', - interactionId, - }) - - while (!response) { - const requestItemResult = - await requestItemModule.getById(interactionId) - - if (requestItemResult.isOk()) { - logger?.trace({ - method: 'waitForWalletResponse.requestItemResult', - requestItemResult: requestItemResult.value, - }) - if (requestItemResult.value?.status !== 'pending') { - error = SdkError( - 'RequestItemNotPending', - interactionId, - 'request not in pending state', - ) - break - } - } - - const walletResponse = - await walletResponses.getItemById(interactionId) - - if (walletResponse.isOk()) { - if (walletResponse.value) { - response = walletResponse.value - await walletResponses.removeItemById(interactionId) - await requestItemModule.patch(interactionId, { - walletResponse: walletResponse.value, - }) - } - } - - if (!response) { - await wait() - } - } - - return response ? resolve(response) : reject(error) - }), - (err) => err as SdkError, - ) - return { id: 'radix-connect-relay' as const, - isSupported: () => isMobile(), + isSupported: () => input.providers.environmentModule.isMobile(), send: sendToWallet, disconnect: () => {}, destroy: () => { diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/rcfm-page.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/rcfm-page.module.ts deleted file mode 100644 index dc410f7f..00000000 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/rcfm-page.module.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isBrowser } from '../../../../helpers/is-browser' -import { Logger } from '../../../../helpers' - -export const RcfmPageState = { - loading: 'loading', - dAppVerified: 'dAppVerified', - timedOut: 'timedOut', -} as const - -export type RcfmPageState = (typeof RcfmPageState)[keyof typeof RcfmPageState] - -export type RcfmPageModule = ReturnType -export const RcfmPageModule = (input: { logger?: Logger }) => { - const logger = input.logger?.getSubLogger({ name: 'RcfmPageModule' }) - if (!isBrowser()) { - logger?.debug({ method: 'isBrowser', isBrowser: false }) - return { - show: () => {}, - hide: () => {}, - showWithData: () => {}, - } - } - - const rcfmPageHtmlElement = document.createElement('radix-rcfm-page') - document.body.appendChild(rcfmPageHtmlElement) - - const showWithData = (values: { - header?: string - subheader?: string - isError?: boolean - isLoading?: boolean - }) => { - const { header, subheader, isError, isLoading } = values - rcfmPageHtmlElement.header = header || '' - rcfmPageHtmlElement.subheader = subheader || '' - rcfmPageHtmlElement.isError = isError || false - rcfmPageHtmlElement.isLoading = isLoading || false - rcfmPageHtmlElement.isHidden = false - logger?.debug({ - method: 'showWithData', - values, - }) - } - const hide = () => { - logger?.debug({ method: 'hide', isHidden: true }) - rcfmPageHtmlElement.isHidden = true - } - - const show = (state: RcfmPageState) => { - logger?.debug({ method: 'show', state }) - switch (state) { - case RcfmPageState.dAppVerified: - showWithData({ - header: 'Connection succesful!', - subheader: 'You can now close this tab', - isError: false, - isLoading: false, - }) - break - case RcfmPageState.loading: - showWithData({ - isLoading: true, - }) - break - case RcfmPageState.timedOut: - showWithData({ - header: 'Connection timed out', - subheader: 'Close this tab and try connecting again', - isError: true, - isLoading: false, - }) - break - default: - break - } - } - - return { - show, - hide, - showWithData, - } -} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts new file mode 100644 index 00000000..6686fa8c --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts @@ -0,0 +1,35 @@ +import { okAsync, ResultAsync } from 'neverthrow' +import { TransportProvider } from '../../../../_types' +import { WalletInteractionResponse } from '../../../../schemas' + +export const TestingTransportModule = ({ + requestResolverModule, +}: { + requestResolverModule: { + addWalletResponses: ( + responses: WalletInteractionResponse[], + ) => ResultAsync + } +}): TransportProvider & { + setNextWalletResponse: (response: WalletInteractionResponse) => void +} => { + let _isSupported = true + let id = 'TestingTransportModule' + let _sendResponse: WalletInteractionResponse + + return { + id, + isSupported: () => _isSupported, + destroy: () => {}, + disconnect: () => {}, + send: () => { + requestResolverModule.addWalletResponses([_sendResponse]) + return okAsync(_sendResponse) + }, + + // Test helpers + setNextWalletResponse: (response: WalletInteractionResponse) => { + _sendResponse = response + }, + } +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts index ec9ec97b..12e03a74 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts @@ -1,16 +1,18 @@ -import { Result, ResultAsync, err, ok } from 'neverthrow' +import { Result, ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow' import { TransportProvider } from '../../_types' -import { Logger, validateWalletResponse } from '../../helpers' +import { Logger } from '../../helpers' import { Metadata, CallbackFns, WalletInteractionItems, WalletInteraction, - WalletInteractionResponse, + WalletInteractionFailureResponse, + WalletInteractionSuccessResponse, } from '../../schemas' import { parse } from 'valibot' import { SdkError } from '../../error' import { v4 as uuidV4 } from 'uuid' +import { EnvironmentModule } from '../environment' export type WalletRequestSdkInput = { networkId: number @@ -22,6 +24,8 @@ export type WalletRequestSdkInput = { ) => Promise providers: { transports: TransportProvider[] + interactionIdFactory?: () => string + environmentModule: EnvironmentModule } } export type WalletRequestSdk = ReturnType @@ -31,9 +35,13 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => { version: 2, dAppDefinitionAddress: input.dAppDefinitionAddress, networkId: input.networkId, - origin: input.origin || window.location.origin, + origin: + input.origin || + input.providers.environmentModule.globalThis?.location?.origin || '', } as Metadata + const interactionIdFactory = input.providers.interactionIdFactory ?? uuidV4 + parse(Metadata, metadata) const logger = input?.logger?.getSubLogger({ name: 'WalletSdk' }) @@ -50,7 +58,7 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => { const createWalletInteraction = ( items: WalletInteractionItems, - interactionId = uuidV4(), + interactionId = interactionIdFactory(), ): WalletInteraction => ({ items, interactionId, @@ -80,47 +88,34 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => { }) } - const request = ( - { - interactionId = uuidV4(), - items, - }: Pick & { interactionId?: string }, - callbackFns: Partial = {}, - ): ResultAsync => - withInterceptor({ - items, - interactionId, - metadata, - }).andThen((walletInteraction) => - getTransport(walletInteraction.interactionId).asyncAndThen((transport) => - transport - .send(walletInteraction, callbackFns) - .andThen(validateWalletResponse), - ), - ) - - const sendTransaction = ( + const sendInteraction = ( { interactionId = uuidV4(), items, }: { interactionId?: string; items: WalletInteraction['items'] }, callbackFns: Partial = {}, - ): ResultAsync => + ): ResultAsync< + WalletInteractionSuccessResponse, + SdkError | WalletInteractionFailureResponse + > => withInterceptor({ - interactionId, items, + interactionId, metadata, }).andThen((walletInteraction) => - getTransport(interactionId).asyncAndThen((transport) => - transport - .send(walletInteraction, callbackFns) - .andThen(validateWalletResponse), - ), + getTransport(walletInteraction.interactionId) + .asyncAndThen((transport) => + transport.send(walletInteraction, callbackFns), + ) + .andThen((response) => + response.discriminator === 'failure' + ? errAsync(response) + : okAsync(response), + ), ) return { - request, - sendTransaction, + sendInteraction, createWalletInteraction, getTransport, } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts new file mode 100644 index 00000000..3422a94c --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts @@ -0,0 +1,314 @@ +import { describe, expect, it, vi } from 'vitest' +import { WalletRequestModule } from './wallet-request' +import { RadixNetwork, TransactionStatus } from '../gateway' +import { LocalStorageModule } from '../storage' +import { ok, okAsync, ResultAsync } from 'neverthrow' +import { WalletInteractionItems } from '../../schemas' +import { + failedResponseResolver, + preAuthorizationResponseResolver, + RequestResolverModule, + sendTransactionResponseResolver, +} from './request-resolver' +import { RequestItemModule } from './request-items' +import { delayAsync } from '../../test-helpers/delay-async' +import { WalletRequestSdk } from './wallet-request-sdk' +import { TestingTransportModule } from './transport/testing-transport/transport.testing-module' +import { EnvironmentModule } from '../environment' +import { SubintentRequestBuilder } from './pre-authorization-request' + +const createMockEnvironment = () => { + const storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`, { + providers: { + environmentModule: EnvironmentModule(), + }, + }) + const gatewayModule = { + pollTransactionStatus: (hash: string) => + ResultAsync.fromSafePromise(delayAsync(2000)).map(() => + ok({ status: 'success' as TransactionStatus }), + ), + + pollSubintentStatus: () => { + return { + stop: () => undefined, + result: ResultAsync.fromSafePromise(delayAsync(100)).map(() => ({ + subintentStatus: 'CommittedSuccess' as TransactionStatus, + transactionIntentHash: 'transactionIntentHash', + })), + } + }, + } as any + const requestItemModule = RequestItemModule({ + providers: { + gatewayModule, + storageModule, + }, + }) + + const updateConnectButtonStatus = () => {} + return { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } +} + +describe('WalletRequestModule', () => { + describe('given `onTransactionId` callback is provided', () => { + it('should call the callback before polling is finished', async () => { + // Arange + const { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } = createMockEnvironment() + + const requestResolverModule = RequestResolverModule({ + providers: { + storageModule, + requestItemModule, + resolvers: [ + sendTransactionResponseResolver({ + gatewayModule, + requestItemModule, + updateConnectButtonStatus, + }), + ], + }, + }) + + const interactionId = 'abcdef' + const resultReturned = vi.fn() + const onTransactionIdSpy = vi.fn() + + const walletRequestModule = WalletRequestModule({ + useCache: false, + networkId: RadixNetwork.Stokenet, + dAppDefinitionAddress: '', + providers: { + environmentModule: EnvironmentModule(), + stateModule: {} as any, + storageModule, + requestItemModule, + requestResolverModule, + gatewayModule, + walletRequestSdk: { + sendInteraction: () => okAsync({}), + createWalletInteraction: (items: WalletInteractionItems) => ({ + items, + interactionId, + metadata: {} as any, + }), + } as any, + }, + }) + + // Act + walletRequestModule + .sendTransaction({ + transactionManifest: ``, + onTransactionId: onTransactionIdSpy, + }) + .map(resultReturned) + + await delayAsync(50) + + requestResolverModule.addWalletResponses([ + { + interactionId, + discriminator: 'success', + items: { + discriminator: 'transaction', + send: { + transactionIntentHash: 'intent_hash', + }, + }, + }, + ]) + + // Assert + expect(resultReturned).not.toHaveBeenCalled() + await expect + .poll(() => onTransactionIdSpy, { + timeout: 1000, + }) + .toHaveBeenCalledWith('intent_hash') + await expect + .poll(() => resultReturned, { + timeout: 3000, + }) + .toHaveBeenCalledWith( + expect.objectContaining({ transactionIntentHash: 'intent_hash' }), + ) + }) + }) + + describe('GIVEN wallet responds with discriminator "failure"', () => { + it('should return error result', async () => { + // Arange + const { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } = createMockEnvironment() + + const requestResolverModule = RequestResolverModule({ + providers: { + storageModule, + requestItemModule, + resolvers: [ + failedResponseResolver({ + requestItemModule, + updateConnectButtonStatus, + }), + ], + }, + }) + + const interactionId = '8cefec84-542d-40af-8782-b89df05db8ac' + + const testingTransport = TestingTransportModule({ requestResolverModule }) + testingTransport.setNextWalletResponse({ + discriminator: 'failure', + interactionId: '8cefec84-542d-40af-8782-b89df05db8ac', + error: 'rejectedByUser', + }) + + const walletRequestModule = WalletRequestModule({ + useCache: false, + networkId: RadixNetwork.Stokenet, + dAppDefinitionAddress: '', + providers: { + stateModule: {} as any, + storageModule, + requestItemModule, + requestResolverModule, + environmentModule: EnvironmentModule(), + gatewayModule, + walletRequestSdk: WalletRequestSdk({ + networkId: 2, + dAppDefinitionAddress: '', + providers: { + environmentModule: EnvironmentModule(), + interactionIdFactory: () => interactionId, + transports: [testingTransport], + }, + }), + }, + }) + + // Act + const result = await walletRequestModule.sendTransaction({ + transactionManifest: ``, + }) + + // Assert + expect(result.isErr() && result.error).toEqual( + expect.objectContaining({ + discriminator: 'failure', + error: 'rejectedByUser', + }), + ) + }) + }) + + describe('GIVEN subintent is submitted to the network', () => { + describe('AND onSubmittedSuccess callback is provided', () => { + it('should call the callback with transaction intent hash', async () => { + // Arange + const { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } = createMockEnvironment() + + const requestResolverModule = RequestResolverModule({ + providers: { + storageModule, + requestItemModule, + resolvers: [ + preAuthorizationResponseResolver({ + requestItemModule, + updateConnectButtonStatus, + }), + ], + }, + }) + + const interactionId = '8cefec84' + + const testingTransport = TestingTransportModule({ + requestResolverModule, + }) + testingTransport.setNextWalletResponse({ + discriminator: 'success', + items: { + discriminator: 'preAuthorizationResponse', + response: { + subintentHash: + 'subtxid_tdx_2_17nhcfn9njxlrvgl8afk5dwcaj2peydrtzty0rppdm5dqnwqxs6sq0u59fe', + expirationTimestamp: Math.floor(Date.now() / 1000) + 3600, + signedPartialTransaction: + '4d220e03210221012105210607020a32ef0000000000000a40ef00000000000022000', + }, + }, + interactionId, + }) + + const walletRequestModule = WalletRequestModule({ + useCache: false, + networkId: RadixNetwork.Stokenet, + dAppDefinitionAddress: '', + providers: { + stateModule: {} as any, + storageModule, + requestItemModule, + requestResolverModule, + environmentModule: EnvironmentModule(), + gatewayModule, + walletRequestSdk: WalletRequestSdk({ + networkId: 2, + dAppDefinitionAddress: '', + providers: { + environmentModule: EnvironmentModule(), + interactionIdFactory: () => interactionId, + transports: [testingTransport], + }, + }), + }, + }) + + const onSubmittedSpy = vi.fn() + // Act + const result = await walletRequestModule.sendPreAuthorizationRequest( + SubintentRequestBuilder() + .manifest(``) + .setExpiration('afterDelay', 4600) + .onSubmittedSuccess((a) => { + console.log('onSubmittedSuccess', a) + onSubmittedSpy(a) + }), + ) + + await delayAsync(2000) + + // Assert + + expect(onSubmittedSpy).toHaveBeenCalledWith('transactionIntentHash') + expect(result.isOk() && result.value).toEqual( + expect.objectContaining({ + signedPartialTransaction: + '4d220e03210221012105210607020a32ef0000000000000a40ef00000000000022000', + subintentHash: + 'subtxid_tdx_2_17nhcfn9njxlrvgl8afk5dwcaj2peydrtzty0rppdm5dqnwqxs6sq0u59fe', + }), + ) + }) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts index 2e05c099..43e120fe 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts @@ -5,17 +5,16 @@ import { filter, firstValueFrom, map, - mergeMap, switchMap, tap, } from 'rxjs' import { validateRolaChallenge, type Logger } from '../../helpers' import { TransactionStatus } from '../gateway' -import { Result, ResultAsync, err, ok, okAsync } from 'neverthrow' +import { ResultAsync, err, ok, okAsync } from 'neverthrow' import type { MessageLifeCycleEvent, + SubintentResponseItem, WalletInteraction, - WalletInteractionResponse, } from '../../schemas' import { SdkError } from '../../error' import { @@ -25,27 +24,29 @@ import { canDataRequestBeResolvedByRdtState, toWalletRequest, transformSharedDataToDataRequestState, - transformWalletRequestToSharedData, - transformWalletResponseToRdtWalletData, } from './data-request' import { StorageModule } from '../storage' -import { StateModule, WalletData } from '../state' +import type { StateModule, WalletData } from '../state' import { AwaitedWalletDataRequestResult, + SendPreAuthorizationRequestInput, + SendTransactionInput, TransportProvider, WalletDataRequestResult, } from '../../_types' import { ConnectorExtensionModule, RadixConnectRelayModule } from './transport' import { GatewayModule } from '../gateway' import { RequestItemModule } from './request-items' - -type SendTransactionInput = { - transactionManifest: string - version?: number - blobs?: string[] - message?: string - onTransactionId?: (transactionId: string) => void -} +import { RequestResolverModule } from './request-resolver/request-resolver.module' +import { + dataResponseResolver, + failedResponseResolver, + preAuthorizationResponseResolver, + sendTransactionResponseResolver, +} from './request-resolver' +import { RequestItemTypes } from 'radix-connect-common' +import { PreauthorizationPollingModule } from './pre-authorization-request/preauthorization-polling-module' +import { EnvironmentModule } from '../environment' export type WalletRequestModule = ReturnType export const WalletRequestModule = (input: { @@ -59,10 +60,13 @@ export const WalletRequestModule = (input: { stateModule: StateModule storageModule: StorageModule gatewayModule: GatewayModule + environmentModule: EnvironmentModule transports?: TransportProvider[] dataRequestStateModule?: DataRequestStateModule requestItemModule?: RequestItemModule walletRequestSdk?: WalletRequestSdk + requestResolverModule?: RequestResolverModule + preauthorizationPollingModule?: PreauthorizationPollingModule } }) => { const logger = input.logger?.getSubLogger({ name: 'WalletRequestModule' }) @@ -70,7 +74,9 @@ export const WalletRequestModule = (input: { const networkId = input.networkId const cancelRequestSubject = new Subject() const ignoreTransactionSubject = new Subject() - const interactionStatusChangeSubject = new Subject<'fail' | 'success'>() + const interactionStatusChangeSubject = new Subject< + 'fail' | 'success' | 'pending' + >() const gatewayModule = input.providers.gatewayModule const dAppDefinitionAddress = input.dAppDefinitionAddress @@ -85,14 +91,67 @@ export const WalletRequestModule = (input: { RequestItemModule({ logger, providers: { + gatewayModule, storageModule: storageModule.getPartition('requests'), }, }) + const preauthorizationPollingModule = + input.providers.preauthorizationPollingModule ?? + PreauthorizationPollingModule({ + logger, + providers: { + gatewayModule, + requestItemModule, + ignoreTransactionSubject, + }, + }) + + const updateConnectButtonStatus = ( + status: 'success' | 'fail' | 'pending', + ) => { + interactionStatusChangeSubject.next(status) + } + + const requestResolverModule = + input.providers.requestResolverModule ?? + RequestResolverModule({ + logger, + providers: { + storageModule, + requestItemModule, + resolvers: [ + sendTransactionResponseResolver({ + gatewayModule, + requestItemModule, + updateConnectButtonStatus, + }), + preAuthorizationResponseResolver({ + requestItemModule, + updateConnectButtonStatus, + }), + failedResponseResolver({ + requestItemModule, + updateConnectButtonStatus, + }), + dataResponseResolver({ + requestItemModule, + getDataRequestController: () => dataRequestControl, + stateModule, + updateConnectButtonStatus, + }), + ], + }, + }) + const transports: TransportProvider[] = input.providers.transports ?? [ ConnectorExtensionModule({ logger, - providers: { requestItemModule, storageModule }, + providers: { + storageModule, + requestResolverModule, + environmentModule: input.providers.environmentModule, + }, }), RadixConnectRelayModule({ logger, @@ -100,8 +159,9 @@ export const WalletRequestModule = (input: { baseUrl: 'https://radix-connect-relay.radixdlt.com', dAppDefinitionAddress: input.dAppDefinitionAddress, providers: { - requestItemModule, storageModule, + requestResolverModule, + environmentModule: input.providers.environmentModule, }, }), ] @@ -114,7 +174,10 @@ export const WalletRequestModule = (input: { origin: input.origin, dAppDefinitionAddress, requestInterceptor: input.requestInterceptor, - providers: { transports }, + providers: { + transports, + environmentModule: input.providers.environmentModule, + }, }) const cancelRequestControl = (id: string) => { @@ -131,7 +194,11 @@ export const WalletRequestModule = (input: { filter((event) => event === 'receivedByWallet'), map(() => getRequest()), tap((request) => { - if (request.items.discriminator === 'transaction') + if ( + ['transaction', 'preAuthorizationRequest'].includes( + request.items.discriminator, + ) + ) requestItemModule.patch(id, { showCancel: false }) }), ), @@ -158,7 +225,7 @@ export const WalletRequestModule = (input: { ), ) }, - } satisfies Parameters[1] + } satisfies Parameters[1] } let challengeGeneratorFn: () => Promise = () => Promise.resolve('') @@ -172,7 +239,9 @@ export const WalletRequestModule = (input: { ) => ResultAsync const isChallengeNeeded = (dataRequestState: DataRequestState) => - dataRequestState.accounts?.withProof || dataRequestState.persona?.withProof + dataRequestState.accounts?.withProof || + dataRequestState.persona?.withProof || + dataRequestState.proofOfOwnership const getChallenge = ( dataRequestState: DataRequestState, @@ -204,6 +273,21 @@ export const WalletRequestModule = (input: { })) } + const sendRequestAndAwaitResponse = ( + walletInteraction: WalletInteraction, + ) => { + updateConnectButtonStatus('pending') + return ResultAsync.combine([ + walletRequestSdk.sendInteraction( + walletInteraction, + cancelRequestControl(walletInteraction.interactionId), + ), + requestResolverModule.waitForWalletResponse( + walletInteraction.interactionId, + ), + ]).map(([_, response]) => response) + } + const sendOneTimeRequest = (...items: DataRequestBuilderItem[]) => sendRequest({ dataRequestState: dataRequestStateModule.toDataRequestState(...items), @@ -211,61 +295,64 @@ export const WalletRequestModule = (input: { oneTime: true, }) - const resolveWalletResponse = ( - walletInteraction: WalletInteraction, - walletInteractionResponse: WalletInteractionResponse, - ) => { - if ( - walletInteractionResponse.discriminator === 'success' && - walletInteractionResponse.items.discriminator === 'authorizedRequest' - ) { - return ResultAsync.combine([ - transformWalletResponseToRdtWalletData(walletInteractionResponse.items), - stateModule.getState(), - ]).andThen(([walletData, state]) => { - return stateModule - .setState({ - loggedInTimestamp: Date.now().toString(), - walletData, - sharedData: transformWalletRequestToSharedData( - walletInteraction, - state!.sharedData, - ), - }) - .andThen(() => - requestItemModule.updateStatus({ - id: walletInteractionResponse.interactionId, - status: 'success', - }), - ) - }) - } - - return okAsync(undefined) - } - - const sendDataRequest = (walletInteraction: WalletInteraction) => { - return walletRequestSdk - .request( - walletInteraction, - cancelRequestControl(walletInteraction.interactionId), - ) - .map((response: WalletInteractionResponse) => { + const sendDataRequest = (walletInteraction: WalletInteraction) => + sendRequestAndAwaitResponse(walletInteraction) + .andThen((response) => { logger?.debug({ method: 'sendDataRequest.successResponse', response }) - - return response + return ok(response.walletData! as WalletData) }) .mapErr((error) => { logger?.debug({ method: 'sendDataRequest.errorResponse', error }) - - requestItemModule.updateStatus({ - id: walletInteraction.interactionId, - status: 'fail', - error: error.error, - }) - return error }) + + const getRdtState = () => + stateModule.getState().mapErr(() => SdkError('FailedToReadRdtState', '')) + + const addNewRequest = ( + type: RequestItemTypes, + walletInteraction: WalletInteraction, + isOneTimeRequest: boolean, + signal?: (transactionIntentHash: string) => void, + ) => + requestItemModule + .add( + { + type, + walletInteraction, + isOneTimeRequest, + }, + signal, + ) + .mapErr(({ message }) => + SdkError( + 'FailedToCreateRequestItem', + walletInteraction.interactionId, + message, + ), + ) + + const sendPreAuthorizationRequest = ( + value: SendPreAuthorizationRequestInput, + ): ResultAsync => { + const walletInteraction = walletRequestSdk.createWalletInteraction({ + discriminator: 'preAuthorizationRequest', + request: value.toRequestItem(), + }) + + return addNewRequest( + 'preAuthorizationRequest', + walletInteraction, + false, + value.getOnSubmittedSuccessFn?.(), + ) + .andThen(() => sendRequestAndAwaitResponse(walletInteraction)) + .map( + (requestItem) => + ({ + ...requestItem.metadata, + }) as SubintentResponseItem, + ) } const sendRequest = ({ @@ -276,137 +363,48 @@ export const WalletRequestModule = (input: { dataRequestState: DataRequestState isConnect: boolean oneTime: boolean - }): WalletDataRequestResult => { - return ResultAsync.combine([ + }): WalletDataRequestResult => + ResultAsync.combine([ getChallenge(dataRequestState), - stateModule.getState().mapErr(() => SdkError('FailedToReadRdtState', '')), - ]) - .andThen(([challenge, state]) => - toWalletRequest({ - dataRequestState, - isConnect, - oneTime, - challenge, - walletData: state.walletData, - }) - .mapErr(() => SdkError('FailedToTransformWalletRequest', '')) - .asyncAndThen((walletDataRequest) => { - const walletInteraction: WalletInteraction = - walletRequestSdk.createWalletInteraction(walletDataRequest) - - if ( - canDataRequestBeResolvedByRdtState(walletDataRequest, state) && - useCache - ) - return okAsync(state.walletData) - - const isLoginRequest = - !state.walletData.persona && - walletDataRequest.discriminator === 'authorizedRequest' - - return requestItemModule - .add({ - type: isLoginRequest ? 'loginRequest' : 'dataRequest', - walletInteraction, - isOneTimeRequest: oneTime, - }) - .mapErr(({ message }) => - SdkError( - 'FailedToCreateRequestItem', - walletInteraction.interactionId, - message, - ), - ) - .andThen(() => - sendDataRequest(walletInteraction) - .andThen((walletInteractionResponse) => { - if ( - walletInteractionResponse.discriminator === 'success' && - walletInteractionResponse.items.discriminator !== - 'transaction' - ) - return ok(walletInteractionResponse.items) - - return err( - SdkError( - 'WalletResponseFailure', - walletInteractionResponse.interactionId, - 'expected data response', - ), - ) - }) - .andThen(transformWalletResponseToRdtWalletData) - .andThen((transformedWalletResponse) => { - if (dataRequestControl) - return dataRequestControl(transformedWalletResponse) - .andThen(() => - requestItemModule - .updateStatus({ - id: walletInteraction.interactionId, - status: 'success', - }) - .mapErr((error) => - SdkError( - error.reason, - walletInteraction.interactionId, - ), - ) - .map(() => transformedWalletResponse), - ) - .mapErr((error) => { - requestItemModule.updateStatus({ - id: walletInteraction.interactionId, - status: 'fail', - error: error.error, - }) - return SdkError( - error.error, - walletInteraction.interactionId, - ) - }) - - return requestItemModule - .updateStatus({ - id: walletInteraction.interactionId, - status: 'success', - }) - .map(() => transformedWalletResponse) - .mapErr((error) => - SdkError(error.reason, walletInteraction.interactionId), - ) - }) - .map((transformedWalletResponse) => { - interactionStatusChangeSubject.next('success') - - if (!oneTime) { - stateModule - .setState({ - loggedInTimestamp: Date.now().toString(), - walletData: transformedWalletResponse, - sharedData: transformWalletRequestToSharedData( - walletInteraction, - state.sharedData, - ), - }) - .map(() => { - stateModule.emitWalletData() - }) - } - - return transformedWalletResponse - }) - .mapErr((err) => { - interactionStatusChangeSubject.next('fail') - return err - }), - ) - }), - ) - .mapErr((error) => { - logger?.error(error) - return error + getRdtState(), + ]).andThen(([challenge, state]) => + toWalletRequest({ + dataRequestState, + isConnect, + oneTime, + challenge, + walletData: state.walletData, }) - } + .mapErr(() => SdkError('FailedToTransformWalletRequest', '')) + .asyncAndThen((walletDataRequest) => { + const walletInteraction: WalletInteraction = + walletRequestSdk.createWalletInteraction(walletDataRequest) + + if ( + canDataRequestBeResolvedByRdtState(walletDataRequest, state) && + useCache + ) + return okAsync(state.walletData) + + const isLoginRequest = + !state.walletData.persona && + walletDataRequest.discriminator === 'authorizedRequest' + + const isProofOfOwnershipRequest = + walletDataRequest.discriminator === 'authorizedRequest' && + !!walletDataRequest.proofOfOwnership + + const requestType = isLoginRequest + ? 'loginRequest' + : isProofOfOwnershipRequest + ? 'proofRequest' + : 'dataRequest' + + return addNewRequest(requestType, walletInteraction, oneTime).andThen( + () => sendDataRequest(walletInteraction), + ) + }), + ) const setRequestDataState = (...items: DataRequestBuilderItem[]) => { dataRequestStateModule.setState(...items) @@ -443,25 +441,6 @@ export const WalletRequestModule = (input: { const subscriptions = new Subscription() - subscriptions.add( - requestItemModule.requests$ - .pipe( - mergeMap((items) => { - const unresolvedItems = items - .filter((item) => item.status === 'pending' && item.walletResponse) - .map((item) => - resolveWalletResponse( - item.walletInteraction, - item.walletResponse, - ), - ) - - return ResultAsync.combineWithAllErrors(unresolvedItems) - }), - ) - .subscribe(), - ) - const sendTransaction = ( value: SendTransactionInput, ): ResultAsync< @@ -471,107 +450,45 @@ export const WalletRequestModule = (input: { }, SdkError > => { - const walletInteraction = walletRequestSdk.createWalletInteraction({ - discriminator: 'transaction', - send: { - blobs: value.blobs, - transactionManifest: value.transactionManifest, - message: value.message, - version: value.version ?? 1, - }, - }) - - requestItemModule.add({ - type: 'sendTransaction', - walletInteraction, - isOneTimeRequest: false, - }) - - return walletRequestSdk - .sendTransaction( - walletInteraction, - cancelRequestControl(walletInteraction.interactionId), - ) - .mapErr((response): SdkError => { - requestItemModule.updateStatus({ - id: walletInteraction.interactionId, - status: 'fail', - error: response.error, - }) - interactionStatusChangeSubject.next('fail') - logger?.debug({ method: 'sendTransaction.errorResponse', response }) - return response - }) - .andThen( - (response): Result<{ transactionIntentHash: string }, SdkError> => { - logger?.debug({ method: 'sendTransaction.successResponse', response }) - if ( - response.discriminator === 'success' && - response.items.discriminator === 'transaction' - ) - return ok(response.items.send) - - if (response.discriminator === 'failure') - return err( - SdkError( - response.error, - response.interactionId, - response.message, - ), - ) - - return err(SdkError('WalletResponseFailure', response.interactionId)) + const createTransactionRequest = () => { + const walletInteraction = walletRequestSdk.createWalletInteraction({ + discriminator: 'transaction', + send: { + blobs: value.blobs, + transactionManifest: value.transactionManifest, + message: value.message, + version: value.version ?? 1, }, - ) - .andThen(({ transactionIntentHash }) => { - if (value.onTransactionId) value.onTransactionId(transactionIntentHash) - return gatewayModule - .pollTransactionStatus(transactionIntentHash) - .map((transactionStatusResponse) => ({ - transactionIntentHash, - status: transactionStatusResponse.status, - })) }) - .andThen((response) => { - const failedTransactionStatus: TransactionStatus[] = [ - TransactionStatus.Rejected, - TransactionStatus.CommittedFailure, - ] - const isFailedTransaction = failedTransactionStatus.includes( - response.status, + return requestItemModule + .add( + { + type: 'sendTransaction', + walletInteraction, + isOneTimeRequest: false, + }, + value.onTransactionId, ) + .mapErr(() => + SdkError('FailedToAddRequestItem', walletInteraction.interactionId), + ) + .map(() => walletInteraction) + } - logger?.debug({ - method: 'sendTransaction.pollTransactionStatus.completed', - response, - }) + return createTransactionRequest() + .andThen((walletInteraction) => + sendRequestAndAwaitResponse(walletInteraction), + ) + .andThen(({ status, transactionIntentHash, metadata, interactionId }) => { + const output = { + transactionIntentHash: transactionIntentHash!, + status: metadata!.transactionStatus as TransactionStatus, + } - const status = isFailedTransaction ? 'fail' : 'success' - - return requestItemModule - .updateStatus({ - id: walletInteraction.interactionId, - status, - transactionIntentHash: response.transactionIntentHash, - }) - .mapErr(() => - SdkError( - 'FailedToUpdateRequestItem', - walletInteraction.interactionId, - ), - ) - .andThen(() => { - interactionStatusChangeSubject.next(status) - return isFailedTransaction - ? err( - SdkError( - 'TransactionNotSuccessful', - walletInteraction.interactionId, - ), - ) - : ok(response) - }) + return status === 'success' + ? ok(output) + : err(SdkError(output.status, interactionId)) }) } @@ -584,6 +501,7 @@ export const WalletRequestModule = (input: { cancelRequestSubject.next(id) requestItemModule.cancel(id) interactionStatusChangeSubject.next('fail') + updateConnectButtonStatus('fail') } const ignoreTransaction = (id: string) => { @@ -614,31 +532,30 @@ export const WalletRequestModule = (input: { const destroy = () => { stateModule.destroy() requestItemModule.destroy() + requestResolverModule.destroy() + preauthorizationPollingModule.destroy() input.providers.transports?.forEach((transport) => transport.destroy()) subscriptions.unsubscribe() } return { - sendRequest: (input: { isConnect: boolean; oneTime: boolean }) => { - const result = sendRequest({ + sendRequest: (input: { isConnect: boolean; oneTime: boolean }) => + sendRequest({ isConnect: input.isConnect, oneTime: input.oneTime, dataRequestState: dataRequestStateModule.getState(), }) - - if (connectResponseCallback) - result - .map((result) => { - connectResponseCallback!(ok(result)) - }) - .mapErr((error) => { - connectResponseCallback!(err(error)) - }) - - return result - }, + .andThen((response) => { + if (connectResponseCallback) connectResponseCallback!(ok(response)) + return ok(response) + }) + .orElse((error) => { + if (connectResponseCallback) connectResponseCallback!(err(error)) + return err(error) + }), sendTransaction, + sendPreAuthorizationRequest, cancelRequest, ignoreTransaction, requestItemModule, diff --git a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts index 50ce4f84..87bb5af4 100644 --- a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts +++ b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts @@ -18,6 +18,7 @@ import { ConnectButtonModule, generateGatewayApiConfig, } from './modules' +import { EnvironmentModule } from './modules/environment' export type RadixDappToolkit = { walletApi: WalletApi @@ -45,9 +46,15 @@ export const RadixDappToolkit = ( useCache = true, } = options || {} + const environmentModule = providers?.environmentModule ?? EnvironmentModule() + const storageModule = providers?.storageModule ?? - LocalStorageModule(`rdt:${dAppDefinitionAddress}:${networkId}`) + LocalStorageModule(`rdt:${dAppDefinitionAddress}:${networkId}`, { + providers: { + environmentModule, + }, + }) const stateModule = providers?.stateModule ?? @@ -83,6 +90,7 @@ export const RadixDappToolkit = ( stateModule, storageModule, gatewayModule, + environmentModule, }, }) @@ -96,9 +104,9 @@ export const RadixDappToolkit = ( dAppDefinitionAddress, providers: { stateModule, + environmentModule, walletRequestModule, gatewayModule, - storageModule: storageModule.getPartition('connectButton'), }, }) @@ -119,6 +127,8 @@ export const RadixDappToolkit = ( walletRequestModule.provideConnectResponseCallback, updateSharedAccounts: () => walletRequestModule.updateSharedAccounts(), sendOneTimeRequest: walletRequestModule.sendOneTimeRequest, + sendPreAuthorizationRequest: + walletRequestModule.sendPreAuthorizationRequest, sendTransaction: (input: SendTransactionInput) => walletRequestModule.sendTransaction(input), walletData$: stateModule.walletData$, diff --git a/packages/dapp-toolkit/src/schemas/index.ts b/packages/dapp-toolkit/src/schemas/index.ts index 5ee3f8a1..519e1e6e 100644 --- a/packages/dapp-toolkit/src/schemas/index.ts +++ b/packages/dapp-toolkit/src/schemas/index.ts @@ -9,50 +9,70 @@ import { minValue, string, union, - merge, - Output, + InferOutput, ValiError, - custom, + check, + pipe, } from 'valibot' /** * Wallet schemas */ -export type Account = Output +export type Account = InferOutput export const Account = object({ address: string(), label: string(), appearanceId: number(), }) -export type Proof = Output +export type Proof = InferOutput export const Proof = object({ publicKey: string(), signature: string(), curve: union([literal('curve25519'), literal('secp256k1')]), }) -export type AccountProof = Output +export type AccountProof = InferOutput export const AccountProof = object({ accountAddress: string(), proof: Proof, }) -export type Persona = Output +export type PersonaProof = InferOutput +export const PersonaProof = object({ + identityAddress: string(), + proof: Proof, +}) + +export type ProofOfOwnershipRequestItem = InferOutput< + typeof ProofOfOwnershipRequestItem +> +export const ProofOfOwnershipRequestItem = object({ + challenge: string(), + identityAddress: optional(string()), + accountAddresses: optional(array(string())), +}) + +export const ProofOfOwnershipResponseItem = object({ + challenge: string(), + proofs: array(union([AccountProof, PersonaProof])), +}) + +export type Persona = InferOutput export const Persona = object({ identityAddress: string(), label: string() }) export const personaDataFullNameVariant = { western: 'western', eastern: 'eastern', } as const -export type PersonaDataNameVariant = Output +export type PersonaDataNameVariant = InferOutput export const PersonaDataNameVariant = union([ literal(personaDataFullNameVariant.eastern), literal(personaDataFullNameVariant.western), ]) -export type PersonaDataName = Output +export type PersonaDataName = InferOutput export const PersonaDataName = object({ variant: PersonaDataNameVariant, familyName: string(), @@ -60,45 +80,43 @@ export const PersonaDataName = object({ givenNames: string(), }) -export type NumberOfValues = Output +export type NumberOfValues = InferOutput export const NumberOfValues = object({ quantifier: union([literal('exactly'), literal('atLeast')]), - quantity: number([minValue(0, 'The number must be at least 0.')]), + quantity: pipe(number(), minValue(0, 'The number must be at least 0.')), }) -export type AccountsRequestItem = Output +export type AccountsRequestItem = InferOutput export const AccountsRequestItem = object({ challenge: optional(string()), numberOfAccounts: NumberOfValues, }) -export type AccountsRequestResponseItem = Output< +export type AccountsRequestResponseItem = InferOutput< typeof AccountsRequestResponseItem > -export const AccountsRequestResponseItem = object( - { +export const AccountsRequestResponseItem = pipe( + object({ accounts: array(Account), challenge: optional(string()), proofs: optional(array(AccountProof)), - }, - [ - custom((data) => { - if (data.challenge || data?.proofs) { - return !!(data.challenge && data?.proofs?.length) - } - return true - }, 'missing challenge or proofs'), - ], + }), + check((data) => { + if (data.challenge || data?.proofs) { + return !!(data.challenge && data?.proofs?.length) + } + return true + }, 'missing challenge or proofs'), ) -export type PersonaDataRequestItem = Output +export type PersonaDataRequestItem = InferOutput export const PersonaDataRequestItem = object({ isRequestingName: optional(boolean()), numberOfRequestedEmailAddresses: optional(NumberOfValues), numberOfRequestedPhoneNumbers: optional(NumberOfValues), }) -export type PersonaDataRequestResponseItem = Output< +export type PersonaDataRequestResponseItem = InferOutput< typeof PersonaDataRequestResponseItem > export const PersonaDataRequestResponseItem = object({ @@ -107,30 +125,30 @@ export const PersonaDataRequestResponseItem = object({ phoneNumbers: optional(array(string())), }) -export type ResetRequestItem = Output +export type ResetRequestItem = InferOutput export const ResetRequestItem = object({ accounts: boolean(), personaData: boolean(), }) -export type LoginRequestResponseItem = Output -export const LoginRequestResponseItem = object( - { +export type LoginRequestResponseItem = InferOutput< + typeof LoginRequestResponseItem +> +export const LoginRequestResponseItem = pipe( + object({ persona: Persona, challenge: optional(string()), proof: optional(Proof), - }, - [ - custom((data) => { - if (data.challenge || data.proof) { - return !!(data.challenge && data.proof) - } - return true - }, 'missing challenge or proof'), - ], + }), + check((data) => { + if (data.challenge || data.proof) { + return !!(data.challenge && data.proof) + } + return true + }, 'missing challenge or proof'), ) -export type WalletUnauthorizedRequestItems = Output< +export type WalletUnauthorizedRequestItems = InferOutput< typeof WalletUnauthorizedRequestItems > export const WalletUnauthorizedRequestItems = object({ @@ -139,20 +157,22 @@ export const WalletUnauthorizedRequestItems = object({ oneTimePersonaData: optional(PersonaDataRequestItem), }) -export type AuthUsePersonaRequestItem = Output +export type AuthUsePersonaRequestItem = InferOutput< + typeof AuthUsePersonaRequestItem +> export const AuthUsePersonaRequestItem = object({ discriminator: literal('usePersona'), identityAddress: string(), }) -export type AuthLoginWithoutChallengeRequestItem = Output< +export type AuthLoginWithoutChallengeRequestItem = InferOutput< typeof AuthLoginWithoutChallengeRequestItem > export const AuthLoginWithoutChallengeRequestItem = object({ discriminator: literal('loginWithoutChallenge'), }) -export type AuthLoginWithChallengeRequestItem = Output< +export type AuthLoginWithChallengeRequestItem = InferOutput< typeof AuthLoginWithChallengeRequestItem > export const AuthLoginWithChallengeRequestItem = object({ @@ -169,26 +189,27 @@ export const AuthRequestItem = union([ AuthLoginRequestItem, ]) -export type WalletAuthorizedRequestItems = Output< +export type WalletAuthorizedRequestItems = InferOutput< typeof WalletAuthorizedRequestItems > export const WalletAuthorizedRequestItems = object({ discriminator: literal('authorizedRequest'), auth: AuthRequestItem, reset: optional(ResetRequestItem), + proofOfOwnership: optional(ProofOfOwnershipRequestItem), oneTimeAccounts: optional(AccountsRequestItem), ongoingAccounts: optional(AccountsRequestItem), oneTimePersonaData: optional(PersonaDataRequestItem), ongoingPersonaData: optional(PersonaDataRequestItem), }) -export type WalletRequestItems = Output +export type WalletRequestItems = InferOutput export const WalletRequestItems = union([ WalletUnauthorizedRequestItems, WalletAuthorizedRequestItems, ]) -export type SendTransactionItem = Output +export type SendTransactionItem = InferOutput export const SendTransactionItem = object({ transactionManifest: string(), version: number(), @@ -196,20 +217,20 @@ export const SendTransactionItem = object({ message: optional(string()), }) -export type WalletTransactionItems = Output +export type WalletTransactionItems = InferOutput export const WalletTransactionItems = object({ discriminator: literal('transaction'), send: SendTransactionItem, }) -export type SendTransactionResponseItem = Output< +export type SendTransactionResponseItem = InferOutput< typeof SendTransactionResponseItem > export const SendTransactionResponseItem = object({ transactionIntentHash: string(), }) -export type WalletTransactionResponseItems = Output< +export type WalletTransactionResponseItems = InferOutput< typeof WalletTransactionResponseItems > const WalletTransactionResponseItems = object({ @@ -217,19 +238,64 @@ const WalletTransactionResponseItems = object({ send: SendTransactionResponseItem, }) -export type CancelRequest = Output +export type CancelRequest = InferOutput export const CancelRequest = object({ discriminator: literal('cancelRequest'), }) -export type WalletInteractionItems = Output +export type ExpireAtTime = InferOutput +export const ExpireAtTime = object({ + discriminator: literal('expireAtTime'), + unixTimestampSeconds: number(), +}) + +export type ExpireAfterDelay = InferOutput +export const ExpireAfterDelay = object({ + discriminator: literal('expireAfterDelay'), + expireAfterSeconds: number(), +}) + +export type SubintentRequestItem = InferOutput +export const SubintentRequestItem = object({ + discriminator: literal('subintent'), + /** + * Version of the message interface + */ + version: number(), + /** + * Version of the Transaction Manifest + */ + manifestVersion: number(), + subintentManifest: string(), + blobs: optional(array(string())), + message: optional(string()), + expiration: union([ExpireAtTime, ExpireAfterDelay]), +}) + +export type SubintentResponseItem = InferOutput +export const SubintentResponseItem = object({ + expirationTimestamp: number(), + subintentHash: string(), + signedPartialTransaction: string(), +}) + +export type WalletPreAuthorizationItems = InferOutput< + typeof WalletPreAuthorizationItems +> +export const WalletPreAuthorizationItems = object({ + discriminator: literal('preAuthorizationRequest'), + request: optional(SubintentRequestItem), +}) + +export type WalletInteractionItems = InferOutput export const WalletInteractionItems = union([ WalletRequestItems, WalletTransactionItems, CancelRequest, + WalletPreAuthorizationItems, ]) -export type Metadata = Output +export type Metadata = InferOutput export const Metadata = object({ version: literal(2), networkId: number(), @@ -237,14 +303,14 @@ export const Metadata = object({ origin: string(), }) -export type WalletInteraction = Output +export type WalletInteraction = InferOutput export const WalletInteraction = object({ interactionId: string(), metadata: Metadata, items: WalletInteractionItems, }) -export type WalletUnauthorizedRequestResponseItems = Output< +export type WalletUnauthorizedRequestResponseItems = InferOutput< typeof WalletUnauthorizedRequestResponseItems > const WalletUnauthorizedRequestResponseItems = object({ @@ -253,7 +319,7 @@ const WalletUnauthorizedRequestResponseItems = object({ oneTimePersonaData: optional(PersonaDataRequestResponseItem), }) -export type AuthLoginWithoutChallengeRequestResponseItem = Output< +export type AuthLoginWithoutChallengeRequestResponseItem = InferOutput< typeof AuthLoginWithoutChallengeRequestResponseItem > export const AuthLoginWithoutChallengeRequestResponseItem = object({ @@ -261,7 +327,7 @@ export const AuthLoginWithoutChallengeRequestResponseItem = object({ persona: Persona, }) -export type AuthLoginWithChallengeRequestResponseItem = Output< +export type AuthLoginWithChallengeRequestResponseItem = InferOutput< typeof AuthLoginWithChallengeRequestResponseItem > export const AuthLoginWithChallengeRequestResponseItem = object({ @@ -271,12 +337,20 @@ export const AuthLoginWithChallengeRequestResponseItem = object({ proof: Proof, }) +export type WalletPreAuthorizationResponseItems = InferOutput< + typeof WalletPreAuthorizationResponseItems +> +export const WalletPreAuthorizationResponseItems = object({ + discriminator: literal('preAuthorizationResponse'), + response: optional(SubintentResponseItem), +}) + export const AuthLoginRequestResponseItem = union([ AuthLoginWithoutChallengeRequestResponseItem, AuthLoginWithChallengeRequestResponseItem, ]) -export type AuthUsePersonaRequestResponseItem = Output< +export type AuthUsePersonaRequestResponseItem = InferOutput< typeof AuthUsePersonaRequestResponseItem > const AuthUsePersonaRequestResponseItem = object({ @@ -284,25 +358,28 @@ const AuthUsePersonaRequestResponseItem = object({ persona: Persona, }) -export type AuthRequestResponseItem = Output +export type AuthRequestResponseItem = InferOutput< + typeof AuthRequestResponseItem +> export const AuthRequestResponseItem = union([ AuthUsePersonaRequestResponseItem, AuthLoginRequestResponseItem, ]) -export type WalletAuthorizedRequestResponseItems = Output< +export type WalletAuthorizedRequestResponseItems = InferOutput< typeof WalletAuthorizedRequestResponseItems > export const WalletAuthorizedRequestResponseItems = object({ discriminator: literal('authorizedRequest'), auth: AuthRequestResponseItem, + proofOfOwnership: optional(ProofOfOwnershipResponseItem), oneTimeAccounts: optional(AccountsRequestResponseItem), ongoingAccounts: optional(AccountsRequestResponseItem), oneTimePersonaData: optional(PersonaDataRequestResponseItem), ongoingPersonaData: optional(PersonaDataRequestResponseItem), }) -export type WalletRequestResponseItems = Output< +export type WalletRequestResponseItems = InferOutput< typeof WalletRequestResponseItems > export const WalletRequestResponseItems = union([ @@ -310,15 +387,16 @@ export const WalletRequestResponseItems = union([ WalletAuthorizedRequestResponseItems, ]) -export type WalletInteractionResponseItems = Output< +export type WalletInteractionResponseItems = InferOutput< typeof WalletInteractionResponseItems > const WalletInteractionResponseItems = union([ WalletRequestResponseItems, WalletTransactionResponseItems, + WalletPreAuthorizationResponseItems, ]) -export type WalletInteractionSuccessResponse = Output< +export type WalletInteractionSuccessResponse = InferOutput< typeof WalletInteractionSuccessResponse > export const WalletInteractionSuccessResponse = object({ @@ -327,7 +405,7 @@ export const WalletInteractionSuccessResponse = object({ items: WalletInteractionResponseItems, }) -export type WalletInteractionFailureResponse = Output< +export type WalletInteractionFailureResponse = InferOutput< typeof WalletInteractionFailureResponse > export const WalletInteractionFailureResponse = object({ @@ -337,7 +415,9 @@ export const WalletInteractionFailureResponse = object({ message: optional(string()), }) -export type WalletInteractionResponse = Output +export type WalletInteractionResponse = InferOutput< + typeof WalletInteractionResponse +> export const WalletInteractionResponse = union([ WalletInteractionSuccessResponse, WalletInteractionFailureResponse, @@ -355,7 +435,7 @@ export const StatusExtensionInteraction = object({ discriminator: literal(extensionInteractionDiscriminator.extensionStatus), }) -export type StatusExtensionInteraction = Output< +export type StatusExtensionInteraction = InferOutput< typeof StatusExtensionInteraction > @@ -364,11 +444,11 @@ export const OpenPopupExtensionInteraction = object({ discriminator: literal(extensionInteractionDiscriminator.openPopup), }) -export type OpenPopupExtensionInteraction = Output< +export type OpenPopupExtensionInteraction = InferOutput< typeof OpenPopupExtensionInteraction > -export type WalletInteractionExtensionInteraction = Output< +export type WalletInteractionExtensionInteraction = InferOutput< typeof WalletInteractionExtensionInteraction > @@ -379,7 +459,7 @@ export const WalletInteractionExtensionInteraction = object({ sessionId: optional(string()), }) -export type CancelWalletInteractionExtensionInteraction = Output< +export type CancelWalletInteractionExtensionInteraction = InferOutput< typeof CancelWalletInteractionExtensionInteraction > @@ -398,7 +478,7 @@ export const ExtensionInteraction = union([ CancelWalletInteractionExtensionInteraction, ]) -export type ExtensionInteraction = Output +export type ExtensionInteraction = InferOutput export const messageLifeCycleEventType = { extensionStatus: 'extensionStatus', @@ -416,7 +496,7 @@ export const MessageLifeCycleExtensionStatusEvent = object({ canHandleSessions: optional(boolean()), }) -export type MessageLifeCycleExtensionStatusEvent = Output< +export type MessageLifeCycleExtensionStatusEvent = InferOutput< typeof MessageLifeCycleExtensionStatusEvent > @@ -431,9 +511,9 @@ export const MessageLifeCycleEvent = object({ interactionId: string(), }) -export type MessageLifeCycleEvent = Output +export type MessageLifeCycleEvent = InferOutput -export type IncomingMessage = Output +export type IncomingMessage = InferOutput const IncomingMessage = union([ MessageLifeCycleEvent, WalletInteractionResponse, @@ -476,25 +556,21 @@ export const SignalingServerMessage = object({ connectionId: optional(string()), // redundant, to be removed }) -export const AnswerIO = merge([ - SignalingServerMessage, - object({ - method: Answer, - payload: object({ - sdp: string(), - }), +export const AnswerIO = object({ + ...SignalingServerMessage.entries, + method: Answer, + payload: object({ + sdp: string(), }), -]) +}) -export const OfferIO = merge([ - SignalingServerMessage, - object({ - method: Offer, - payload: object({ - sdp: string(), - }), +export const OfferIO = object({ + ...SignalingServerMessage.entries, + method: Offer, + payload: object({ + sdp: string(), }), -]) +}) export const IceCandidatePayloadIO = object({ candidate: string(), @@ -502,28 +578,24 @@ export const IceCandidatePayloadIO = object({ sdpMLineIndex: number(), }) -export const IceCandidateIO = merge([ - SignalingServerMessage, - object({ - method: IceCandidate, - payload: IceCandidatePayloadIO, - }), -]) +export const IceCandidateIO = object({ + ...SignalingServerMessage.entries, + method: IceCandidate, + payload: IceCandidatePayloadIO, +}) -export const IceCandidatesIO = merge([ - SignalingServerMessage, - object({ - method: IceCandidates, - payload: array(IceCandidatePayloadIO), - }), -]) +export const IceCandidatesIO = object({ + ...SignalingServerMessage.entries, + method: IceCandidates, + payload: array(IceCandidatePayloadIO), +}) -export type Answer = Output -export type Offer = Output -export type IceCandidate = Output -export type IceCandidates = Output -export type MessagePayloadTypes = Output -export type MessageSources = Output +export type Answer = InferOutput +export type Offer = InferOutput +export type IceCandidate = InferOutput +export type IceCandidates = InferOutput +export type MessagePayloadTypes = InferOutput +export type MessageSources = InferOutput export type DataTypes = Answer | IceCandidate | Offer | IceCandidates @@ -568,7 +640,7 @@ export type InvalidMessageError = { export type ValidationError = { info: 'validationError' requestId: DataTypes['requestId'] - error: ValiError + error: ValiError } export type SignalingServerResponse = diff --git a/packages/dapp-toolkit/tsup.config.ts b/packages/dapp-toolkit/tsup.config.ts index 5b3ccb6f..1f65dac4 100644 --- a/packages/dapp-toolkit/tsup.config.ts +++ b/packages/dapp-toolkit/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: ['src/index.ts', 'src/connect-button.ts'], - dts: true, + dts: { resolve: true }, format: ['esm', 'cjs'], noExternal: ['@radixdlt/connect-button', 'radix-connect-common'], }) diff --git a/packages/dapp-toolkit/vite-single-file.config.ts b/packages/dapp-toolkit/vite-single-file.config.ts index 2bc7f56f..482efd99 100644 --- a/packages/dapp-toolkit/vite-single-file.config.ts +++ b/packages/dapp-toolkit/vite-single-file.config.ts @@ -3,12 +3,20 @@ import { viteSingleFile } from 'vite-plugin-singlefile' export default defineConfig({ plugins: [viteSingleFile()], + build: { emptyOutDir: false, + lib: { entry: 'src/single-file.js', name: 'RDT', fileName: 'radix-dapp-toolkit.bundle', + formats: ['umd'], + }, + rollupOptions: { + output: { + entryFileNames: `radix-dapp-toolkit.bundle.umd.js`, + }, }, }, define: { 'process.env.NODE_ENV': '"production"' }, diff --git a/packages/dapp-toolkit/vitest.config.ts b/packages/dapp-toolkit/vitest.config.ts index 95c5d26e..226fc0cf 100644 --- a/packages/dapp-toolkit/vitest.config.ts +++ b/packages/dapp-toolkit/vitest.config.ts @@ -4,5 +4,12 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'jsdom', + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.testing-module.ts', 'src/**/*.spec.ts'], + reporter: ['lcov', 'text', 'html'], + }, + include: ['src/**/*.spec.ts'], }, })