Skip to content

Commit

Permalink
feat(explorer): cluster testing
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Feb 26, 2025
1 parent d9bcdcb commit 594c375
Show file tree
Hide file tree
Showing 36 changed files with 425 additions and 123 deletions.
5 changes: 2 additions & 3 deletions apps/explorer-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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.
Expand All @@ -25,8 +24,8 @@ export default defineConfig({
trace: 'on-first-retry',
video: 'on-first-retry',
},
// Timeout per test.
timeout: 60_000,
// Timeout per test. The cluster takes up to 30 seconds to start and form contracts.
timeout: 180_000,
expect: {
// Raise the timeout because it is running against next dev mode
// which requires compilation the first to a page is visited.
Expand Down
90 changes: 90 additions & 0 deletions apps/explorer-e2e/src/fixtures/cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Explored } from '@siafoundation/explored-js'
import { Hostd } from '@siafoundation/hostd-js'
import { Bus } from '@siafoundation/renterd-js'
import { Walletd } from '@siafoundation/walletd-js'
import { clusterd, setupCluster } from '@siafoundation/clusterd'
import { BrowserContext } from 'playwright'

export type Cluster = Awaited<ReturnType<typeof startCluster>>

export async function startCluster({
renterdCount = 1,
walletdCount = 1,
hostdCount = 3,
context,
}: {
renterdCount?: number
walletdCount?: number
hostdCount?: number
context: BrowserContext
}) {
await setupCluster({
exploredCount: 1,
renterdCount,
walletdCount,
hostdCount,
})
const explored = clusterd.nodes.find((n) => n.type === 'explored')
const renterds = clusterd.nodes.filter((n) => n.type === 'renterd')
const hostds = clusterd.nodes.filter((n) => n.type === 'hostd')
const walletds = clusterd.nodes.filter((n) => n.type === 'walletd')
if (
explored === undefined ||
renterds.length !== renterdCount ||
hostds.length !== hostdCount ||
walletds.length !== walletdCount
) {
throw new Error('Failed to start cluster')
}
const daemons = {
explored: {
node: explored,
api: Explored({
api: `${explored.apiAddress}/api`,
password: explored.password,
}),
},
renterds: renterds.map((r) => ({
node: r,
api: Bus({
api: `${r.apiAddress}/api`,
password: r.password,
}),
})),
hostds: hostds.map((h) => ({
node: h,
api: Hostd({
api: `${h.apiAddress}/api`,
password: h.password,
}),
})),
walletds: walletds.map((w) => ({
node: w,
api: Walletd({
api: `${w.apiAddress}/api`,
password: w.password,
}),
})),
}
console.log(`
clusterd: http://localhost:${clusterd.managementPort}
explored: ${daemons.explored.node.apiAddress}
renterds: ${daemons.renterds.map((r) => r.node.apiAddress)}
hostds: ${daemons.hostds.map((h) => h.node.apiAddress)}
walletds: ${daemons.walletds.map((w) => w.node.apiAddress)}
`)
// Set the explorerd address cookie so that the explorer app overrides the
// normal zen address with the testnet cluster address.
await context.addCookies([
{
// This should match `exploredCustomApiCookieName` in apps/explorer/config/explored.ts
name: 'exploredAddress',
value: daemons.explored.node.apiAddress,
url: 'http://localhost:3005',
},
])
return {
clusterd,
daemons,
}
}
9 changes: 0 additions & 9 deletions apps/explorer-e2e/src/fixtures/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,6 @@ export const TEST_TX_1 = {
},
}

export const TEST_ADDRESS_1 = {
id: '68bf48e81536f2221f3809aa9d1c89c1c869a17c6f186a088e49fd2605e4bfaaa24f26e4c42c',
display: {
title: 'Address 68bf48e81536f22...',
transactionNumber: '500 events',
transactionID: '95a2b31155be...',
},
}

export const TEST_CONTRACT_1 = {
id: '25c94822bf7bd86a92d28a148d9d30151949f3599bf93af0df7b4f1e1b3c990d',
renewedFromTitle: 'Contract 494d147a8028217...',
Expand Down
82 changes: 82 additions & 0 deletions apps/explorer-e2e/src/fixtures/walletd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Walletd } from '@siafoundation/walletd-js'
import { blake2bHex } from 'blakejs'
import { WalletAddressMetadata } from '@siafoundation/walletd-types'
import { to } from '@siafoundation/request'
import { mine } from '@siafoundation/clusterd'
import { humanSiacoin } from '@siafoundation/units'
import { Cluster } from './cluster'

export async function addWalletToWalletd(walletd: ReturnType<typeof Walletd>) {
// For some reason when this code runs on GitHub Actions it throws an ESM
// import error for the SDK. Running locally it works fine.
// Error: require() of ES Module sdk/index.esm.js from /fixtures/walletd.ts not supported.
// This dynamic import is a workaround.
const { initSDK, getSDK } = await import('@siafoundation/sdk')
await initSDK()
const sdk = getSDK()
const { phrase: mnemonic } = sdk.wallet.generateSeedPhrase()
const mnemonicHash = blake2bHex(mnemonic)
const [wallet, walletError] = await to(
walletd.walletAdd({
data: {
name: 'test',
description: 'test',
metadata: {
type: 'seed',
mnemonicHash,
},
},
})
)
if (!wallet || walletError) {
throw new Error(`Failed to add wallet: ${walletError}`)
}
const kp = sdk.wallet.keyPairFromSeedPhrase(mnemonic, 0)
const suh = sdk.wallet.standardUnlockHash(kp.publicKey)
const uc = sdk.wallet.standardUnlockConditions(kp.publicKey)
const metadata: WalletAddressMetadata = {
index: 0,
}
const [, addressError] = await to(
walletd.walletAddressAdd({
params: {
id: wallet.id,
},
data: {
address: suh.address,
description: '',
spendPolicy: {
type: 'uc',
policy: uc.unlockConditions,
},
metadata,
},
})
)
if (addressError) {
throw new Error(`Failed to add address: ${addressError}`)
}
return { wallet, address: suh.address }
}

// renterd cluster nodes have siacoins we can use to fund other wallets.
export async function sendSiacoinFromRenterd(
renterd: Cluster['daemons']['renterds'][number],
address: string,
amount: string
) {
console.log(`Sending ${humanSiacoin(amount)} from renterd to:`, address)
try {
// Send some funds to the wallet.
await renterd.api.walletSend({
data: {
address,
amount,
subtractMinerFee: false,
},
})
await mine(1)
} catch (e) {
console.log('error sending siacoin', e)
}
}
103 changes: 76 additions & 27 deletions apps/explorer-e2e/src/specs/address.spec.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,102 @@
import { test, expect } from '@playwright/test'
import { ExplorerApp } from '../fixtures/ExplorerApp'
import { TEST_ADDRESS_1 } from '../fixtures/constants'
import { keys } from '../utils'
import { startCluster } from '../fixtures/cluster'
import {
mine,
renterdWaitForContracts,
teardownCluster,
} from '@siafoundation/clusterd'
import { addWalletToWalletd, sendSiacoinFromRenterd } from '../fixtures/walletd'
import { Cluster } from '../fixtures/cluster'
import { toHastings } from '@siafoundation/units'

let explorerApp: ExplorerApp
let cluster: Cluster

test.beforeEach(async ({ page }) => {
test.beforeEach(async ({ page, context }) => {
// Start the cluster which includes an explored node, a renterd node, a
// walletd node, and 3 hostd nodes.
cluster = await startCluster({ context })
// The renterd node will automatically form contracts with the hostd nodes.
// Wait for the contracts to form so that all related activity is on the
// blockchain and visible to explored.
await renterdWaitForContracts({
renterdNode: cluster.daemons.renterds[0].node,
hostdCount: cluster.daemons.hostds.length,
})
explorerApp = new ExplorerApp(page)
})

test.afterEach(async () => {
await teardownCluster()
})

test('address can be searched by id', async ({ page }) => {
const wallet = await cluster.daemons.renterds[0].api.wallet()
await explorerApp.goTo('/')
await explorerApp.navigateBySearchBar(TEST_ADDRESS_1.id)

await expect(page.getByText(TEST_ADDRESS_1.display.title)).toBeVisible()
await explorerApp.navigateBySearchBar(wallet.data.address)
await expect(
page
.getByTestId('entity-heading')
.getByText(`Address ${wallet.data.address.slice(0, 5)}`)
).toBeVisible()
})

test('address can be directly navigated to by id', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)

await expect(page.getByText(TEST_ADDRESS_1.display.title)).toBeVisible()
const wallet = await cluster.daemons.renterds[0].api.wallet()
await explorerApp.goTo('/address/' + wallet.data.address)
await expect(
page
.getByTestId('entity-heading')
.getByText(`Address ${wallet.data.address.slice(0, 5)}`)
).toBeVisible()
})

test('address displays the intended data', async ({ page }) => {
const displayKeys = keys(TEST_ADDRESS_1.display)

await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)

for (const key of displayKeys) {
const currentProperty = TEST_ADDRESS_1.display[key]
await expect(page.getByText(currentProperty)).toBeVisible()
}
})

test('address can navigate to the unspent outputs list', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)
const walletd = cluster.daemons.walletds[0]
const { wallet, address } = await addWalletToWalletd(walletd.api)
await sendSiacoinFromRenterd(
cluster.daemons.renterds[0],
address,
toHastings(1_000_000).toString()
)
await mine(10)
const events = await walletd.api.walletEvents({
params: {
id: wallet.id,
limit: 1_000,
offset: 0,
},
})
const outputs = await walletd.api.walletOutputsSiacoin({
params: {
id: wallet.id,
},
})
await explorerApp.goTo('/address/' + address)
await expect(page.getByText(`Address ${address.slice(0, 5)}`)).toBeVisible()
await expect(page.getByText(events.data[0].id.slice(0, 5))).toBeVisible()
await expect(page.getByText(`${events.data.length} events`)).toBeVisible()
await page.getByRole('tab').getByText('Unspent outputs').click()

await expect(page.getByText('Siacoin output').first()).toBeVisible()
await expect(
page.getByText(outputs.data.outputs[0].id.slice(0, 5))
).toBeVisible()
})

test('address can navigate through to a transaction', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)
const wallet = await cluster.daemons.renterds[0].api.wallet()
const events = await cluster.daemons.renterds[0].api.walletEvents({
params: {
limit: 1,
offset: 0,
},
})
await explorerApp.goTo('/address/' + wallet.data.address)

await page
.locator(
'a[data-testid="entity-link"][href*="b24a7d623b82206cd3363db0c0d41446f106a59227d86ef081d601315dbd8cca"]'
)
.locator(`a[data-testid="entity-link"][href*="${events.data[0].id}"]`)
.click()

await expect(page.getByText('Transaction b24a7d623b82206...')).toBeVisible()
await expect(page.getByText(events.data[0].id.slice(0, 5))).toBeVisible()
})
4 changes: 2 additions & 2 deletions apps/explorer/app/address/[id]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { humanSiacoin, humanSiafund } from '@siafoundation/units'
import { getOGImage } from '../../../components/OGImageEntity'
import { truncate } from '@siafoundation/design-system'
import { to } from '@siafoundation/request'
import { explored } from '../../../config/explored'
import { getExplored } from '../../../lib/explored'

export const revalidate = 0

Expand All @@ -18,7 +18,7 @@ export default async function Image({ params }) {
const address = params?.id as string

const [[balance, balanceError]] = await Promise.all([
to(explored.addressBalance({ params: { address } })),
to(getExplored().addressBalance({ params: { address } })),
])

if (balanceError) throw balanceError
Expand Down
8 changes: 4 additions & 4 deletions apps/explorer/app/address/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { buildMetadata } from '../../../lib/utils'
import { notFound } from 'next/navigation'
import { truncate } from '@siafoundation/design-system'
import { to } from '@siafoundation/request'
import { explored } from '../../../config/explored'
import { getExplored } from '../../../lib/explored'

export function generateMetadata({ params }): Metadata {
const id = decodeURIComponent((params?.id as string) || '')
Expand All @@ -29,9 +29,9 @@ export default async function Page({ params }) {
[events, eventsError],
[unspentOutputs, unspentOutputsError],
] = await Promise.all([
to(explored.addressBalance({ params: { address } })),
to(explored.addressEvents({ params: { address, limit: 500 } })),
to(explored.addressSiacoinUTXOs({ params: { address } })),
to(getExplored().addressBalance({ params: { address } })),
to(getExplored().addressEvents({ params: { address, limit: 500 } })),
to(getExplored().addressSiacoinUTXOs({ params: { address } })),
])

if (balanceError) throw balanceError
Expand Down
Loading

0 comments on commit 594c375

Please sign in to comment.