diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index cf2aa1bb6..f22f3b561 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -32,16 +32,24 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
skip-cache: true
- - name: Compile
+ - name: NX pre-build compile targets
shell: bash
run: npx nx affected --target=compile --parallel=5
- - name: Test
+ - name: Test TypeScript
shell: bash
run: npx nx affected --target=test --parallel=5
- name: Test Go
uses: n8maninger/action-golang-test@v1
with:
args: '-race'
+ - name: Install playwright deps for e2e
+ shell: bash
+ run: npx playwright install-deps
+ - name: Test e2e
+ shell: bash
+ run: npx nx affected --target=e2e --parallel=5
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build
shell: bash
run: npx nx affected --target=build --configuration=production --parallel=5
diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml
index 087cd0c26..515f12a71 100644
--- a/.github/workflows/release-main.yml
+++ b/.github/workflows/release-main.yml
@@ -29,15 +29,31 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Lint
+ - name: Lint TypeScript
shell: bash
run: npx nx run-many --target=lint --all --parallel=5
- - name: Compile
+ - name: Lint Go
+ uses: golangci/golangci-lint-action@v3
+ with:
+ skip-cache: true
+ - name: NX pre-build compile targets
shell: bash
run: npx nx run-many --target=compile --all --parallel=5
- - name: Test
+ - name: Test TypeScript
shell: bash
run: npx nx run-many --target=test --all --parallel=5
+ - name: Test Go
+ uses: n8maninger/action-golang-test@v1
+ with:
+ args: '-race'
+ - name: Install playwright deps for e2e
+ shell: bash
+ run: npx playwright install-deps
+ - name: Test e2e
+ shell: bash
+ run: npx nx run-many --target=e2e --all --parallel=5
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build for publishing
shell: bash
run: npx nx run-many --target=build --configuration=production --all --parallel=5
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 64553b175..d3f39ca94 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,6 +3,7 @@
"nrwl.angular-console",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
- "dbaeumer.vscode-eslint"
+ "dbaeumer.vscode-eslint",
+ "ms-playwright.playwright"
]
}
diff --git a/apps/walletd-e2e/.eslintrc.json b/apps/walletd-e2e/.eslintrc.json
new file mode 100644
index 000000000..fbf2c975e
--- /dev/null
+++ b/apps/walletd-e2e/.eslintrc.json
@@ -0,0 +1,22 @@
+{
+ "extends": ["plugin:playwright/recommended", "../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["src/**/*.{ts,js,tsx,jsx}"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/walletd-e2e/.gitignore b/apps/walletd-e2e/.gitignore
new file mode 100644
index 000000000..53752db25
--- /dev/null
+++ b/apps/walletd-e2e/.gitignore
@@ -0,0 +1 @@
+output
diff --git a/apps/walletd-e2e/playwright.config.ts b/apps/walletd-e2e/playwright.config.ts
new file mode 100644
index 000000000..d7f247b0f
--- /dev/null
+++ b/apps/walletd-e2e/playwright.config.ts
@@ -0,0 +1,76 @@
+import { defineConfig, devices } from '@playwright/test'
+import { nxE2EPreset } from '@nx/playwright/preset'
+
+import { workspaceRoot } from '@nx/devkit'
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:3008'
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ video: 'on-first-retry',
+ },
+ expect: {
+ // Raise the timeout because it is running against next dev mode
+ // which requires compilation the first to a page is visited
+ timeout: 10_000,
+ },
+ outputDir: 'output',
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx serve walletd',
+ url: 'http://localhost:3008',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+})
diff --git a/apps/walletd-e2e/project.json b/apps/walletd-e2e/project.json
new file mode 100644
index 000000000..a7a3ac2f1
--- /dev/null
+++ b/apps/walletd-e2e/project.json
@@ -0,0 +1,19 @@
+{
+ "name": "walletd-e2e",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "apps/walletd-e2e/src",
+ "projectType": "application",
+ "targets": {
+ "e2e": {
+ "executor": "@nx/playwright:playwright",
+ "outputs": ["{workspaceRoot}/dist/.playwright/apps/walletd-e2e"],
+ "options": {
+ "config": "apps/walletd-e2e/playwright.config.ts"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ }
+ },
+ "implicitDependencies": ["walletd"]
+}
diff --git a/apps/walletd-e2e/src/fixtures/createWallet.ts b/apps/walletd-e2e/src/fixtures/createWallet.ts
new file mode 100644
index 000000000..35d83c34c
--- /dev/null
+++ b/apps/walletd-e2e/src/fixtures/createWallet.ts
@@ -0,0 +1,50 @@
+import { expect, Page } from '@playwright/test'
+import { mockApiWallet } from '../mocks/mock'
+import {
+ Wallet,
+ WalletAddressesResponse,
+ WalletBalanceResponse,
+ WalletFundResponse,
+ WalletOutputsSiacoinResponse,
+} from '@siafoundation/react-walletd'
+import { mockApiWallets } from '../mocks/wallets'
+
+export async function createWallet({
+ page,
+ seed,
+ newWallet,
+ responses = {},
+}: {
+ page: Page
+ seed: string
+ newWallet: Wallet
+ responses?: {
+ balance?: WalletBalanceResponse
+ outputsSiacoin?: WalletOutputsSiacoinResponse
+ fund?: WalletFundResponse
+ addresses?: WalletAddressesResponse
+ }
+}) {
+ const wallets = await mockApiWallets({ page, createWallet: newWallet })
+ await mockApiWallet({
+ page,
+ wallet: newWallet,
+ responses,
+ })
+
+ await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible()
+ await page.getByRole('button', { name: 'Add wallet' }).click()
+ await page.getByRole('button', { name: 'Create a wallet Generate a' }).click()
+ await page.locator('input[name=name]').fill(newWallet.name)
+ await page.locator('input[name=name]').press('Tab')
+ await page.locator('textarea[name=description]').fill(newWallet.name)
+ await page.locator('textarea[name=description]').press('Tab')
+ await page.locator('textarea[name=mnemonic]').click()
+ wallets.push(newWallet)
+ await page.getByRole('button', { name: 'Add wallet' }).click()
+ await expect(
+ page.getByText(`Wallet ${newWallet.name.slice(0, 5)}`)
+ ).toBeVisible()
+ await page.locator('input[name=mnemonic]').fill(seed)
+ await page.getByRole('button', { name: 'Continue' }).click()
+}
diff --git a/apps/walletd-e2e/src/fixtures/login.ts b/apps/walletd-e2e/src/fixtures/login.ts
new file mode 100644
index 000000000..7194cef2a
--- /dev/null
+++ b/apps/walletd-e2e/src/fixtures/login.ts
@@ -0,0 +1,12 @@
+import { Page, expect } from '@playwright/test'
+
+export async function login({ page }: { page: Page }) {
+ await page.goto('/')
+ await expect(page).toHaveTitle('walletd')
+ await page.getByLabel('login settings').click()
+ await page.getByRole('menuitem', { name: 'Show custom API' }).click()
+ await page.locator('input[name=api]').fill('https://walletd.local')
+ await page.locator('input[name=api]').press('Tab')
+ await page.locator('input[name=password]').fill('password')
+ await page.locator('input[name=password]').press('Enter')
+}
diff --git a/apps/walletd-e2e/src/fixtures/navigateToWallet.ts b/apps/walletd-e2e/src/fixtures/navigateToWallet.ts
new file mode 100644
index 000000000..4fc6bef93
--- /dev/null
+++ b/apps/walletd-e2e/src/fixtures/navigateToWallet.ts
@@ -0,0 +1,13 @@
+import { Page } from '@playwright/test'
+import { Wallet } from '@siafoundation/react-walletd'
+
+export async function navigateToWallet({
+ page,
+ wallet,
+}: {
+ page: Page
+ wallet: Wallet
+}) {
+ await page.getByLabel('Dashboard').click()
+ await page.getByText(wallet.name).click()
+}
diff --git a/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts
new file mode 100644
index 000000000..2c334e07c
--- /dev/null
+++ b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts
@@ -0,0 +1,19 @@
+import { Page } from 'playwright'
+
+export async function fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount,
+}: {
+ page: Page
+ receiveAddress: string
+ changeAddress: string
+ amount: string
+}) {
+ await page.locator('input[name=receiveAddress]').fill(receiveAddress)
+ await page.getByLabel('customChangeAddress').click()
+ await page.locator('input[name=changeAddress]').fill(changeAddress)
+ await page.locator('input[name=siacoin]').fill(amount)
+ await page.getByRole('button', { name: 'Generate transaction' }).click()
+}
diff --git a/apps/walletd-e2e/src/mocks/consensusNetwork.ts b/apps/walletd-e2e/src/mocks/consensusNetwork.ts
new file mode 100644
index 000000000..660e7c77a
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/consensusNetwork.ts
@@ -0,0 +1,54 @@
+import { ConsensusNetwork } from '@siafoundation/types'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: ConsensusNetwork = {
+ name: 'zen',
+ initialCoinbase: '300000000000000000000000000000',
+ minimumCoinbase: '300000000000000000000000000000',
+ initialTarget:
+ 'bid:0000000100000000000000000000000000000000000000000000000000000000',
+ hardforkDevAddr: {
+ height: 1,
+ oldAddress:
+ 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69',
+ newAddress:
+ 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69',
+ },
+ hardforkTax: {
+ height: 2,
+ },
+ hardforkStorageProof: {
+ height: 5,
+ },
+ hardforkOak: {
+ height: 10,
+ fixHeight: 12,
+ genesisTimestamp: '2023-01-13T03:53:20-05:00',
+ },
+ hardforkASIC: {
+ height: 20,
+ oakTime: 10000000000000,
+ oakTarget:
+ 'bid:0000000100000000000000000000000000000000000000000000000000000000',
+ },
+ hardforkFoundation: {
+ height: 30,
+ primaryAddress:
+ 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807',
+ failsafeAddress:
+ 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69',
+ },
+ hardforkV2: {
+ allowHeight: 100000,
+ requireHeight: 102000,
+ },
+}
+
+export async function mockApiConsensusNetwork({ page }: { page: Page }) {
+ const json = cloneDeep(data)
+ await page.route('**/api/consensus/network*', async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/consensusTip.ts b/apps/walletd-e2e/src/mocks/consensusTip.ts
new file mode 100644
index 000000000..dc1309043
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/consensusTip.ts
@@ -0,0 +1,16 @@
+import { ConsensusTipResponse } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: ConsensusTipResponse = {
+ height: 61676,
+ id: 'bid:00000010d5da9002b9640d920d9eb9f7502c5c3b2a796ecf800a103920bea96f',
+}
+
+export async function mockApiConsensusTipState({ page }: { page: Page }) {
+ const json = cloneDeep(data)
+ await page.route('**/api/consensus/tipstate*', async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/consensusTipState.ts b/apps/walletd-e2e/src/mocks/consensusTipState.ts
new file mode 100644
index 000000000..02d72c34b
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/consensusTipState.ts
@@ -0,0 +1,63 @@
+import { ConsensusState } from '@siafoundation/types'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: ConsensusState = {
+ index: {
+ height: 61676,
+ id: 'bid:00000010d5da9002b9640d920d9eb9f7502c5c3b2a796ecf800a103920bea96f',
+ },
+ prevTimestamps: [
+ '2024-03-25T17:33:24-04:00',
+ '2024-03-25T17:18:32-04:00',
+ '2024-03-25T17:07:59-04:00',
+ '2024-03-25T17:04:15-04:00',
+ '2024-03-25T16:50:56-04:00',
+ '2024-03-25T16:38:22-04:00',
+ '2024-03-25T16:38:12-04:00',
+ '2024-03-25T16:36:56-04:00',
+ '2024-03-25T16:35:17-04:00',
+ '2024-03-25T16:33:59-04:00',
+ '2024-03-25T16:31:16-04:00',
+ ],
+ depth: 'bid:000000000001aa096ddebfbf467c56faba46eb4b3e53e72c775bf9ab202a4890',
+ childTarget:
+ 'bid:0000005a6c49bca6186415827e15b13371c3de541dd64b7cf36a519b5128b2f8',
+ siafundPool: '88386728360671853873752300000',
+ oakTime: 127103000000000,
+ oakTarget:
+ 'bid:0000000024d11bd550a6acf3718192825fa2cef5582314db8e92c4f4e5efbf4c',
+ foundationPrimaryAddress:
+ 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807',
+ foundationFailsafeAddress:
+ 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69',
+ totalWork: '169134658088983',
+ difficulty: '47498615',
+ oakWork: '29864374508',
+ elements: {
+ numLeaves: 304383,
+ trees: [
+ 'h:6d5fedd2f6d71cad5fc7bb35ef8bad6631c63e1323142e2a4825314b77f87d1e',
+ 'h:16e0c70194d2e41a21fe070ff936551b2379df8054890b95bada3c6b6a0ed3f7',
+ 'h:73defbf6af352d9401698492c407662b8feb62ba82e92028bf9bf0103f261844',
+ 'h:e22d4da6204096eef1eb0fc217c8900dce1040312cc721b9b6b3fc2be7a77da8',
+ 'h:4a9860d3e3ccf6cf434ed94d485b4385c62c443fd18e539998bd817a3ebed772',
+ 'h:6cd1e1d71a0796980193bcbb1adff069ef3f70fd12749d9e877b2bd0c8d561d1',
+ 'h:7aa0bdb94da13c43abd9446f77638d3e6e2ae4c36c5224a76433044c56db906c',
+ 'h:66c119cf5cb9ca3a174637c751988f03300e0375b43d22c82df7145841515d38',
+ 'h:ce8a94296e60887232915d6801a9246827661d329de47a08f25a6a3884809037',
+ 'h:8aa7872a63ef37e1ca8e4211700daf96068ce2cc215d4e2ab45434b5ffcab818',
+ 'h:3f0175c94ed31190e6da23e80d6167e2da619b460e27798750491163dbe5677b',
+ 'h:9f120f6489c3e3a324cd51a6f8f9dd033930688095f4440dc5f8511692b4efc1',
+ ],
+ },
+ attestations: 0,
+}
+
+export async function mockApiConsensusTip({ page }: { page: Page }) {
+ const json = cloneDeep(data)
+ await page.route('**/api/consensus/tip*', async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/mock.ts b/apps/walletd-e2e/src/mocks/mock.ts
new file mode 100644
index 000000000..8416e599b
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/mock.ts
@@ -0,0 +1,79 @@
+import { Page } from 'playwright'
+import {
+ Wallet,
+ WalletAddressesResponse,
+ WalletBalanceResponse,
+ WalletFundResponse,
+ WalletOutputsSiacoinResponse,
+} from '@siafoundation/react-walletd'
+import { mockSiaCentralExchangeRates } from './siaCentralExchangeRates'
+import { mockApiSyncerPeers } from './peers'
+import { mockApiConsensusTip } from './consensusTipState'
+import { mockApiConsensusTipState } from './consensusTip'
+import { mockApiConsensusNetwork } from './consensusNetwork'
+import { mockApiWallets } from './wallets'
+import { mockApiWalletBalance } from './walletBalance'
+import { mockApiWalletAddresses } from './walletAddresses'
+import { mockApiWalletEvents } from './walletEvents'
+import { mockApiWalletTxpool } from './walletTxpool'
+import { mockApiWalletOutputsSiacoin } from './walletOutputsSiacoin'
+import { mockApiWalletOutputsSiafund } from './walletOutputsSiafund'
+import { mockApiWalletFund } from './walletFund'
+import { mockApiTxpoolBroadcast } from './txpoolBroadcast'
+import { mockApiWalletRelease } from './walletRelease'
+
+export async function mockApiDefaults({ page }: { page: Page }) {
+ await mockSiaCentralExchangeRates({ page })
+ await mockApiSyncerPeers({ page })
+ await mockApiConsensusTip({ page })
+ await mockApiConsensusTipState({ page })
+ await mockApiConsensusNetwork({ page })
+ await mockApiTxpoolBroadcast({ page })
+ const wallets = await mockApiWallets({ page })
+ for (const wallet of wallets) {
+ await mockApiWallet({ page, wallet })
+ }
+}
+
+export async function mockApiWallet({
+ page,
+ wallet,
+ responses = {},
+}: {
+ page: Page
+ wallet: Wallet
+ responses?: {
+ balance?: WalletBalanceResponse
+ outputsSiacoin?: WalletOutputsSiacoinResponse
+ fund?: WalletFundResponse
+ addresses?: WalletAddressesResponse
+ }
+}) {
+ await mockApiWalletBalance({
+ page,
+ walletId: wallet.id,
+ response: responses.balance,
+ })
+ await mockApiWalletAddresses({
+ page,
+ walletId: wallet.id,
+ response: responses.addresses,
+ })
+ await mockApiWalletEvents({ page, walletId: wallet.id })
+ await mockApiWalletTxpool({ page, walletId: wallet.id })
+ await mockApiWalletOutputsSiacoin({
+ page,
+ walletId: wallet.id,
+ response: responses.outputsSiacoin,
+ })
+ await mockApiWalletOutputsSiafund({ page, walletId: wallet.id })
+ await mockApiWalletFund({
+ page,
+ walletId: wallet.id,
+ response: responses.fund,
+ })
+ await mockApiWalletRelease({
+ page,
+ walletId: wallet.id,
+ })
+}
diff --git a/apps/walletd-e2e/src/mocks/peers.ts b/apps/walletd-e2e/src/mocks/peers.ts
new file mode 100644
index 000000000..0a5c29593
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/peers.ts
@@ -0,0 +1,81 @@
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data = [
+ {
+ addr: '51.81.242.140:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T17:14:43Z',
+ connectedSince: '2024-03-25T21:08:49Z',
+ syncDuration: 1321006459,
+ },
+ {
+ addr: '3.36.68.121:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T13:39:10Z',
+ connectedSince: '2024-03-25T21:08:50Z',
+ syncDuration: 4230014834,
+ },
+ {
+ addr: '185.200.116.131:9807',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T17:14:43Z',
+ connectedSince: '2024-03-25T21:08:51Z',
+ syncedBlocks: 2,
+ syncDuration: 6018991169,
+ },
+ {
+ addr: '62.30.63.90:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T13:39:10Z',
+ connectedSince: '2024-03-25T21:09:07Z',
+ syncDuration: 6341708209,
+ },
+ {
+ addr: '43.203.121.70:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T17:14:44Z',
+ connectedSince: '2024-03-25T21:09:08Z',
+ syncedBlocks: 3,
+ syncDuration: 6860545337,
+ },
+ {
+ addr: '23.239.8.40:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T13:39:10Z',
+ connectedSince: '2024-03-25T21:09:08Z',
+ syncDuration: 580416082,
+ },
+ {
+ addr: '141.94.161.198:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T13:39:10Z',
+ connectedSince: '2024-03-25T21:09:13Z',
+ syncedBlocks: 93,
+ syncDuration: 4894896166,
+ },
+ {
+ addr: '64.227.180.244:9881',
+ inbound: false,
+ version: '1.5.4',
+ firstSeen: '2024-03-20T13:39:25Z',
+ connectedSince: '2024-03-25T21:08:49Z',
+ syncedBlocks: 2,
+ syncDuration: 4687705000,
+ },
+]
+
+export async function mockApiSyncerPeers({ page }: { page: Page }) {
+ const json = cloneDeep(data)
+ await page.route('**/api/syncer/peers*', async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts b/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts
new file mode 100644
index 000000000..a24cabd47
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts
@@ -0,0 +1,38 @@
+import { SiaCentralExchangeRatesResponse } from '@siafoundation/sia-central'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: SiaCentralExchangeRatesResponse = {
+ message: 'successfully retrieved exchange rate',
+ type: 'success',
+ rates: {
+ sc: {
+ aud: '0.016136871549',
+ bch: '0.000021499880703',
+ btc: '0.000000149047',
+ cad: '0.014328484298',
+ cny: '0.076310722577',
+ eth: '0.0000029068532077',
+ eur: '0.009737538604',
+ gbp: '0.008359151948',
+ jpy: '1.600530478116',
+ ltc: '0.000116295710314',
+ rub: '0.978819669836',
+ scp: '0.0623627615062762',
+ sf: '0.000000745235',
+ usd: '0.010571307522',
+ },
+ },
+ timestamp: '2024-03-26T13:12:22.7348119Z',
+}
+
+export async function mockSiaCentralExchangeRates({ page }: { page: Page }) {
+ const json = cloneDeep(data)
+ await page.route(
+ 'https://api.siacentral.com/v2/market/exchange-rate?currencies=sc',
+ async (route) => {
+ await route.fulfill({ json })
+ }
+ )
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts b/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts
new file mode 100644
index 000000000..c452c5438
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts
@@ -0,0 +1,7 @@
+import { Page } from 'playwright'
+
+export async function mockApiTxpoolBroadcast({ page }: { page: Page }) {
+ await page.route(`**/api/txpool/broadcast*`, async (route) => {
+ await route.fulfill()
+ })
+}
diff --git a/apps/walletd-e2e/src/mocks/walletAddresses.ts b/apps/walletd-e2e/src/mocks/walletAddresses.ts
new file mode 100644
index 000000000..04768fbea
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletAddresses.ts
@@ -0,0 +1,57 @@
+import { WalletAddress } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: WalletAddress[] = [
+ {
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ description: '',
+ metadata: {
+ index: 0,
+ publicKey:
+ 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535',
+ },
+ },
+ {
+ address:
+ 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8',
+ description: '',
+ metadata: {
+ index: 1,
+ publicKey:
+ 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43',
+ },
+ },
+ {
+ address:
+ 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518',
+ description: '',
+ metadata: {
+ index: 2,
+ publicKey:
+ 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa',
+ },
+ },
+]
+
+export async function mockApiWalletAddresses({
+ page,
+ walletId,
+ response,
+}: {
+ page: Page
+ walletId: string
+ response?: WalletAddress[]
+}) {
+ const json = response || cloneDeep(data)
+ await page.route(`**/api/wallets/${walletId}/addresses*`, async (route) => {
+ if (route.request().method() === 'PUT') {
+ json.push(JSON.parse(route.request().postData()))
+ await route.fulfill({ json })
+ } else {
+ await route.fulfill({ json })
+ }
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletBalance.ts b/apps/walletd-e2e/src/mocks/walletBalance.ts
new file mode 100644
index 000000000..cc2b87dd2
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletBalance.ts
@@ -0,0 +1,26 @@
+import { WalletBalanceResponse } from '@siafoundation/react-walletd'
+import { toHastings } from '@siafoundation/units'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: WalletBalanceResponse = {
+ siacoins: toHastings('100').toString(),
+ immatureSiacoins: toHastings('10').toString(),
+ siafunds: 10,
+}
+
+export async function mockApiWalletBalance({
+ page,
+ walletId,
+ response,
+}: {
+ page: Page
+ walletId: string
+ response?: WalletBalanceResponse
+}) {
+ const json = response || cloneDeep(data)
+ await page.route(`**/api/wallets/${walletId}/balance*`, async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletEvents.ts b/apps/walletd-e2e/src/mocks/walletEvents.ts
new file mode 100644
index 000000000..7c6f53e8b
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletEvents.ts
@@ -0,0 +1,19 @@
+import { WalletEvent } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: WalletEvent[] = []
+
+export async function mockApiWalletEvents({
+ page,
+ walletId,
+}: {
+ page: Page
+ walletId: string
+}) {
+ const json = cloneDeep(data)
+ await page.route(`**/api/wallets/${walletId}/events*`, async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletFund.ts b/apps/walletd-e2e/src/mocks/walletFund.ts
new file mode 100644
index 000000000..3a5f1480f
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletFund.ts
@@ -0,0 +1,62 @@
+import { WalletFundResponse } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: WalletFundResponse = {
+ transaction: {
+ siacoinInputs: [
+ {
+ parentID:
+ 'scoid:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ unlockConditions: {
+ timelock: 0,
+ publicKeys: null,
+ signaturesRequired: 0,
+ },
+ },
+ {
+ parentID:
+ 'scoid:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ unlockConditions: {
+ timelock: 0,
+ publicKeys: null,
+ signaturesRequired: 0,
+ },
+ },
+ ],
+ siacoinOutputs: [
+ {
+ value: '1000000000000000000000000',
+ address:
+ 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8',
+ },
+ {
+ value: '97984280000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ ],
+ minerFees: ['3930000000000000000000'],
+ },
+ toSign: [
+ 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ ],
+ dependsOn: null,
+}
+
+export async function mockApiWalletFund({
+ page,
+ walletId,
+ response,
+}: {
+ page: Page
+ walletId: string
+ response?: WalletFundResponse
+}) {
+ const json = cloneDeep(response || data)
+ await page.route(`**/api/wallets/${walletId}/fund*`, async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts b/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts
new file mode 100644
index 000000000..d49c95803
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts
@@ -0,0 +1,66 @@
+import { SiacoinElement } from '@siafoundation/types'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: SiacoinElement[] = [
+ {
+ id: 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ leafIndex: 304248,
+ merkleProof: [
+ 'h:0a7a4c392f78899e3c38c5cd9e6a673b2c7afec97930af539af9c8e20209aa78',
+ 'h:a1e074dc48634a234b7366a0d7ab19cd05e3e698e1d44bf07e24d75ae0c65b3c',
+ 'h:44d107342962e2068d289ce090c87b7bf0c847f734bdfad10db5546e402c3ad7',
+ 'h:64ea3e65ecd5ebc1c0ce014673148060855ce550571208b6303b8e2a83e33451',
+ 'h:19e37bf7747c6d8c7b87d2474dbbd3a8f5b26d89642f8e1af4b9b02abdfb2ea6',
+ 'h:1fc2ac9f70211a3be6f334db14feb8b327458aee49d3539770640cbdec9b4a5f',
+ 'h:defbbc18c64349f11e75537955861ececce0fadf10baec456f6c74b024820af1',
+ 'h:87d27ff868ca3b1dce59ae754eaec48239718e81e2e6f3b7b418f5a00362bcf7',
+ 'h:93d823c55fbd09de462a8e355921433b3693d63de58ea8e2780a2c2ffabd0fee',
+ 'h:a68820d6b79b2735b15c69d0fc26b11252bb27f22b9088559ed13f9420f5dda1',
+ 'h:1bbcead690290291ea9628214a121ef783411693975171803bf5716a3a6ff19b',
+ ],
+ siacoinOutput: {
+ value: '1000000000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ maturityHeight: 0,
+ },
+ {
+ id: 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ leafIndex: 305723,
+ merkleProof: [
+ 'h:8c02aeec48de589ce497ebe72fb8b527cfe022ef513fcfdc56745c84832f00ec',
+ 'h:1bf63b9959e60272fd7a48a8cecd4120a852c0e14557ea27ccad6ea2071e70b3',
+ 'h:21b7e1606e9fd677059c58a1a687682182f71ce09e071431dcaff823a3a5d49e',
+ 'h:e81ac37d3b4db6166dc1bb10ebfa49f57cbf99aababd36ee4e3e5e12082dc6dc',
+ 'h:ecc307c6c3e505d97ccf821938e5e5702ef0130d33c991ca95735f7d9706a4b8',
+ 'h:9560060ee399793f102e092afdfdbdd33692706256955e8390af552de0addfc0',
+ ],
+ siacoinOutput: {
+ value: '97988210000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ maturityHeight: 0,
+ },
+]
+
+export async function mockApiWalletOutputsSiacoin({
+ page,
+ walletId,
+ response,
+}: {
+ page: Page
+ walletId: string
+ response?: SiacoinElement[]
+}) {
+ const json = cloneDeep(response || data)
+ await page.route(
+ `**/api/wallets/${walletId}/outputs/siacoin*`,
+ async (route) => {
+ await route.fulfill({ json })
+ }
+ )
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts b/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts
new file mode 100644
index 000000000..4573beddf
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts
@@ -0,0 +1,22 @@
+import { SiafundElement } from '@siafoundation/types'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: SiafundElement[] = []
+
+export async function mockApiWalletOutputsSiafund({
+ page,
+ walletId,
+}: {
+ page: Page
+ walletId: string
+}) {
+ const json = cloneDeep(data)
+ await page.route(
+ `**/api/wallets/${walletId}/outputs/siafund*`,
+ async (route) => {
+ await route.fulfill({ json })
+ }
+ )
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/walletRelease.ts b/apps/walletd-e2e/src/mocks/walletRelease.ts
new file mode 100644
index 000000000..a47a12c1a
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletRelease.ts
@@ -0,0 +1,15 @@
+import { WalletFundResponse } from '@siafoundation/react-walletd'
+import { Page } from 'playwright'
+
+export async function mockApiWalletRelease({
+ page,
+ walletId,
+}: {
+ page: Page
+ walletId: string
+ response?: WalletFundResponse
+}) {
+ await page.route(`**/api/wallets/${walletId}/release*`, async (route) => {
+ await route.fulfill()
+ })
+}
diff --git a/apps/walletd-e2e/src/mocks/walletTxpool.ts b/apps/walletd-e2e/src/mocks/walletTxpool.ts
new file mode 100644
index 000000000..e7c7af18f
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/walletTxpool.ts
@@ -0,0 +1,19 @@
+import { PoolTransaction } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: PoolTransaction[] = []
+
+export async function mockApiWalletTxpool({
+ page,
+ walletId,
+}: {
+ page: Page
+ walletId: string
+}) {
+ const json = cloneDeep(data)
+ await page.route(`**/api/wallets/${walletId}/txpool*`, async (route) => {
+ await route.fulfill({ json })
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/mocks/wallets.ts b/apps/walletd-e2e/src/mocks/wallets.ts
new file mode 100644
index 000000000..862de8567
--- /dev/null
+++ b/apps/walletd-e2e/src/mocks/wallets.ts
@@ -0,0 +1,58 @@
+import { Wallet } from '@siafoundation/react-walletd'
+import { cloneDeep } from '@technically/lodash'
+import { Page } from 'playwright'
+
+const data: Wallet[] = [
+ {
+ id: '1',
+ name: '1',
+ description: '1',
+ dateCreated: '2024-03-20T18:49:01Z',
+ lastUpdated: '2024-03-20T18:49:01Z',
+ metadata: {
+ type: 'seed',
+ seedHash:
+ '302626ee851d3712c19007c6141fc7f3a229ae64e6df71403a31b3ad131ac53657cf3dcae9405d9203e57204eb0084a7899224880e8d8beb4e103949255d861e',
+ },
+ },
+ {
+ id: '2',
+ name: '2',
+ description: '2',
+ dateCreated: '2024-03-20T18:49:50Z',
+ lastUpdated: '2024-03-20T19:30:04Z',
+ metadata: {
+ type: 'seed',
+ seedHash:
+ '302626ee851d3712c19007c6141fc7f3a229ae64e6df71403a31b3ad131ac53657cf3dcae9405d9203e57204eb0084a7899224880e8d8beb4e103949255d861e',
+ },
+ },
+ {
+ id: '3',
+ name: '3',
+ description: '3',
+ dateCreated: '2024-03-20T18:53:27Z',
+ lastUpdated: '2024-03-20T18:53:27Z',
+ metadata: {
+ type: 'watch',
+ },
+ },
+]
+
+export async function mockApiWallets({
+ page,
+ createWallet,
+}: {
+ page: Page
+ createWallet?: Wallet
+}) {
+ const json = cloneDeep(data)
+ await page.route('**/api/wallets*', async (route) => {
+ if (route.request().method() === 'POST') {
+ await route.fulfill({ json: createWallet })
+ } else {
+ await route.fulfill({ json })
+ }
+ })
+ return json
+}
diff --git a/apps/walletd-e2e/src/specs/login.spec.ts b/apps/walletd-e2e/src/specs/login.spec.ts
new file mode 100644
index 000000000..58a2676ba
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/login.spec.ts
@@ -0,0 +1,9 @@
+import { test, expect } from '@playwright/test'
+import { mockApiDefaults } from '../mocks/mock'
+import { login } from '../fixtures/login'
+
+test('login', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+ await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible()
+})
diff --git a/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts
new file mode 100644
index 000000000..f4a327f00
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts
@@ -0,0 +1,50 @@
+import { WalletAddress } from '@siafoundation/react-walletd'
+
+export const seed =
+ 'ridge business wish transfer home glove office salt wealth baby journey diary'
+
+export const newWallet = {
+ id: '100',
+ name: 'test send wallet',
+ description: 'wallet description',
+ dateCreated: new Date().toISOString(),
+ lastUpdated: new Date().toISOString(),
+ metadata: {
+ type: 'seed',
+ seedHash:
+ 'd4ad57da37936a053b0587fcc4964e9de89da8fbd610f7de190c32e22e6acd2a1c8f9f88bf219ffac04801b709712f3a2e64cc4f0271c402a6c4659573024440',
+ },
+}
+
+export const mockWalletAddressesResponse: WalletAddress[] = [
+ {
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ description: '',
+ metadata: {
+ index: 0,
+ publicKey:
+ 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535',
+ },
+ },
+ {
+ address:
+ 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8',
+ description: '',
+ metadata: {
+ index: 1,
+ publicKey:
+ 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43',
+ },
+ },
+ {
+ address:
+ 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518',
+ description: '',
+ metadata: {
+ index: 2,
+ publicKey:
+ 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa',
+ },
+ },
+]
diff --git a/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts
new file mode 100644
index 000000000..882350bb0
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts
@@ -0,0 +1,51 @@
+import { test, expect } from '@playwright/test'
+import { mockApiDefaults } from '../../mocks/mock'
+import { login } from '../../fixtures/login'
+import { createWallet } from '../../fixtures/createWallet'
+import { navigateToWallet } from '../../fixtures/navigateToWallet'
+import {
+ seed,
+ newWallet,
+ mockWalletAddressesResponse,
+} from './seedGenerateAddresses.mock'
+import { cloneDeep } from '@technically/lodash'
+
+const defaultMockWalletResponses = {
+ addresses: mockWalletAddressesResponse,
+}
+
+test('generate new addresses', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+
+ const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse)
+ mockAddressesInvalid.forEach((address) => {
+ address.metadata.publicKey = undefined
+ })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: defaultMockWalletResponses,
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('view addresses').click()
+ await page.getByRole('button', { name: 'Add addresses' }).click()
+ await page.locator('input[name=count]').fill('5')
+ await page.getByRole('button', { name: 'Continue' }).click()
+ await expect(
+ page.getByText('60838523a4bdeeec5b4f70a6678da48a77ad58fe...')
+ ).toBeVisible()
+ await expect(
+ page.getByText('65b40f6a720352ad5b9546b9f5077209672914cc...')
+ ).toBeVisible()
+ await expect(
+ page.getByText('e94e8113563a549f95ff3904dccf77f1b8fbaad4...')
+ ).toBeVisible()
+ await expect(
+ page.getByText('cc7241334772c6d10d47882b06b21a60242a19c3...')
+ ).toBeVisible()
+ await expect(
+ page.getByText('170173c40ca0f39f9618da30af14c390c7ce7024...')
+ ).toBeVisible()
+})
diff --git a/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts
new file mode 100644
index 000000000..8fa2a7212
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts
@@ -0,0 +1,154 @@
+import { SiacoinElement } from '@siafoundation/types'
+import {
+ WalletAddress,
+ WalletBalanceResponse,
+ WalletFundResponse,
+} from '@siafoundation/react-walletd'
+import { toHastings } from '@siafoundation/units'
+
+export const seed =
+ 'ridge business wish transfer home glove office salt wealth baby journey diary'
+
+export const receiveAddress =
+ '5739945c21e60afd70eaf97ccd33ea27836e0219212449f39e4b38acaa8b3119aa4150a9ef0f'
+export const changeAddress =
+ '170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518'
+
+export const newWallet = {
+ id: '100',
+ name: 'test send wallet',
+ description: 'wallet description',
+ dateCreated: new Date().toISOString(),
+ lastUpdated: new Date().toISOString(),
+ metadata: {
+ type: 'seed',
+ seedHash:
+ 'd4ad57da37936a053b0587fcc4964e9de89da8fbd610f7de190c32e22e6acd2a1c8f9f88bf219ffac04801b709712f3a2e64cc4f0271c402a6c4659573024440',
+ },
+}
+
+export const mockWalletBalanceResponse: WalletBalanceResponse = {
+ siacoins: toHastings('100').toString(),
+ immatureSiacoins: toHastings('10').toString(),
+ siafunds: 10,
+}
+
+export const mockWalletOutputsSiacoinResponse: SiacoinElement[] = [
+ {
+ id: 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ leafIndex: 304248,
+ merkleProof: [
+ 'h:0a7a4c392f78899e3c38c5cd9e6a673b2c7afec97930af539af9c8e20209aa78',
+ 'h:a1e074dc48634a234b7366a0d7ab19cd05e3e698e1d44bf07e24d75ae0c65b3c',
+ 'h:44d107342962e2068d289ce090c87b7bf0c847f734bdfad10db5546e402c3ad7',
+ 'h:64ea3e65ecd5ebc1c0ce014673148060855ce550571208b6303b8e2a83e33451',
+ 'h:19e37bf7747c6d8c7b87d2474dbbd3a8f5b26d89642f8e1af4b9b02abdfb2ea6',
+ 'h:1fc2ac9f70211a3be6f334db14feb8b327458aee49d3539770640cbdec9b4a5f',
+ 'h:defbbc18c64349f11e75537955861ececce0fadf10baec456f6c74b024820af1',
+ 'h:87d27ff868ca3b1dce59ae754eaec48239718e81e2e6f3b7b418f5a00362bcf7',
+ 'h:93d823c55fbd09de462a8e355921433b3693d63de58ea8e2780a2c2ffabd0fee',
+ 'h:a68820d6b79b2735b15c69d0fc26b11252bb27f22b9088559ed13f9420f5dda1',
+ 'h:1bbcead690290291ea9628214a121ef783411693975171803bf5716a3a6ff19b',
+ ],
+ siacoinOutput: {
+ value: '1000000000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ maturityHeight: 0,
+ },
+ {
+ id: 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ leafIndex: 305723,
+ merkleProof: [
+ 'h:8c02aeec48de589ce497ebe72fb8b527cfe022ef513fcfdc56745c84832f00ec',
+ 'h:1bf63b9959e60272fd7a48a8cecd4120a852c0e14557ea27ccad6ea2071e70b3',
+ 'h:21b7e1606e9fd677059c58a1a687682182f71ce09e071431dcaff823a3a5d49e',
+ 'h:e81ac37d3b4db6166dc1bb10ebfa49f57cbf99aababd36ee4e3e5e12082dc6dc',
+ 'h:ecc307c6c3e505d97ccf821938e5e5702ef0130d33c991ca95735f7d9706a4b8',
+ 'h:9560060ee399793f102e092afdfdbdd33692706256955e8390af552de0addfc0',
+ ],
+ siacoinOutput: {
+ value: '97988210000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ maturityHeight: 0,
+ },
+]
+
+export const mockWalletFundResponse: WalletFundResponse = {
+ transaction: {
+ siacoinInputs: [
+ {
+ parentID:
+ 'scoid:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ unlockConditions: {
+ timelock: 0,
+ publicKeys: null,
+ signaturesRequired: 0,
+ },
+ },
+ {
+ parentID:
+ 'scoid:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ unlockConditions: {
+ timelock: 0,
+ publicKeys: null,
+ signaturesRequired: 0,
+ },
+ },
+ ],
+ siacoinOutputs: [
+ {
+ value: '1000000000000000000000000',
+ address:
+ 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8',
+ },
+ {
+ value: '97984280000000000000000000',
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ },
+ ],
+ minerFees: ['3930000000000000000000'],
+ },
+ toSign: [
+ 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb',
+ 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724',
+ ],
+ dependsOn: null,
+}
+
+export const mockWalletAddressesResponse: WalletAddress[] = [
+ {
+ address:
+ 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981',
+ description: '',
+ metadata: {
+ index: 0,
+ publicKey:
+ 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535',
+ },
+ },
+ {
+ address:
+ 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8',
+ description: '',
+ metadata: {
+ index: 1,
+ publicKey:
+ 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43',
+ },
+ },
+ {
+ address:
+ 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518',
+ description: '',
+ metadata: {
+ index: 2,
+ publicKey:
+ 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa',
+ },
+ },
+]
diff --git a/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts
new file mode 100644
index 000000000..8f978625b
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts
@@ -0,0 +1,194 @@
+import { test, expect } from '@playwright/test'
+import { mockApiDefaults } from '../../mocks/mock'
+import { login } from '../../fixtures/login'
+import { createWallet } from '../../fixtures/createWallet'
+import { navigateToWallet } from '../../fixtures/navigateToWallet'
+import {
+ seed,
+ newWallet,
+ receiveAddress,
+ changeAddress,
+ mockWalletFundResponse,
+ mockWalletOutputsSiacoinResponse,
+ mockWalletAddressesResponse,
+} from './seedSendSiacoin.mock'
+import { fillComposeTransactionSiacoin } from '../../fixtures/sendSiacoinDialog'
+import { cloneDeep } from '@technically/lodash'
+
+const defaultMockWalletResponses = {
+ fund: mockWalletFundResponse,
+ outputsSiacoin: mockWalletOutputsSiacoinResponse,
+ addresses: mockWalletAddressesResponse,
+}
+
+test('send siacoin with a seed wallet', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: defaultMockWalletResponses,
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('send').click()
+ await fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount: '1',
+ })
+ await expect(page.getByText('The wallet is currently unlocked')).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+
+ await page
+ .getByRole('button', { name: 'Sign and broadcast transaction' })
+ .click()
+ await expect(
+ page.getByText('Transaction successfully broadcasted')
+ ).toBeVisible()
+ await expect(page.getByText(receiveAddress.slice(0, 5))).toBeVisible()
+ await expect(page.getByText(changeAddress.slice(0, 5))).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+})
+
+test('errors if the input to sign is not found on the transaction', async ({
+ page,
+}) => {
+ await mockApiDefaults({ page })
+ const mockFundInvalid = cloneDeep(mockWalletFundResponse)
+ mockFundInvalid.transaction.siacoinInputs[0].parentID =
+ 'scoid:bb3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb'
+ await login({ page })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: {
+ ...defaultMockWalletResponses,
+ fund: mockFundInvalid,
+ },
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('send').click()
+ await fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount: '1',
+ })
+ await expect(page.getByText('The wallet is currently unlocked')).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+
+ await page
+ .getByRole('button', { name: 'Sign and broadcast transaction' })
+ .click()
+ await expect(page.getByText('Missing input')).toBeVisible()
+})
+
+test('errors if the inputs matching utxo is not found', async ({ page }) => {
+ await mockApiDefaults({ page })
+ const mockOutputsInvalid = cloneDeep(mockWalletOutputsSiacoinResponse)
+ mockOutputsInvalid.forEach((output) => {
+ output.id = 'not the matching id'
+ })
+ await login({ page })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: {
+ ...defaultMockWalletResponses,
+ outputsSiacoin: mockOutputsInvalid,
+ },
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('send').click()
+ await fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount: '1',
+ })
+ await expect(page.getByText('The wallet is currently unlocked')).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+
+ await page
+ .getByRole('button', { name: 'Sign and broadcast transaction' })
+ .click()
+ await expect(page.getByText('Missing utxo')).toBeVisible()
+})
+
+test('errors if the address is missing its index', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+
+ const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse)
+ mockAddressesInvalid.forEach((address) => {
+ address.metadata.index = undefined
+ })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: {
+ ...defaultMockWalletResponses,
+ addresses: mockAddressesInvalid,
+ },
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('send').click()
+ await fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount: '1',
+ })
+ await expect(page.getByText('The wallet is currently unlocked')).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+
+ await page
+ .getByRole('button', { name: 'Sign and broadcast transaction' })
+ .click()
+ await expect(page.getByText('Missing address index')).toBeVisible()
+})
+
+test('errors if the address is missing its public key', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+
+ const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse)
+ mockAddressesInvalid.forEach((address) => {
+ address.metadata.publicKey = undefined
+ })
+ await createWallet({
+ page,
+ newWallet,
+ seed,
+ responses: {
+ ...defaultMockWalletResponses,
+ addresses: mockAddressesInvalid,
+ },
+ })
+ await navigateToWallet({ page, wallet: newWallet })
+ await page.getByLabel('send').click()
+ await fillComposeTransactionSiacoin({
+ page,
+ receiveAddress,
+ changeAddress,
+ amount: '1',
+ })
+ await expect(page.getByText('The wallet is currently unlocked')).toBeVisible()
+ await expect(page.getByText('Total')).toBeVisible()
+ await expect(page.getByText('1.004 SC')).toBeVisible()
+
+ await page
+ .getByRole('button', { name: 'Sign and broadcast transaction' })
+ .click()
+ await expect(page.getByText('Missing address public key')).toBeVisible()
+})
diff --git a/apps/walletd-e2e/src/specs/walletCreate.spec.ts b/apps/walletd-e2e/src/specs/walletCreate.spec.ts
new file mode 100644
index 000000000..929309078
--- /dev/null
+++ b/apps/walletd-e2e/src/specs/walletCreate.spec.ts
@@ -0,0 +1,30 @@
+import { test, expect } from '@playwright/test'
+import { mockApiDefaults } from '../mocks/mock'
+import { login } from '../fixtures/login'
+import { createWallet } from '../fixtures/createWallet'
+
+const seed =
+ 'credit tonight gauge army noodle reopen pepper property try mom taste solid'
+
+const newWallet = {
+ id: '100',
+ name: 'new wallet 1',
+ description: 'wallet description',
+ dateCreated: new Date().toISOString(),
+ lastUpdated: new Date().toISOString(),
+ metadata: {
+ type: 'seed',
+ seedHash:
+ '9fe676742eb80d61fea4476e2d33a088d708a24c2762fa028f9bfbe693612d15b83223f7ba5bc2e0fe2390d243a47a18144f19acd34b105757b1ac7751da821f',
+ },
+}
+
+test('wallet create', async ({ page }) => {
+ await mockApiDefaults({ page })
+ await login({ page })
+ await createWallet({ page, newWallet, seed })
+ await expect(page.locator(`span:text('${newWallet.name}')`)).toBeVisible()
+ await page.locator(`span:text('${newWallet.name}')`).click()
+ await expect(page.getByText('The wallet has no')).toBeVisible()
+ await expect(page.getByRole('link', { name: 'Addresses' })).toBeVisible()
+})
diff --git a/apps/walletd-e2e/tsconfig.json b/apps/walletd-e2e/tsconfig.json
new file mode 100644
index 000000000..fd1b9df91
--- /dev/null
+++ b/apps/walletd-e2e/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "allowJs": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "incremental": true,
+ "types": ["jest", "node"]
+ },
+ "include": ["**/*.ts", "**/*.js"]
+}
diff --git a/apps/walletd/components/Wallet/AddressesButton.tsx b/apps/walletd/components/Wallet/AddressesButton.tsx
index 1e6ccdb70..6e25c4977 100644
--- a/apps/walletd/components/Wallet/AddressesButton.tsx
+++ b/apps/walletd/components/Wallet/AddressesButton.tsx
@@ -8,6 +8,7 @@ export function AddressesButton() {
const id = router.query.id as string
return (
{wallet?.metadata.type !== 'watch' && (