diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index b6fcc09e..24651837 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -13,6 +13,7 @@ jobs: - "tests/erc20-paymaster.spec.ts" - "tests/how-to-test-contracts.spec.ts" - "tests/daily-spend-limit.spec.ts" + - "tests/signing-txns-with-webauthn.spec.ts" steps: - uses: actions/checkout@v4 diff --git a/code/webauthn/frontend/src/pages/api/generate-authentication-options.ts b/code/webauthn/frontend/src/pages/api/get-authentication-options.ts similarity index 100% rename from code/webauthn/frontend/src/pages/api/generate-authentication-options.ts rename to code/webauthn/frontend/src/pages/api/get-authentication-options.ts diff --git a/code/webauthn/frontend/utils/webauthn.ts b/code/webauthn/frontend/utils/webauthn.ts index 1672826c..34e8aa63 100644 --- a/code/webauthn/frontend/utils/webauthn.ts +++ b/code/webauthn/frontend/utils/webauthn.ts @@ -8,7 +8,7 @@ import type { TransactionRequest } from 'zksync-ethers/src/types'; import * as cbor from 'cbor'; export async function authenticate(challenge: string) { - const resp = await fetch('http://localhost:3000/api/generate-authentication-options', { + const resp = await fetch('http://localhost:3000/api/get-authentication-options', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/content/tutorials/signing-transactions-with-webauthn/20.building-the-contracts.md b/content/tutorials/signing-transactions-with-webauthn/20.building-the-contracts.md index 4083caa1..566d509a 100644 --- a/content/tutorials/signing-transactions-with-webauthn/20.building-the-contracts.md +++ b/content/tutorials/signing-transactions-with-webauthn/20.building-the-contracts.md @@ -8,6 +8,8 @@ description: Build the contracts for your app. Make a new folder for the project called `zksync-webauthn` and navigate to that folder in your terminal: +:test-action{actionId="make-project-folder"} + ```shell mkdir zksync-webauthn cd zksync-webauthn @@ -15,10 +17,14 @@ cd zksync-webauthn We can start by creating the contracts for the smart account, paymaster, and NFT. +:test-action{actionId="initialize-contracts"} + ```shell -zksync-cli create contracts --template hardhat_solidity --project contracts +npx zksync-cli create contracts --template hardhat_solidity --project contracts ``` +:test-action{actionId="wait-for-init"} + The CLI will prompt you to enter a private key for deploying. Enter the private key below for a pre-configured rich wallet: @@ -27,16 +33,31 @@ Enter the private key below for a pre-configured rich wallet: 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 ``` +:test-action{actionId="add-env-pk"} + Once that is done, move into the `contracts` folders and install the dependency below: -```shell +:test-action{actionId="install-deps"} + +::code-group + +```bash [npm] cd contracts npm install -D @matterlabs/hardhat-zksync-deploy@1.3.0 ``` +```bash [yarn] +cd contracts +yarn add -D @matterlabs/hardhat-zksync-deploy@1.3.0 +``` + +:: + Then, delete the template contracts, scripts, and tests: +:test-action{actionId="remove-template-files"} + ```shell rm -rf ./contracts/* rm -rf ./deploy/* @@ -58,10 +79,15 @@ We will be creating 4 contracts: Create a new file in the `contracts/contracts` folder called `GeneralPaymaster.sol`. +:test-action{actionId="create-paymaster"} + ```shell touch contracts/GeneralPaymaster.sol ``` +:test-action{actionId="open-paymaster"} +:test-action{actionId="paymaster-contract-code"} + ::drop-panel ::panel{label="GeneralPaymaster.sol"} @@ -78,10 +104,15 @@ This is a basic paymaster contract that allows us to sponsor transactions for us Create another file in the `contracts/contracts` folder called `MyNFT.sol`. +:test-action{actionId="create-nft-contract"} + ```shell touch contracts/MyNFT.sol ``` +:test-action{actionId="open-nft"} +:test-action{actionId="nft-contract-code"} + ::drop-panel ::panel{label="MyNFT.sol"} @@ -99,10 +130,15 @@ We will use this to test interacting with smart contracts using WebAuthn. Create a file in the `contracts/contracts` folder called `AAFactory.sol`. +:test-action{actionId="create-aa-factory"} + ```shell touch contracts/AAFactory.sol ``` +:test-action{actionId="open-aa-factory"} +:test-action{actionId="aa-factory-contract-code"} + ::drop-panel ::panel{label="AAFactory.sol"} @@ -119,10 +155,15 @@ This contract is a factory contract responsbile for deploying new instances of t Finally, create a file in the `contracts/contracts` folder called `Account.sol`. +:test-action{actionId="create-account-contract"} + ```shell touch contracts/Account.sol ``` +:test-action{actionId="open-account"} +:test-action{actionId="account-contract-code"} + ::drop-panel ::panel{label="Account.sol"} @@ -193,12 +234,17 @@ The output data should be 1 (in 32 bytes format) if the signature verification p Open a new terminal and start a local in-memory node with `era_test_node`: +:test-action{actionId="start-era-test-node"} + ```shell era_test_node run ``` Next, replace your `hardhat.config.ts` file with the file below: +:test-action{actionId="open-hardhat-config"} +:test-action{actionId="hardhat-config"} + ::drop-panel ::panel{label="hardhat.config.ts"} @@ -213,12 +259,17 @@ Next, replace your `hardhat.config.ts` file with the file below: Create a new file inside the `deploy` folder called `deploy.ts`: +:test-action{actionId="make-deploy-script"} + ```shell touch deploy/deploy.ts ``` Copy and paste the code below. +:test-action{actionId="open-deploy-script"} +:test-action{actionId="deploy-script"} + ::drop-panel ::panel{label="deploy.ts"} @@ -239,11 +290,22 @@ This script will: Finally, compile and deploy the contracts with: -```shell +:test-action{actionId="compile-and-deploy"} + +::code-group + +```bash [npm] npm run compile npm run deploy ``` +```bash [yarn] +yarn compile +yarn deploy +``` + +:: + Save the output of this command, as we will use these deployed contract addresses in the frontend. That's all for the contracts! diff --git a/content/tutorials/signing-transactions-with-webauthn/30.building-the-frontend.md b/content/tutorials/signing-transactions-with-webauthn/30.building-the-frontend.md index e87b6fef..7e8eae89 100644 --- a/content/tutorials/signing-transactions-with-webauthn/30.building-the-frontend.md +++ b/content/tutorials/signing-transactions-with-webauthn/30.building-the-frontend.md @@ -11,6 +11,8 @@ We will use Next.js to build the frontend. Move out of your `contract` folder and back into the `zksync-webauthn` folder. Make a new Next.js project for the frontend using the configuration below: +:test-action{actionId="init-next-app"} + ```shell npx create-next-app@latest ``` @@ -25,13 +27,28 @@ npx create-next-app@latest ✔ Would you like to customize the default import alias (@/*)? No ``` +:test-action{actionId="wait-for-nextjs-init"} + Move into your projects and install the dependencies below: -```shell +:test-action{actionId="install-nextjs-deps"} + +::code-group + +```bash [npm] cd frontend npm install @simplewebauthn/browser@10.0.0 @simplewebauthn/server@10.0.1 cbor@9.0.2 ethers@5.7.2 zksync-ethers@5.1.0 ``` +```bash [yarn] +cd frontend +yarn add @simplewebauthn/browser@10.0.0 @simplewebauthn/server@10.0.1 cbor@9.0.2 ethers@5.7.2 zksync-ethers@5.1.0 +``` + +:: + +:test-action{actionId="wait-for-nextjs-deps"} + ## Home Page We can start by replacing the home page with one that has four links for each page we will make. @@ -40,6 +57,9 @@ Replace your `frontend/src/pages/index.tsx` file with the code below. Then, replace the `AA_FACTORY_ADDRESS`, `NFT_CONTRACT_ADDRESS`, and `PAYMASTER_ADDRESS` variables at the top with the deployed addresses from the last section. +:test-action{actionId="open-home-page"} +:test-action{actionId="home-page-code"} + ::drop-panel ::panel{label="index.tsx"} @@ -50,12 +70,20 @@ variables at the top with the deployed addresses from the last section. :: :: +:test-action{actionId="create-env-file"} +:test-action{actionId="extract-aa-factory-address"} +:test-action{actionId="extract-nft-contract-address"} +:test-action{actionId="extract-paymaster-contract-address"} +:test-action{actionId="replace-deployed-contract-address"} + ### Layout Next, let's create the `Layout` component used in the home page. Create a `src/components` folder and a new file inside called `Layout.tsx`. +:test-action{actionId="make-layout-component"} + ```shell mkdir src/components touch src/components/Layout.tsx @@ -63,6 +91,9 @@ touch src/components/Layout.tsx Copy the `Layout` component below to your file: +:test-action{actionId="open-layout-component"} +:test-action{actionId="layout-component-code"} + ::drop-panel ::panel{label="Layout.tsx"} @@ -77,6 +108,9 @@ Copy the `Layout` component below to your file: Let's also update the styles in `src/styles/globals.css`. +:test-action{actionId="open-styles"} +:test-action{actionId="update-styles"} + ::drop-panel ::panel{label="globals.css"} @@ -96,10 +130,15 @@ We need to do this because some parts of the WebAuthn ceremonies must run on a s Create a new file in the `src/pages/api` folder called `get-registration-options.ts`. +:test-action{actionId="make-registration-api"} + ```shell touch src/pages/api/get-registration-options.ts ``` +:test-action{actionId="open-registration-api"} +:test-action{actionId="registration-api-code"} + ::drop-panel ::panel{label="get-registration-options.ts"} @@ -118,23 +157,28 @@ You can learn more about the options available for registration in the [`simplew ### Get Authentication Options -Create another file in the `src/pages/api` folder called `generate-authentication-options.ts`. +Create another file in the `src/pages/api` folder called `get-authentication-options.ts`. + +:test-action{actionId="make-auth-api"} ```shell -touch src/pages/api/generate-authentication-options.ts +touch src/pages/api/get-authentication-options.ts ``` +:test-action{actionId="open-auth-api"} +:test-action{actionId="auth-api-code"} + ::drop-panel -::panel{label="generate-authentication-options.ts"} +::panel{label="get-authentication-options.ts"} -```ts [frontend/src/pages/api/generate-authentication-options.ts] -:code-import{filePath="webauthn/frontend/src/pages/api/generate-authentication-options.ts"} +```ts [frontend/src/pages/api/get-authentication-options.ts] +:code-import{filePath="webauthn/frontend/src/pages/api/get-authentication-options.ts"} ``` :: :: -The `generate-authentication-options` endpoint is used to generate the options input object for the WebAuthn authentication ceremony. +The `get-authentication-options` endpoint is used to generate the options input object for the WebAuthn authentication ceremony. You can learn more about the options available for authentication in the [`simplewebauthn` docs](https://simplewebauthn.dev/docs/packages/server#1-generate-authentication-options). The challenge passed in through the request body represents the transaction data that would normally be signed by a wallet like Metamask. @@ -151,6 +195,8 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers'; Next, let's make a new folder called `utils` where we can add some utility functions. +:test-action{actionId="make-utils-folder"} + ```shell mkdir utils ``` @@ -159,10 +205,15 @@ mkdir utils Create a new file inside the `utils` folder called `string.ts` to help handle some string conversions we will need to work with hex values and base64 URLs. +:test-action{actionId="make-strings-utils"} + ```shell touch utils/string.ts ``` +:test-action{actionId="open-strings-utils"} +:test-action{actionId="strings-utils-code"} + ::drop-panel ::panel{label="string.ts"} @@ -177,10 +228,15 @@ touch utils/string.ts Next, create a file called `tx.ts`. +:test-action{actionId="make-tx-utils"} + ```shell touch utils/tx.ts ``` +:test-action{actionId="open-tx-utils"} +:test-action{actionId="tx-utils-code"} + ::drop-panel ::panel{label="tx.ts"} @@ -207,12 +263,17 @@ This means that wallet passed into this function must be the same wallet created ### WebAuthn Utils -The last file we need to add in the `utils` folder is called `webauthn.ts`, +The last file we need to add in the `utils` folder is called `webauthn.ts`. + +:test-action{actionId="make-webauthn-utils"} ```shell touch utils/webauthn.ts ``` +:test-action{actionId="open-webauthn-utils"} +:test-action{actionId="webauthn-utils-code"} + ::drop-panel ::panel{label="webauthn.ts"} @@ -230,7 +291,7 @@ send transactions with a WebAuthn signature. and passes them into the `startAuthentication` method provided by `@simplewebauthn`. - The `signAndSend` function takes the response from the authentication process and processes it so it's in the correct format for the smart account contract. -Remember that WebAuthn authentication process returns three pieces of information that we need for validation: +Remember that the WebAuthn authentication process returns three pieces of information that we need for validation: the WebAuthn signature, the `authenticatorData`, and the `clientData`. The `getSignatureFromAuthResponse` function encodes the `rs` values from the signature along with the `authenticatorData` and the `clientData` into one signature that can be passed into the smart contract via the transaction `customData.customSignature`. diff --git a/content/tutorials/signing-transactions-with-webauthn/40.completing-the-frontend.md b/content/tutorials/signing-transactions-with-webauthn/40.completing-the-frontend.md index 3ff42c35..9c4b52cd 100644 --- a/content/tutorials/signing-transactions-with-webauthn/40.completing-the-frontend.md +++ b/content/tutorials/signing-transactions-with-webauthn/40.completing-the-frontend.md @@ -8,6 +8,8 @@ description: Complete the frontend for your app. Next, create a `src/hooks` folder and add the hooks below to manage the created smart account. +:test-action{actionId="make-hooks-dir"} + ```shell mkdir src/hooks ``` @@ -23,10 +25,15 @@ They should only be used for demonstration purposes. Create a new file in the `hooks` folder called `useAccount.tsx`. +:test-action{actionId="make-account-hook"} + ```shell touch src/hooks/useAccount.tsx ``` +:test-action{actionId="open-account-hook"} +:test-action{actionId="account-hook-code"} + ::drop-panel ::panel{label="useAccount.tsx"} @@ -44,10 +51,15 @@ This allows the app to "remember" the created account after the user has closed Create another file in the `hooks` folder called `useWallet.tsx`. +:test-action{actionId="make-wallet-hook"} + ```shell touch src/hooks/useWallet.tsx ``` +:test-action{actionId="open-wallet-hook"} +:test-action{actionId="wallet-hook-code"} + ::drop-panel ::panel{label="useWallet.tsx"} @@ -71,6 +83,9 @@ However, the user could still transfer ETH and mint the NFT as the passkey and p Import the hooks into your `src/pages/_app.tsx` file and wrap your app in the hook providers. +:test-action{actionId="open-app"} +:test-action{actionId="app-code"} + ::drop-panel ::panel{label="_app.tsx"} @@ -93,10 +108,15 @@ Inside the `frontend/src/pages` folder we will create four new pages: `create-ac Create a new file in the `src/pages` folder called `create-account.tsx`. +:test-action{actionId="make-create-account"} + ```shell touch src/pages/create-account.tsx ``` +:test-action{actionId="open-create-account"} +:test-action{actionId="create-account-code"} + ::drop-panel ::panel{label="create-account.tsx"} @@ -116,10 +136,15 @@ For testing purposes, when a new account is created, some test funds are sent to Create a new file in the `src/pages` folder called `register.tsx`. +:test-action{actionId="make-register"} + ```shell touch src/pages/register.tsx ``` +:test-action{actionId="open-register"} +:test-action{actionId="register-code"} + ::drop-panel ::panel{label="register.tsx"} @@ -146,10 +171,15 @@ Once the passkey is registered to the browser, the public key is extraced from t Create a new file in the `src/pages` folder called `transfer.tsx`. +:test-action{actionId="make-transfer"} + ```shell touch src/pages/transfer.tsx ``` +:test-action{actionId="open-transfer"} +:test-action{actionId="transfer-code"} + ::drop-panel ::panel{label="transfer.tsx"} @@ -172,10 +202,15 @@ which finally gets passed to the `signAndSend` function. Create a new file in the `src/pages` folder called `mint.tsx`. +:test-action{actionId="make-mint"} + ```shell touch src/pages/mint.tsx ``` +:test-action{actionId="open-mint"} +:test-action{actionId="mint-code"} + ::drop-panel ::panel{label="mint.tsx"} @@ -196,10 +231,20 @@ Once the transaction is complete, it looks through the logs to find the token ID Start the frontend app by running the command below in the `frontend` folder: -```shell +:test-action{actionId="run-frontend"} + +::code-group + +```bash [npm] npm run dev ``` +```bash [yarn] +yarn dev +``` + +:: + You can now try out the app! Follow the order of the buttons on the home page: @@ -209,3 +254,9 @@ Follow the order of the buttons on the home page: 1. Finally, you can use the registered passkey to transfer ETH and mint an NFT. Note: You can edit or remove your browser passkeys by going to `chrome://settings/passkeys`. + +:test-action{actionId="wait-for-frontend"} +:test-action{actionId="visit-frontend"} +:test-action{actionId="create-new-account"} +:test-action{actionId="wait-for-account"} +:test-action{actionId="verify-account-made"} diff --git a/playwright.config.ts b/playwright.config.ts index ca3ff957..ad89c8a7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -72,8 +72,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'bun run dev', - url: 'http://localhost:3000', + command: 'PORT=3030 bun dev', + url: 'http://localhost:3030', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, }, diff --git a/server/plugins/content.ts b/server/plugins/content.ts index f2b30431..963cb19c 100644 --- a/server/plugins/content.ts +++ b/server/plugins/content.ts @@ -16,10 +16,29 @@ export default defineNitroPlugin((nitroApp) => { function handleCodeImport(body: string) { const lines = body.split(EOL); + let inCodeBlock = false; + let codeBlockIndent = ''; + for (let i = 0; i < lines.length; i++) { - if (lines[i].includes(':code-import{filePath')) { - const filepath = lines[i].split('"')[1]; - const newCode = getCodeFromFilepath(filepath); + const trimmedLine = lines[i].trim(); + + if (trimmedLine.startsWith('```')) { + inCodeBlock = !inCodeBlock; + if (inCodeBlock) { + const matches = lines[i].match(/^\s*/); + codeBlockIndent = matches ? matches[0] : ''; + } + } + + if (inCodeBlock && trimmedLine.includes(':code-import{filePath')) { + const filepath = trimmedLine.split('"')[1]; + let newCode = getCodeFromFilepath(filepath); + + newCode = newCode + .split(EOL) + .map((line) => codeBlockIndent + line) + .join(EOL); + lines[i] = newCode; } } @@ -37,13 +56,16 @@ function getCodeFromFilepath(filepath: string) { } else { const fullPath = join(process.cwd(), 'code', cleanPath); code = readFileSync(fullPath, 'utf8'); + files.set(filepath, code); } const exampleComment = splitPath[1] || null; if (exampleComment) { code = extractCommentBlock(code, exampleComment); } - files.set(filepath, code); - return code; + // remove any other ANCHOR tags + const lines = code.split(EOL); + const trimmedLines = lines.filter((line) => !line.trimStart().startsWith('// ANCHOR')); + return trimmedLines.join(EOL); } function extractCommentBlock(content: string, comment: string | null) { @@ -70,12 +92,6 @@ function extractCommentBlock(content: string, comment: string | null) { } } const newLines = lines.slice(lineStart, lineEnd); - - // remove any other example tags - const trimmedLines = newLines.filter((line) => { - const thisLine = line.trimStart(); - return thisLine.startsWith('// ANCHOR') === false; - }); - const linesContent = trimmedLines.join(EOL); + const linesContent = newLines.join(EOL); return linesContent; } diff --git a/tests/configs/config.ts b/tests/configs/config.ts index 925f8f05..b0969c91 100644 --- a/tests/configs/config.ts +++ b/tests/configs/config.ts @@ -1,6 +1,7 @@ import { steps as erc20PaymasterSteps } from './erc20-paymaster'; import { steps as howToTestContractsSteps } from './how-to-test-contracts'; import { steps as dailySpendLimitSteps } from './daily-spend-limit'; +import { steps as signingWithWebAuthnSteps } from './signing-txns-with-webauthn'; export function getConfig(tutorialName: string) { let steps; @@ -14,6 +15,9 @@ export function getConfig(tutorialName: string) { case 'daily-spend-limit': steps = dailySpendLimitSteps; break; + case 'signing-txns-with-webauthn': + steps = signingWithWebAuthnSteps; + break; default: break; } diff --git a/tests/configs/signing-txns-with-webauthn.ts b/tests/configs/signing-txns-with-webauthn.ts new file mode 100644 index 00000000..3405ded9 --- /dev/null +++ b/tests/configs/signing-txns-with-webauthn.ts @@ -0,0 +1,378 @@ +import type { IStepConfig } from '../utils/types'; + +const contractSteps: IStepConfig = { + 'make-project-folder': { + action: 'runCommand', + }, + 'initialize-contracts': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn', + prompts: 'Private key of the wallet: |❯ npm: ', + }, + 'wait-for-init': { + action: 'wait', + timeout: 5000, + }, + 'add-env-pk': { + action: 'modifyFile', + filepath: 'tests-output/zksync-webauthn/contracts/.env', + useSetData: 'WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110', + atLine: 1, + removeLines: [1], + }, + 'install-deps': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn', + }, + 'wait-for-install': { + action: 'wait', + timeout: 5000, + }, + 'remove-template-files': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'create-paymaster': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'open-paymaster': { + action: 'clickButtonByText', + buttonText: 'GeneralPaymaster.sol', + }, + 'paymaster-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/contracts/GeneralPaymaster.sol', + }, + 'create-nft-contract': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'open-nft': { + action: 'clickButtonByText', + buttonText: 'MyNFT.sol', + }, + 'nft-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/contracts/MyNFT.sol', + }, + 'create-aa-factory': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'open-aa-factory': { + action: 'clickButtonByText', + buttonText: 'AAFactory.sol', + }, + 'aa-factory-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/contracts/AAFactory.sol', + }, + 'create-account-contract': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'open-account': { + action: 'clickButtonByText', + buttonText: 'Account.sol', + }, + 'account-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/contracts/Account.sol', + }, + 'start-era-test-node': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + preCommand: "bun pm2 start '' --name era-test-node", + }, + 'open-hardhat-config': { + action: 'clickButtonByText', + buttonText: 'hardhat.config.ts', + }, + 'hardhat-config': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/hardhat.config.ts', + }, + 'make-deploy-script': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + }, + 'open-deploy-script': { + action: 'clickButtonByText', + buttonText: 'deploy.ts', + }, + 'deploy-script': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/contracts/deploy/deploy.ts', + }, + 'compile-and-deploy': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/contracts', + saveOutput: 'tests-output/zksync-webauthn/contracts/deploy-output.txt', + }, +}; + +const frontendPart1Steps: IStepConfig = { + 'init-next-app': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn', + preCommand: ' --ts --eslint --src-dir --use-npm --no-app --no-tailwind --import-alias "@/*" frontend', + }, + 'wait-for-nextjs-init': { + action: 'wait', + timeout: 5000, + }, + 'install-nextjs-deps': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn', + }, + 'wait-for-nextjs-deps': { + action: 'wait', + timeout: 5000, + }, + 'open-home-page': { + action: 'clickButtonByText', + buttonText: 'index.tsx', + }, + 'home-page-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/index.tsx', + }, + 'create-env-file': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + useSetCommand: 'touch .env.local', + }, + 'extract-aa-factory-address': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/zksync-webauthn/contracts/deploy-output.txt', + envFilepath: 'tests-output/zksync-webauthn/frontend/.env.local', + variableName: 'NEXT_PUBLIC_AA_FACTORY_ADDRESS', + regex: /(?<=factory address:\s*)0x[a-fA-F0-9]{40}/i, + }, + 'extract-nft-contract-address': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/zksync-webauthn/contracts/deploy-output.txt', + envFilepath: 'tests-output/zksync-webauthn/frontend/.env.local', + variableName: 'NEXT_PUBLIC_NFT_CONTRACT_ADDRESS', + regex: /(?<=NFT CONTRACT ADDRESS:\s*)0x[a-fA-F0-9]{40}/i, + }, + 'extract-paymaster-contract-address': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/zksync-webauthn/contracts/deploy-output.txt', + envFilepath: 'tests-output/zksync-webauthn/frontend/.env.local', + variableName: 'NEXT_PUBLIC_PAYMASTER_ADDRESS', + regex: /(?<=PAYMASTER CONTRACT ADDRESS:\s*)0x[a-fA-F0-9]{40}/i, + }, + 'replace-deployed-contract-address': { + action: 'modifyFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/index.tsx', + atLine: 5, + removeLines: [5, 7, 9], + useSetData: `export const AA_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_AA_FACTORY_ADDRESS || ''; + export const NFT_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_NFT_CONTRACT_ADDRESS || ''; + export const PAYMASTER_ADDRESS = process.env.NEXT_PUBLIC_PAYMASTER_ADDRESS || '';`, + }, + 'make-layout-component': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-layout-component': { + action: 'clickButtonByText', + buttonText: 'Layout.tsx', + }, + 'layout-component-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/components/Layout.tsx', + }, + 'open-styles': { + action: 'clickButtonByText', + buttonText: 'globals.css', + }, + 'update-styles': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/styles/globals.css', + }, + 'make-registration-api': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-registration-api': { + action: 'clickButtonByText', + buttonText: 'get-registration-options.ts', + }, + 'registration-api-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/api/get-registration-options.ts', + }, + 'make-auth-api': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-auth-api': { + action: 'clickButtonByText', + buttonText: 'get-authentication-options.ts', + }, + 'auth-api-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/api/get-authentication-options.ts', + }, + 'make-utils-folder': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'make-strings-utils': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-strings-utils': { + action: 'clickButtonByText', + buttonText: 'string.ts', + }, + 'strings-utils-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/utils/string.ts', + }, + 'make-tx-utils': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-tx-utils': { + action: 'clickButtonByText', + buttonText: 'tx.ts', + }, + 'tx-utils-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/utils/tx.ts', + }, + 'make-webauthn-utils': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-webauthn-utils': { + action: 'clickButtonByText', + buttonText: 'webauthn.ts', + }, + 'webauthn-utils-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/utils/webauthn.ts', + }, +}; + +const frontendPart2Steps: IStepConfig = { + 'make-hooks-dir': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'make-account-hook': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-account-hook': { + action: 'clickButtonByText', + buttonText: 'useAccount.tsx', + }, + 'account-hook-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/hooks/useAccount.tsx', + }, + 'make-wallet-hook': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-wallet-hook': { + action: 'clickButtonByText', + buttonText: 'useWallet.tsx', + }, + 'wallet-hook-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/hooks/useWallet.tsx', + }, + 'open-app': { + action: 'clickButtonByText', + buttonText: '_app.tsx', + }, + 'app-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/_app.tsx', + }, + 'make-create-account': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-create-account': { + action: 'clickButtonByText', + buttonText: 'create-account.tsx', + }, + 'create-account-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/create-account.tsx', + }, + 'make-register': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-register': { + action: 'clickButtonByText', + buttonText: 'register.tsx', + }, + 'register-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/register.tsx', + }, + 'make-transfer': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-transfer': { + action: 'clickButtonByText', + buttonText: 'transfer.tsx', + }, + 'transfer-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/transfer.tsx', + }, + 'make-mint': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + }, + 'open-mint': { + action: 'clickButtonByText', + buttonText: 'mint.tsx', + }, + 'mint-code': { + action: 'writeToFile', + filepath: 'tests-output/zksync-webauthn/frontend/src/pages/mint.tsx', + }, + 'run-frontend': { + action: 'runCommand', + commandFolder: 'tests-output/zksync-webauthn/frontend', + preCommand: "bun pm2 start '' --name webauthn-frontend", + }, + 'wait-for-frontend': { + action: 'wait', + timeout: 5000, + }, + 'visit-frontend': { + action: 'visitURL', + url: 'http://localhost:3000/create-account', + }, + 'create-new-account': { + action: 'clickButtonByText', + buttonText: 'Create a New Account', + }, + 'wait-for-account': { + action: 'wait', + timeout: 5000, + }, + 'verify-account-made': { + action: 'findText', + text: 'Your current account is', + }, +}; + +export const steps: IStepConfig = { + ...contractSteps, + ...frontendPart1Steps, + ...frontendPart2Steps, +}; diff --git a/tests/signing-txns-with-webauthn.spec.ts b/tests/signing-txns-with-webauthn.spec.ts new file mode 100644 index 00000000..3663fc45 --- /dev/null +++ b/tests/signing-txns-with-webauthn.spec.ts @@ -0,0 +1,16 @@ +import { test } from '@playwright/test'; +import { setupAndRunTest } from './utils/runTest'; + +test('Signing Transactions With WebAuthn', async ({ page, context }) => { + await setupAndRunTest( + page, + context, + 'zksync-webauthn', + [ + '/signing-transactions-with-webauthn/building-the-contracts', + '/signing-transactions-with-webauthn/building-the-frontend', + '/signing-transactions-with-webauthn/completing-the-frontend', + ], + 'signing-txns-with-webauthn' + ); +}); diff --git a/tests/utils/runCommand.ts b/tests/utils/runCommand.ts index 46983cd0..126b3a2c 100644 --- a/tests/utils/runCommand.ts +++ b/tests/utils/runCommand.ts @@ -29,7 +29,7 @@ export async function runCommand( } else { if (preCommand) { if (preCommand.includes('')) { - command = preCommand.replace('', copied); + command = preCommand.replace('', copied.trimEnd()); } else { command = preCommand + copied; } diff --git a/tests/utils/runTest.ts b/tests/utils/runTest.ts index 27f9d4a1..dbf89245 100644 --- a/tests/utils/runTest.ts +++ b/tests/utils/runTest.ts @@ -26,7 +26,7 @@ export async function setupAndRunTest( // TEST for (const pageUrl of pageUrls) { - await runTest(page, `http://localhost:3000/tutorials${pageUrl}`, config!); + await runTest(page, `http://localhost:3030/tutorials${pageUrl}`, config!); } // SHUT DOWN ANY RUNNING PROJECTS @@ -95,6 +95,12 @@ export async function runTest(page: Page, url: string, config: IStepConfig) { case 'clickButtonByText': clickButtonByText(page, stepData.buttonText); break; + case 'visitURL': + await visit(page, stepData.url); + break; + case 'findText': + page.getByText(stepData.text); + break; default: console.log('STEP NOT FOUND:', stepData); } diff --git a/tests/utils/types.ts b/tests/utils/types.ts index 11986e6e..2cf547c6 100644 --- a/tests/utils/types.ts +++ b/tests/utils/types.ts @@ -10,7 +10,9 @@ export type IStep = | ICompareToFile | ICheckIfBalanceIsZero | IExtractDataToEnv - | IClickButtonByText; + | IClickButtonByText + | IVisitURL + | IFindText; export interface IRunCommand { action: 'runCommand'; @@ -81,3 +83,13 @@ export interface IClickButtonByText { action: 'clickButtonByText'; buttonText: string; } + +export interface IVisitURL { + action: 'visitURL'; + url: string; +} + +export interface IFindText { + action: 'findText'; + text: string; +}