Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- changed: Upgrade react-native-worklets to 0.10.4, which stops the animation loop from running while the app is in the background on Android and fixes crashes when the JavaScript runtime shuts down.
- changed: Upgrade react-native-reanimated to 4.5.5, fixing a crash when an event arrives for a view that has just unmounted, an iOS crash at launch, and scroll-driven animations on Android jumping back a frame.
- changed: Rename the HoudiniSwap provider to Houdini and link its privacy pages on docs.houdini.com
- changed: Send scene tests cover the transparent Zcash payout warning on a pasted unified address
- fixed: Swaps with no exact receive quote switch to a send amount of the same value with a toast, not a no-provider error
- fixed: The side menu's dimming overlay works again on both platforms. React Native 0.86 removed the `StyleSheet.absoluteFillObject` the drawer's overlay spreads, which collapsed it to nothing, so tapping outside the menu no longer closed it.
- fixed: Text inputs mounted in the disabled state (such as the spending-limit amount) no longer flash their enabled look before dimming when a scene appears.
Expand Down
127 changes: 109 additions & 18 deletions src/__tests__/scenes/SendScene2.recipientNetwork.ui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { EdgeRow } from '../../components/rows/EdgeRow'
import { SendScene2 } from '../../components/scenes/SendScene2'
import type * as AirshipInstance from '../../components/services/AirshipInstance'
import { Airship, showToast } from '../../components/services/AirshipInstance'
import { SafeSlider } from '../../components/themed/SafeSlider'
import { StealthInfoText } from '../../components/themed/StealthInfoText'
import { AddressTile2 } from '../../components/tiles/AddressTile2'
import { lstrings } from '../../locales/strings'
Expand Down Expand Up @@ -61,6 +62,13 @@ jest.mock('../../components/services/AirshipInstance', () => {
return { ...actual, showToast: jest.fn() }
})

// A same-chain Zcash pick falls through to a ZNS reverse lookup over the
// network:
jest.mock('../../util/nameServices', () => ({
...jest.requireActual<object>('../../util/nameServices'),
reverseLookupName: async () => null
}))

const BTC_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'
const EVM_ADDRESS = '0xF0825Aec2c79189C6bB1FEe9293F9478103c9B9e'
const OTHER_EVM_ADDRESS = '0x52908400098527886E0F7030069857D2E4169EE7'
Expand Down Expand Up @@ -117,6 +125,11 @@ const FIFTY_USD_OF_ETH = '37700000000000000'
const SCAM_WARNING = 'Scam Warning'
const SWAP_BEFORE_SEND = 'Swap before send'
const ZCASH_TRANSPARENT_WARNING = 'Transparent Zcash payout'
// A ZIP-316 unified address with a P2PKH receiver, which the provider pays
// out to as a t1:
const ZCASH_UNIFIED_ADDRESS =
'u1pg2aaph7jp8rpf6yhsza25722sg5fcn3vaca6ze27hqjw7jvvhhuxkpcg0ge9xh6drsgdkda8qjq5chpehkcpxf87rnjryjqwymdheptpvnljqqrjqzjwkc2ma6hcq666kgwfytxwac8eyex6ndgr6ezte66706e3vaqrd25dzvzkc69kw0jgywtd0cmq52q5lkw6uh7hyvzjse8ksx'
const ZCASH_TRANSPARENT_ADDRESS = 't1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs'

/**
* A fake plugin whose parser accepts one address format, bare or inside a
Expand Down Expand Up @@ -199,16 +212,37 @@ const ftmCurrencyInfo: EdgeCurrencyInfo = {
/**
* The swap provider every send-to-address quote goes to. It records the
* destination addresses each request reached it with, then declines the pair
* as a provider with no route would.
* as a provider with no route would. With `fakeHoudiniQuotes.enabled` set it
* quotes the pair one to one instead, so the scene holds a live quote.
*/
const quotedDestinations: EdgeAddress[][] = []
const fakeHoudiniQuotes = { enabled: false }
const fakeHoudiniPlugin: EdgeSwapPlugin = {
swapInfo: { pluginId: 'houdini', displayName: 'Houdini', supportEmail: '' },
async fetchSwapQuote(request) {
quotedDestinations.push(
await request.toWallet.getAddresses({ tokenId: null })
)
throw new SwapCurrencyError(fakeHoudiniPlugin.swapInfo, request)
if (!fakeHoudiniQuotes.enabled) {
throw new SwapCurrencyError(fakeHoudiniPlugin.swapInfo, request)
}
return {
swapInfo: fakeHoudiniPlugin.swapInfo,
request,
isEstimate: false,
fromNativeAmount: request.nativeAmount,
toNativeAmount: request.nativeAmount,
networkFee: {
currencyCode: request.fromWallet.currencyInfo.currencyCode,
tokenId: null,
nativeAmount: '0'
},
pluginId: fakeHoudiniPlugin.swapInfo.pluginId,
approve: async () => {
throw new Error('The fake provider sends nothing')
},
close: async () => {}
}
}
}

Expand Down Expand Up @@ -369,11 +403,27 @@ afterAll(async () => {
await context?.close()
})

/**
* Waits in act scopes, for up to four seconds, until `done` holds. A quote or
* a wallet pick settles on its own promise chain, and only an act scope that
* closes over it delivers it. waitFor polls outside act, so an update landing
* between its polls never renders.
*/
async function settleInAct(done: () => boolean): Promise<void> {
const deadline = Date.now() + 4000
while (!done() && Date.now() < deadline) {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 20))
})
}
}

describe('SendScene2 recipient network', () => {
const mockShowToast = jest.mocked(showToast)

beforeEach(() => {
mockShowToast.mockClear()
fakeHoudiniQuotes.enabled = false
})

interface RenderedSend {
Expand Down Expand Up @@ -422,6 +472,8 @@ describe('SendScene2 recipient network', () => {
position: (text: string) => number
/** Whether some text node on the scene reads exactly this. */
shows: (text: string) => boolean
/** Whether the slider is shown and ready to slide. */
sliderLive: () => boolean
/** The notes under the Stealth Send toggle, top to bottom. */
stealthNotes: () => string[]
/** Taps the Stealth Send toggle. */
Expand Down Expand Up @@ -585,13 +637,7 @@ describe('SendScene2 recipient network', () => {
.UNSAFE_getByProps({ testID: 'addressTileMyself' })
.props.onPress()
})
// The tile settles the pick on its own promise chain, whose updates
// only reach the scene once an act scope closes over them:
for (let i = 0; i < 50 && tileProps().recipientAddress === ''; ++i) {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 20))
})
}
await settleInAct(() => tileProps().recipientAddress !== '')
Comment thread
j0ntz marked this conversation as resolved.
Resolved
expect(tileProps().recipientAddress).not.toBe('')
show.mockRestore()
},
Expand Down Expand Up @@ -679,6 +725,10 @@ describe('SendScene2 recipient network', () => {
1,
shows: text =>
JSON.stringify(rendered.toJSON()).includes(JSON.stringify([text])),
sliderLive: () =>
rendered
.UNSAFE_queryAllByType(SafeSlider)
.some(slider => slider.props.disabled !== true),
stealthNotes: () =>
rendered
.UNSAFE_queryAllByType(StealthInfoText)
Expand Down Expand Up @@ -1159,15 +1209,7 @@ describe('SendScene2 recipient network', () => {
// The fake provider refuses every quote. The refusal settles on the
// quote's own promise chain, so only a closing act scope delivers it:
await scene.enterSendAmount(ONE_BTC)
for (
let i = 0;
i < 50 && !scene.shows(lstrings.exchange_generic_error_title);
++i
) {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 20))
})
}
await settleInAct(() => scene.shows(lstrings.exchange_generic_error_title))
expect(scene.shows(lstrings.exchange_generic_error_title)).toBe(true)
expect(scene.shows(ZCASH_TRANSPARENT_WARNING)).toBe(false)
scene.unmount()
Expand All @@ -1188,6 +1230,55 @@ describe('SendScene2 recipient network', () => {
scene.unmount()
})

describe('with a pasted Zcash address the provider quotes', () => {
/**
* Pastes a Zcash address on a Bitcoin send, enters an amount, and waits
* until the provider's quote for that address leaves the slider ready.
*/
const renderQuotedPaste = async (
address: string,
stealth: boolean
): Promise<RenderedSend> => {
fakeHoudiniQuotes.enabled = true
const scene = renderBitcoinSend()
await scene.pickRecipientAsset('Zcash')
expect(await scene.enterCrossChainAddress(address)).toBe(true)
if (stealth) await scene.toggleStealth()
expect(scene.stealthArmed()).toBe(stealth)

quotedDestinations.length = 0
await scene.enterSendAmount(ONE_BTC)
await settleInAct(scene.sliderLive)
expect(scene.sliderLive()).toBe(true)
expect(quotedDestinations.at(-1)).toEqual([
{ addressType: 'publicAddress', publicAddress: address }
])
expect(scene.shows(lstrings.exchange_generic_error_title)).toBe(false)
return scene
}

it('warns before the slide that a Stealth u1 send pays a t1', async () => {
const scene = await renderQuotedPaste(ZCASH_UNIFIED_ADDRESS, true)
expect(scene.shows(ZCASH_TRANSPARENT_WARNING)).toBe(true)
expect(scene.shows(lstrings.stealth_zcash_transparent_payout_body)).toBe(
true
)
scene.unmount()
})

it('shows no warning for a Stealth send to a pasted t1', async () => {
const scene = await renderQuotedPaste(ZCASH_TRANSPARENT_ADDRESS, true)
expect(scene.shows(ZCASH_TRANSPARENT_WARNING)).toBe(false)
scene.unmount()
})

it('shows no warning for a u1 send with Stealth off', async () => {
const scene = await renderQuotedPaste(ZCASH_UNIFIED_ADDRESS, false)
expect(scene.shows(ZCASH_TRANSPARENT_WARNING)).toBe(false)
scene.unmount()
})
})

it('shows the scam warning on a plain send with no address', async () => {
const scene = renderBitcoinSend()

Expand Down
Loading