Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JSON onboarding in e2e tests of transactions #3559

Merged
merged 12 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 22 additions & 24 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ jobs:
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: .github/workflows/pledge-signer-sync
- run: yarn install --frozen-lockfile
working-directory: scripts/key-generation
- run: yarn lint
detect-if-flag-changed:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -189,19 +191,17 @@ jobs:
|| contains(github.head_ref, 'e2e')
|| needs.detect-if-flag-changed.outputs.path-filter == 'true'
env:
RECOVERY_PHRASE: ${{ secrets.TEST_WALLET_RECOVERY_PHRASE }}
TEST_WALLET_JSON_BODY: ${{ secrets.TEST_WALLET_JSON_BODY }}
TEST_WALLET_JSON_PASSWORD: ${{ secrets.TEST_WALLET_JSON_PASSWORD }}
run: xvfb-run npx playwright test --grep @expensive
# Upload of Playwright artifacts commented out until we change the method
# of account import in the tests from seed to JSON (we don't want to leak
# the seed phrase in logs/recordings from tests).
# - uses: actions/upload-artifact@v3
# if: failure()
# with:
# name: debug-output
# path: |
# test-results/
# #videos/
# retention-days: 30
- uses: actions/upload-artifact@v3
if: failure()
with:
name: debug-output
path: |
test-results/
#videos/
retention-days: 30
e2e-tests-fork:
if: |
github.ref == 'refs/heads/main'
Expand Down Expand Up @@ -249,17 +249,15 @@ jobs:
sleep 20
- name: Run Playwright tests designed for fork
env:
RECOVERY_PHRASE: ${{ secrets.TEST_WALLET_RECOVERY_PHRASE }}
TEST_WALLET_JSON_BODY: ${{ secrets.TEST_WALLET_JSON_BODY }}
TEST_WALLET_JSON_PASSWORD: ${{ secrets.TEST_WALLET_JSON_PASSWORD }}
USE_MAINNET_FORK: true
run: xvfb-run npx playwright test
# Upload of Playwright artifacts commented out until we change the method
# of account import in the tests from seed to JSON (we don't want to leak
# the seed phrase in logs/recordings from tests).
# - uses: actions/upload-artifact@v3
# if: failure()
# with:
# name: fork-debug-output
# path: |
# test-results/
# #videos/
# retention-days: 30
- uses: actions/upload-artifact@v3
if: failure()
with:
name: fork-debug-output
path: |
test-results/
#videos/
retention-days: 30
64 changes: 49 additions & 15 deletions e2e-tests/fork-based/transactions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import fs from "fs"
import { test, expect } from "../utils"

test.describe("Transactions", () => {
test.beforeAll(async () => {
/**
* Create a JSON file with an encoded private key based on the file
* content passed from an environment variable. The further steps of
* the tests assume that the file encodes the pk of the `testertesting.eth`
* account. The JSON file can be generated using a script
* `scripts/key-generation/export-key-as-json.js`.
*/
const jsonBody = process.env.TEST_WALLET_JSON_BODY
if (jsonBody) {
fs.writeFileSync("./e2e-tests/utils/JSON.json", jsonBody)
} else {
throw new Error(
"TEST_WALLET_JSON_BODY environment variable is not defined."
)
}
})

test("User can send base asset", async ({
page: popup,
walletPageHelper,
transactionsHelper,
}) => {
await test.step("Import account", async () => {
/**
* Onboard using walletPageHelper
* Onboard using JSON file.
*/
const recoveryPhrase = process.env.RECOVERY_PHRASE
if (recoveryPhrase) {
await walletPageHelper.onboardWithSeedPhrase(recoveryPhrase)
const jsonPassword = process.env.TEST_WALLET_JSON_PASSWORD
if (jsonPassword) {
await walletPageHelper.onboardWithJSON(
"./e2e-tests/utils/JSON.json",
jsonPassword
)
} else {
throw new Error("RECOVERY_PHRASE environment variable is not defined.")
throw new Error(
"TEST_WALLET_JSON_PASSWORD environment variable is not defined."
)
}

/**
Expand Down Expand Up @@ -185,13 +209,18 @@ test.describe("Transactions", () => {
}) => {
await test.step("Import account", async () => {
/**
* Onboard using walletPageHelper
* Onboard using JSON file.
*/
const recoveryPhrase = process.env.RECOVERY_PHRASE
if (recoveryPhrase) {
await walletPageHelper.onboardWithSeedPhrase(recoveryPhrase)
const jsonPassword = process.env.TEST_WALLET_JSON_PASSWORD
if (jsonPassword) {
await walletPageHelper.onboardWithJSON(
"./e2e-tests/utils/JSON.json",
jsonPassword
)
} else {
throw new Error("RECOVERY_PHRASE environment variable is not defined.")
throw new Error(
"TEST_WALLET_JSON_PASSWORD environment variable is not defined."
)
}

/**
Expand Down Expand Up @@ -327,13 +356,18 @@ test.describe("Transactions", () => {
}) => {
await test.step("Import account", async () => {
/**
* Onboard using walletPageHelper
* Onboard using JSON file.
*/
const recoveryPhrase = process.env.RECOVERY_PHRASE
if (recoveryPhrase) {
await walletPageHelper.onboardWithSeedPhrase(recoveryPhrase)
const jsonPassword = process.env.TEST_WALLET_JSON_PASSWORD
if (jsonPassword) {
await walletPageHelper.onboardWithJSON(
"./e2e-tests/utils/JSON.json",
jsonPassword
)
} else {
throw new Error("RECOVERY_PHRASE environment variable is not defined.")
throw new Error(
"TEST_WALLET_JSON_PASSWORD environment variable is not defined."
)
}

/**
Expand Down
32 changes: 27 additions & 5 deletions e2e-tests/regular/transactions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from "fs"
import { test, expect } from "../utils"

test.describe("Transactions", () => {
Expand All @@ -8,13 +9,34 @@ test.describe("Transactions", () => {
}) => {
await test.step("Import account", async () => {
/**
* Onboard using walletPageHelper
* Create a JSON file with an encoded private key based on the file
* content passed from an environment variable. The further steps of
* the test assume that the file encodes the pk of the `testertesting.eth`
* account. The JSON file can be generated using a script
* `scripts/key-generation/export-key-as-json.js`.
*/
const recoveryPhrase = process.env.RECOVERY_PHRASE
if (recoveryPhrase) {
await walletPageHelper.onboardWithSeedPhrase(recoveryPhrase)
const jsonBody = process.env.TEST_WALLET_JSON_BODY
if (jsonBody) {
fs.writeFileSync("./e2e-tests/utils/JSON.json", jsonBody)
} else {
throw new Error("RECOVERY_PHRASE environment variable is not defined.")
throw new Error(
"TEST_WALLET_JSON_BODY environment variable is not defined."
)
}

/**
* Onboard using JSON file.
*/
const jsonPassword = process.env.TEST_WALLET_JSON_PASSWORD
if (jsonPassword) {
await walletPageHelper.onboardWithJSON(
"./e2e-tests/utils/JSON.json",
jsonPassword
)
} else {
throw new Error(
"TEST_WALLET_JSON_PASSWORD environment variable is not defined."
)
}

/**
Expand Down
62 changes: 61 additions & 1 deletion e2e-tests/utils/onboarding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserContext, test as base, expect, Page } from "@playwright/test"
import * as path from "path"

export const getOnboardingPage = async (
context: BrowserContext
Expand Down Expand Up @@ -60,7 +61,7 @@ export default class OnboardingHelper {
}): Promise<void> {
const page = onboardingPage || (await getOnboardingPage(this.context))

await base.step("Onboard readonly address", async () => {
await base.step("Onboard using seed", async () => {
await page.getByRole("button", { name: "Use existing wallet" }).click()
await page.getByRole("button", { name: "Import recovery phrase" }).click()

Expand All @@ -86,6 +87,65 @@ export default class OnboardingHelper {
})
}

async addAccountFromJSON({
file,
filePassword,
onboardingPage,
}: {
file: string
filePassword: string
onboardingPage?: Page
}): Promise<void> {
const page = onboardingPage || (await getOnboardingPage(this.context))

await base.step("Onboard using JSON with private key", async () => {
await page.getByRole("button", { name: "Use existing wallet" }).click()
await page.getByRole("button", { name: "Import private key" }).click()

const passwordInput = page.locator('input[name="password"]')

if (await passwordInput.isVisible()) {
await page.locator('input[name="password"]').fill(DEFAULT_PASSWORD)
await page
.locator('input[name="confirm_password"]')
.fill(DEFAULT_PASSWORD)
}

await page.getByRole("button", { name: "Begin the hunt" }).click()

await page.getByTestId("panel_switcher").getByText("JSON").click()
// await page.getByText("Browse files").click()

// Start waiting for file chooser before clicking. Note no await.
const fileChooserPromise = page.waitForEvent("filechooser")
await page.getByText("Browse files").click({ force: true })
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(file)

await expect(
page.getByTestId("file_status").getByText(path.basename(file))
).toBeVisible()
await expect(
page.getByText("Wrong file, only JSON accepted")
).toHaveCount(0)

await page.getByPlaceholder(" ").fill(filePassword)
await page.getByRole("button", { name: "Decrypt file" }).click()

await expect(page.getByTestId("loading_doggo")).toBeVisible()
await expect(page.getByText("Decrypting file...")).toBeVisible()
await expect(page.getByText("this may take up to 1 minute")).toBeVisible()

await expect(page.getByText("Completed!")).toBeVisible({ timeout: 60000 })

await page.getByRole("button", { name: "Finalize" }).click()

await expect(
page.getByRole("heading", { name: "Welcome to Taho" })
).toBeVisible()
})
}

async addNewWallet(onboardingPage?: Page): Promise<void> {
const page = onboardingPage || (await getOnboardingPage(this.context))

Expand Down
16 changes: 15 additions & 1 deletion e2e-tests/utils/walletPageHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class WalletPageHelper {
}

/**
* Onboard using walletPageHelper
* Onboard using seed phrase.
*/
async onboardWithSeedPhrase(recoveryPhrase: string): Promise<void> {
const onboardingPage = await getOnboardingPage(this.context)
Expand All @@ -50,6 +50,20 @@ export default class WalletPageHelper {
await this.goToStartPage()
}

/**
* Onboard using JSON with password-encrypted private key
*/
async onboardWithJSON(file: string, filePassword: string): Promise<void> {
const onboardingPage = await getOnboardingPage(this.context)
await this.onboarding.addAccountFromJSON({
file,
filePassword,
onboardingPage,
})
await this.setViewportSize()
await this.goToStartPage()
}

async verifyTopWrap(network: RegExp, accountLabel: RegExp): Promise<void> {
// TODO: maybe we could also verify graphical elements (network icon, profile picture, etc)?

Expand Down
50 changes: 50 additions & 0 deletions scripts/key-generation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# JSON keystore generation

## What is a keystore file?

A JSON keystore is a file format commonly used to securely store and manage
cryptographic keys associated with cryptocurrency wallets. It provides a
structured and standardized way to store the private key in a JSON format.

Typically, a JSON keystore file contains the encrypted private key along with
relevant metadata and parameters needed for key derivation and decryption. The
metadata may include information such as the key derivation function used,
encryption algorithm, initialization vector (IV), salt, and other necessary
parameters.

The private key is encrypted within the JSON keystore file using a user-defined
password or passphrase. This encryption adds an extra layer of security,
protecting the private key from unauthorized access. The encrypted private key
can only be decrypted and accessed by providing the correct password or
passphrase.

When a user wants to use their crypto wallet, they can import the JSON keystore
file into a wallet software or application that supports this file format (one
of them being the Taho extension). The wallet software will prompt the user to
enter their password or passphrase to decrypt the private key stored in the JSON
file. Once the private key is decrypted, the wallet software can utilize it to
sign transactions and perform other wallet-related operations.

JSON keystore files are designed to be portable, allowing users to easily back
up and transfer their private keys between different wallet applications or
devices while maintaining a standardized format. However, it's crucial to
protect the JSON keystore file and the associated password or passphrase since
they provide access to the private key and, ultimately, control over the
associated cryptocurrency funds.

## Generating JSON keystore for a private key

In order to produce a keystore file run:

```sh
$ yarn install # installs dependencies
$ PRIVATE_KEY="<private key without the 0x prefix>" PASSWORD="<password>" yarn run generate # executes the script
```

As a result a JSON keystore file encoding the provided private key with the
provided password will be created in the current directory.

## Credits

The script was based on a solution suggested in
[this comment](https://ethereum.stackexchange.com/a/55617).
18 changes: 18 additions & 0 deletions scripts/key-generation/export-key-as-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Since this is a script we can’t use import yet.
/* eslint-disable @typescript-eslint/no-var-requires */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to add a linter exception here, as I couldn't make the linter happy without braking the script (can't say confidently that this can't be done though...).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choice looks correct, just drop a note above it that since this is a script we can’t use import yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const fs = require("fs")
const wallet = require("ethereumjs-wallet").default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the lint error below—we need to yarn install in the parent dir in the actions workflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 24b20bc, it helped, thanks!


const pk = Buffer.from(
process.env.PRIVATE_KEY, // should not contrain `0x` prefix
"hex"
)
const account = wallet.fromPrivateKey(pk)
const password = process.env.PASSWORD
account.toV3(password).then((value) => {
const address = account.getAddress().toString("hex")
const file = `UTC--${new Date()
.toISOString()
.replace(/[:]/g, "-")}--${address}.json`
fs.writeFileSync(file, JSON.stringify(value))
})
Loading
Loading