diff --git a/integrations/viem-v2/README.md b/integrations/viem-v2/README.md index 24fb36c5..d2d01345 100644 --- a/integrations/viem-v2/README.md +++ b/integrations/viem-v2/README.md @@ -2,41 +2,60 @@ A plugin for [Viem] 2.x that encrypts transactions, gas estimations and calls to the Oasis Sapphire network to enable end-to-end encryption between the dApp and -smart contracts. +smart contracts. It provides three functions which can be used together or +separately, see the guides below for usage instructions: + + * `sapphireHttpTransport` - [Transport] that intercepts & encrypts requests. + * `wrapWalletClient` - Wraps a [WalletClient], to provide encryption. + * `createSapphireSerializer` - [Transaction serializer], that encrypts calldata. [Viem]: https://viem.sh/ +[WalletClient]: https://viem.sh/docs/clients/wallet.html +[Transaction serializer]: https://viem.sh/docs/chains/serializers#api +[Transport]: https://viem.sh/docs/clients/transports/http ## Usage -First install the package. +Add the package to your project: ``` npm install @oasisprotocol/sapphire-viem-v2 viem@2.x ``` -Next you must ensure that any clients use the `sapphireHttpTransport()` to encrypt -any unsigned communication, for example when using [hardhat-viem] pass the -`transport` parameter when constructing a Public Client: +For local testing the chain configuration for `sapphireLocalnet` is available +in the `@oasisprotocol/sapphire-viem-v2` package installed above. The Sapphire +`mainnet` and `testnet` configurations are available in `viem/chains`: -[hardhat-viem]: https://hardhat.org/hardhat-runner/docs/advanced/using-viem +```ts +import { sapphireLocalnet } from '@oasisprotocol/sapphire-viem-v2'; +import { sapphire, sapphireTestnet } from 'viem/chains'; +``` -```typescript -import { sapphireLocalnet, sapphireHttpTransport } from '@oasisprotocol/sapphire-viem-v2'; +## Encryption -const publicClient = await hre.viem.getPublicClient({ - chain: sapphireLocalnet, - transport: sapphireHttpTransport() -}); -``` +Use the `sapphireHttpTransport()` transport to automatically intercept and +encrypt the calldata provided to the `eth_estimateGas`, `eth_call` and +`eth_sendTransaction` JSON-RPC calls. -The Sapphire transport will only encrypt transactions if connected to an -in-browser wallet provider which accepts `eth_sendTransaction` calls. To encrypt -transactions when using a local wallet client you must not only provide the -`transport` parameter, but must also wrap the wallet client, as such: +Transaction calldata will be encrypted while using the in-browser wallet +provider (`window.ethereum`) because this uses the `eth_sendTransaction` +JSON-RPC call to sign transactions. + +> [!IMPORTANT] +> To encrypt transactions when using a local wallet client you must not only +> provide the `transport` parameter, but must also wrap the wallet client. + +This example creates local wallet that is then wrapped with `wrapWalletClient` +which replaces the transaction serializer with one which performs Sapphire +encryption: ```typescript +import { createWalletClient } from 'viem' +import { english, generateMnemonic, mnemonicToAccount } from 'viem/accounts'; import { sapphireLocalnet, sapphireHttpTransport, wrapWalletClient } from '@oasisprotocol/sapphire-viem-v2'; +const account = mnemonicToAccount(generateMnemonic(english)); + const walletClient = await wrapWalletClient(createWalletClient({ account, chain: sapphireLocalnet, @@ -44,10 +63,6 @@ const walletClient = await wrapWalletClient(createWalletClient({ })); ``` -### Chains - -The chain configuration for `sapphireLocalnet` is available in the `@oasisprotocol/sapphire-viem-v2` package as seen above. -The Sapphire `mainnet` and `testnet` configurations are available in `viem/chains` -```ts -import { sapphire, sapphireTestnet } from 'viem/chains'; -``` +Be careful to verify that any transactions which contain sensitive information +are encrypted by checking the Oasis block explorer and looking for the green +lock icon. diff --git a/integrations/viem-v2/hardhat.config.cjs b/integrations/viem-v2/hardhat.config.cjs new file mode 100644 index 00000000..7a850141 --- /dev/null +++ b/integrations/viem-v2/hardhat.config.cjs @@ -0,0 +1,2 @@ +require("@nomicfoundation/hardhat-viem"); +module.exports = {}; diff --git a/integrations/viem-v2/package.json b/integrations/viem-v2/package.json index 66bff0f7..217d4335 100644 --- a/integrations/viem-v2/package.json +++ b/integrations/viem-v2/package.json @@ -60,6 +60,7 @@ "devDependencies": { "@biomejs/biome": "^1.7.0", "@types/node": "^18.19.31", + "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.4.5", "vitest": "^1.6.0" diff --git a/integrations/viem-v2/test/docs.test.ts b/integrations/viem-v2/test/docs.test.ts new file mode 100644 index 00000000..bb92ef1e --- /dev/null +++ b/integrations/viem-v2/test/docs.test.ts @@ -0,0 +1,39 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rimraf } from "rimraf"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +const EXTRACT_CODE_RX = /\`\`\`(?\w+)\s*(?[\s\S]*?)\`\`\`/gm; + +function extractCode(markdown: string) { + return Array.from(markdown.matchAll(EXTRACT_CODE_RX), (_) => { + return { + lang: _.groups?.lang, + content: _.groups?.content, + }; + }); +} + +const TEST_DIR = join(process.cwd(), "test-modules"); + +describe("README.md", async () => { + beforeAll(async () => { + await mkdir(TEST_DIR, { recursive: true }); + }); + afterAll(async () => { + await rimraf(TEST_DIR); + }); + for (const [index, c] of extractCode( + await readFile("./README.md", "utf8"), + ).entries()) { + const content = c.content; + if (content) { + it(`Code snippet #${index + 1}`, async () => { + const fileName = join(TEST_DIR, `example_${index + 1}.ts`); + await writeFile(fileName, content); + console.log(fileName, content); + await expect(import(fileName)).resolves.not.toThrow(); + }); + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 574ff69f..571d1fd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: clients/js: @@ -559,6 +563,9 @@ importers: '@types/node': specifier: ^18.19.31 version: 18.19.31 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@18.19.31)(typescript@5.4.5) @@ -579,7 +586,7 @@ importers: version: link:../viem-v2 '@wagmi/core': specifier: 2.x - version: 2.6.17(@types/react@18.2.79)(react@18.2.0)(typescript@5.4.5)(viem@2.9.19) + version: 2.6.17(react@18.2.0)(typescript@5.4.5)(viem@2.13.8) viem: specifier: 2.x version: 2.13.8(typescript@5.4.5) @@ -1305,6 +1312,16 @@ packages: '@babel/helper-plugin-utils': 7.24.0 dev: true + /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.23.5): + resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} engines: {node: '>=6.9.0'} @@ -1313,6 +1330,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: false /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} @@ -1388,6 +1406,16 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} @@ -2467,6 +2495,20 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + /@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5) + '@babel/types': 7.24.0 + dev: true + /@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.24.4): resolution: {integrity: sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==} engines: {node: '>=6.9.0'} @@ -4419,6 +4461,18 @@ packages: deprecated: Use @eslint/object-schema instead dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@isaacs/ttlcache@1.4.1: resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -8724,6 +8778,32 @@ packages: - zod dev: false + /@wagmi/core@2.6.17(react@18.2.0)(typescript@5.4.5)(viem@2.13.8): + resolution: {integrity: sha512-Ghr7PlD5HO1YJrsaC52j/csgaigBAiTR7cFiwrY7WdwvWLsR5na4Dv6KfHTU3d3al0CKDLanQdRS5nB4mX1M+g==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.5(typescript@5.4.5) + typescript: 5.4.5 + viem: 2.13.8(typescript@5.4.5) + zustand: 4.4.1(@types/react@18.2.79)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - utf-8-validate + - zod + dev: false + /@walletconnect/core@2.11.0: resolution: {integrity: sha512-2Tjp5BCevI7dbmqo/OrCjX4tqgMqwJNQLlQAlphqPfvwlF9+tIu6pGcVbSN3U9zyXzWIZCeleqEaWUeSeET4Ew==} dependencies: @@ -10175,6 +10255,11 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /antlr4@4.13.0: resolution: {integrity: sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==} engines: {node: '>=16'} @@ -12725,6 +12810,10 @@ packages: stream-shift: 1.0.3 dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} dependencies: @@ -13279,8 +13368,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.4) - '@babel/plugin-transform-react-jsx': 7.23.4(@babel/core@7.24.4) + '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.23.5) + '@babel/plugin-transform-react-jsx': 7.23.4(@babel/core@7.23.5) eslint: 8.57.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -13897,6 +13986,7 @@ packages: dependencies: is-hex-prefixed: 1.0.0 strip-hex-prefix: 1.0.0 + bundledDependencies: false /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} @@ -14598,6 +14688,19 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true + /glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + dev: true + /glob@5.0.15: resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} deprecated: Glob versions prior to v9 are no longer supported @@ -16079,6 +16182,13 @@ packages: set-function-name: 2.0.1 dev: true + /jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + dependencies: + '@isaacs/cliui': 8.0.2 + dev: true + /jake@10.8.7: resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} engines: {node: '>=10'} @@ -17708,6 +17818,11 @@ packages: engines: {node: 14 || >=16.14} dev: false + /lru-cache@11.0.1: + resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==} + engines: {node: 20 || >=22} + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -18151,6 +18266,13 @@ packages: /minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + /minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimatch@3.0.4: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: @@ -18190,6 +18312,11 @@ packages: yallist: 3.1.1 dev: true + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /minizlib@1.3.3: resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==} dependencies: @@ -19032,6 +19159,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: true + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -19121,6 +19252,14 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + /path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + dependencies: + lru-cache: 11.0.1 + minipass: 7.1.2 + dev: true + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} dev: true @@ -21096,6 +21235,15 @@ packages: dependencies: glob: 7.2.3 + /rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + dependencies: + glob: 11.0.0 + package-json-from-dist: 1.0.1 + dev: true + /ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} dependencies: @@ -22141,6 +22289,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} dependencies: @@ -23181,7 +23338,7 @@ packages: typescript: '>=4.3.0' dependencies: '@types/prettier': 2.7.3 - debug: 4.3.5 + debug: 4.3.4(supports-color@8.1.1) fs-extra: 7.0.1 glob: 7.1.7 js-sha3: 0.8.0 @@ -25265,6 +25422,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}