Skip to content

Commit 81d374f

Browse files
committed
feat: renterd wallet send
1 parent a8c22bb commit 81d374f

File tree

30 files changed

+673
-166
lines changed

30 files changed

+673
-166
lines changed

.changeset/moody-zebras-fly.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'hostd': minor
3+
'renterd': minor
4+
---
5+
6+
The send siacoin feature now calculates the fee using the daemon's recommended fee per byte and a standard transaction size.

.changeset/shy-dots-complain.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@siafoundation/hostd-types': minor
3+
'@siafoundation/hostd-js': minor
4+
'@siafoundation/hostd-react': minor
5+
---
6+
7+
The wallet send API now supports the subtractMinerFee option.

.changeset/thick-rats-smell.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@siafoundation/renterd-js': patch
3+
'@siafoundation/renterd-react': patch
4+
'@siafoundation/renterd-types': patch
5+
---
6+
7+
Fixed the route for the recommended fee API.

.changeset/young-gifts-return.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@siafoundation/renterd-js': minor
3+
'@siafoundation/renterd-react': minor
4+
'@siafoundation/renterd-types': minor
5+
---
6+
7+
Added the wallet send API.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {
2+
mockApiSiaCentralExchangeRates,
3+
mockApiSiaCentralHostsNetworkAverages,
4+
} from '@siafoundation/sia-central-mock'
5+
import { login } from './login'
6+
import { navigateToConfig } from './navigate'
7+
import { configResetAllSettings } from './configResetAllSettings'
8+
import { setViewMode } from './configViewMode'
9+
import { Page } from 'playwright'
10+
import { mockApiSiaScanExchangeRates } from './siascan'
11+
import { setCurrencyDisplay } from './preferences'
12+
13+
export async function beforeTest(page: Page, shouldResetConfig = true) {
14+
await mockApiSiaCentralExchangeRates({ page })
15+
await mockApiSiaCentralHostsNetworkAverages({ page })
16+
await mockApiSiaScanExchangeRates({ page })
17+
await login({ page })
18+
19+
// Reset state.
20+
await setCurrencyDisplay(page, 'bothPreferSc')
21+
if (shouldResetConfig) {
22+
await navigateToConfig({ page })
23+
await configResetAllSettings({ page })
24+
await setViewMode({ page, state: 'basic' })
25+
await navigateToConfig({ page })
26+
}
27+
}
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { Page, expect } from '@playwright/test'
22

33
export async function navigateToDashboard({ page }: { page: Page }) {
4-
await page.getByLabel('Overview').click()
4+
await page.getByTestId('sidenav').getByLabel('Overview').click()
55
await expect(page.getByTestId('navbar').getByText('Overview')).toBeVisible()
66
}
77

88
export async function navigateToConfig({ page }: { page: Page }) {
9-
await page.getByLabel('Configuration').click()
9+
await page.getByTestId('sidenav').getByLabel('Configuration').click()
1010
await expect(
1111
page.getByTestId('navbar').getByText('Configuration')
1212
).toBeVisible()
1313
}
1414

1515
export async function navigateToVolumes({ page }: { page: Page }) {
16-
await page.getByLabel('Volumes').click()
16+
await page.getByTestId('sidenav').getByLabel('Volumes').click()
1717
await expect(page.getByTestId('navbar').getByText('Volumes')).toBeVisible()
1818
}
19+
20+
export async function navigateToWallet(page: Page) {
21+
await page.getByTestId('sidenav').getByLabel('Wallet').click()
22+
await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible()
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Page } from 'playwright'
2+
import { fillSelectInputByName } from './selectInput'
3+
4+
export async function setCurrencyDisplay(
5+
page: Page,
6+
display: 'sc' | 'fiat' | 'bothPreferSc' | 'bothPreferFiat'
7+
) {
8+
await page.getByLabel('App preferences').click()
9+
await fillSelectInputByName(page, 'currencyDisplay', display)
10+
await page.getByRole('dialog').getByLabel('close').click()
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Page } from 'playwright'
2+
3+
export async function mockApiSiaScanExchangeRates({ page }: { page: Page }) {
4+
await page.route(
5+
'https://api.siascan.com/exchange-rate/siacoin/*',
6+
async (route) => {
7+
await route.fulfill({ json: 0.003944045283 })
8+
}
9+
)
10+
}

apps/hostd-e2e/src/specs/config.spec.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
import { test, expect } from '@playwright/test'
2-
import { login } from '../fixtures/login'
32
import {
43
expectSwitchByLabel,
54
expectSwitchVisible,
65
setSwitchByLabel,
76
} from '../fixtures/switchValue'
87
import { setViewMode } from '../fixtures/configViewMode'
98
import { navigateToConfig } from '../fixtures/navigate'
10-
import { mockApiSiaCentralExchangeRates } from '@siafoundation/sia-central-mock'
11-
import { configResetAllSettings } from '../fixtures/configResetAllSettings'
129
import {
1310
expectTextInputByName,
1411
expectTextInputNotVisible,
1512
fillTextInputByName,
1613
} from '../fixtures/textInput'
1714
import { fillSelectInputByName } from '../fixtures/selectInput'
15+
import { beforeTest } from '../fixtures/beforeTest'
1816

19-
test('basic field change and save behaviour', async ({ page }) => {
20-
// Set up.
21-
await mockApiSiaCentralExchangeRates({ page })
22-
await login({ page })
17+
test.beforeEach(async ({ page }) => {
18+
await beforeTest(page)
19+
})
2320

21+
test('basic field change and save behaviour', async ({ page }) => {
2422
// Reset state.
25-
await navigateToConfig({ page })
26-
await configResetAllSettings({ page })
2723
await setViewMode({ page, state: 'advanced' })
2824

2925
// Test that values can be updated.
@@ -63,10 +59,6 @@ test('basic field change and save behaviour', async ({ page }) => {
6359
})
6460

6561
test('pin switches should show in both view modes', async ({ page }) => {
66-
// Set up.
67-
await mockApiSiaCentralExchangeRates({ page })
68-
await login({ page })
69-
7062
await navigateToConfig({ page })
7163
await setViewMode({ page, state: 'basic' })
7264
await expectSwitchVisible(page, 'shouldPinStoragePrice')
@@ -83,14 +75,7 @@ test('pin switches should show in both view modes', async ({ page }) => {
8375
})
8476

8577
test('dynamic max collateral suggestion', async ({ page }) => {
86-
// Set up.
87-
await mockApiSiaCentralExchangeRates({ page })
88-
await login({ page })
89-
90-
// Reset state.
9178
await navigateToConfig({ page })
92-
await configResetAllSettings({ page })
93-
await setViewMode({ page, state: 'basic' })
9479
await fillTextInputByName(page, 'maxCollateral', '777')
9580
await expect(
9681
page

apps/hostd-e2e/src/specs/volumes.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { test } from '@playwright/test'
22
import { navigateToVolumes } from '../fixtures/navigate'
3-
import { login } from '../fixtures/login'
43
import {
54
createVolume,
65
deleteVolume,
76
deleteVolumeIfExists,
87
} from '../fixtures/volumes'
8+
import { beforeTest } from '../fixtures/beforeTest'
9+
10+
test.beforeEach(async ({ page }) => {
11+
await beforeTest(page, false)
12+
})
913

1014
test('can create and delete a volume', async ({ page }) => {
1115
const name = 'my-new-volume'
1216
const path = '/data'
13-
await login({ page })
1417
await navigateToVolumes({ page })
1518
await deleteVolumeIfExists(page, name, path)
1619
await createVolume(page, name, path)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { test, expect } from '@playwright/test'
2+
import { navigateToWallet } from '../fixtures/navigate'
3+
import { random } from '@technically/lodash'
4+
import { beforeTest } from '../fixtures/beforeTest'
5+
import { fillTextInputByName } from '../fixtures/textInput'
6+
import { setSwitchByLabel } from '../fixtures/switchValue'
7+
import BigNumber from 'bignumber.js'
8+
9+
test.beforeEach(async ({ page }) => {
10+
await beforeTest(page, false)
11+
})
12+
13+
test('send siacoin with include fee off', async ({ page }) => {
14+
const receiveAddress =
15+
'5739945c21e60afd70eaf97ccd33ea27836e0219212449f39e4b38acaa8b3119aa4150a9ef0f'
16+
const amount = String(random(1, 5))
17+
const amountString = `${amount}.000 SC`
18+
const amountWithFeeString = `${amount}.012 SC`
19+
20+
await navigateToWallet(page)
21+
22+
// Setup.
23+
await page.getByLabel('send').click()
24+
const sendDialog = page.getByRole('dialog', { name: 'Send siacoin' })
25+
await fillTextInputByName(page, 'address', receiveAddress)
26+
await fillTextInputByName(page, 'siacoin', amount)
27+
await expect(
28+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
29+
).toBeVisible()
30+
await expect(
31+
sendDialog.getByTestId('total').getByText(amountWithFeeString)
32+
).toBeVisible()
33+
34+
// Confirm.
35+
await page.getByRole('button', { name: 'Generate transaction' }).click()
36+
await expect(
37+
sendDialog.getByTestId('address').getByText(receiveAddress.slice(0, 5))
38+
).toBeVisible()
39+
await expect(
40+
sendDialog.getByTestId('amount').getByText(amountString)
41+
).toBeVisible()
42+
await expect(
43+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
44+
).toBeVisible()
45+
await expect(
46+
sendDialog.getByTestId('total').getByText(amountWithFeeString)
47+
).toBeVisible()
48+
49+
// Complete.
50+
await page.getByRole('button', { name: 'Broadcast transaction' }).click()
51+
await expect(
52+
page.getByText('Transaction successfully broadcasted.')
53+
).toBeVisible()
54+
await expect(
55+
sendDialog.getByTestId('address').getByText(receiveAddress.slice(0, 5))
56+
).toBeVisible()
57+
await expect(
58+
sendDialog.getByTestId('amount').getByText(amountString)
59+
).toBeVisible()
60+
await expect(
61+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
62+
).toBeVisible()
63+
await expect(
64+
sendDialog.getByTestId('total').getByText(amountWithFeeString)
65+
).toBeVisible()
66+
await expect(sendDialog.getByTestId('transactionId')).toBeVisible()
67+
68+
// List.
69+
// TODO: Add this after we migrate to the new events API.
70+
// await sendDialog.getByRole('button', { name: 'Close' }).click()
71+
// await expect(page.getByTestId('eventsTable')).toBeVisible()
72+
// await expect(
73+
// page.getByTestId('eventsTable').locator('tbody tr').first()
74+
// ).toBeVisible()
75+
// await expect(
76+
// page
77+
// .getByTestId('eventsTable')
78+
// .locator('tbody tr')
79+
// .first()
80+
// .getByTestId('amount')
81+
// .getByText(`-${amountWithFeeString}`)
82+
// ).toBeVisible()
83+
})
84+
85+
test('send siacoin with include fee on', async ({ page }) => {
86+
const receiveAddress =
87+
'5739945c21e60afd70eaf97ccd33ea27836e0219212449f39e4b38acaa8b3119aa4150a9ef0f'
88+
const amount = new BigNumber(random(1, 5))
89+
const amountString = `${amount.toFixed(3)} SC`
90+
const amountWithoutFeeString = `${amount.minus(0.012).toFixed(3)} SC`
91+
92+
await navigateToWallet(page)
93+
94+
// Setup.
95+
await page.getByLabel('send').click()
96+
const sendDialog = page.getByRole('dialog', { name: 'Send siacoin' })
97+
await fillTextInputByName(page, 'address', receiveAddress)
98+
await fillTextInputByName(page, 'siacoin', amount.toString())
99+
await setSwitchByLabel(page, 'include fee', true)
100+
await expect(
101+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
102+
).toBeVisible()
103+
await expect(
104+
sendDialog.getByTestId('total').getByText(amountString)
105+
).toBeVisible()
106+
107+
// Confirm.
108+
await page.getByRole('button', { name: 'Generate transaction' }).click()
109+
await expect(
110+
sendDialog.getByTestId('address').getByText(receiveAddress.slice(0, 5))
111+
).toBeVisible()
112+
await expect(
113+
sendDialog.getByTestId('amount').getByText(amountWithoutFeeString)
114+
).toBeVisible()
115+
await expect(
116+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
117+
).toBeVisible()
118+
await expect(
119+
sendDialog.getByTestId('total').getByText(amountString)
120+
).toBeVisible()
121+
122+
// Complete.
123+
await page.getByRole('button', { name: 'Broadcast transaction' }).click()
124+
await expect(
125+
page.getByText('Transaction successfully broadcasted.')
126+
).toBeVisible()
127+
await expect(
128+
sendDialog.getByTestId('address').getByText(receiveAddress.slice(0, 5))
129+
).toBeVisible()
130+
await expect(
131+
sendDialog.getByTestId('amount').getByText(amountWithoutFeeString)
132+
).toBeVisible()
133+
await expect(
134+
sendDialog.getByTestId('networkFee').getByText('0.012 SC')
135+
).toBeVisible()
136+
await expect(
137+
sendDialog.getByTestId('total').getByText(amountString)
138+
).toBeVisible()
139+
await expect(sendDialog.getByTestId('transactionId')).toBeVisible()
140+
141+
// List.
142+
// TODO: Add this after we migrate to the new events API.
143+
// await sendDialog.getByRole('button', { name: 'Close' }).click()
144+
// await expect(page.getByTestId('eventsTable')).toBeVisible()
145+
// await expect(
146+
// page.getByTestId('eventsTable').locator('tbody tr').first()
147+
// ).toBeVisible()
148+
// await expect(
149+
// page
150+
// .getByTestId('eventsTable')
151+
// .locator('tbody tr')
152+
// .first()
153+
// .getByTestId('amount')
154+
// .getByText(`-${amountWithFeeString}`)
155+
// ).toBeVisible()
156+
})

0 commit comments

Comments
 (0)