Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased (develop)

- added: Zcash: Orchard -> Ironwood (NU6.3) migration card on the wallet scene - when the engine reports a sweep is worthwhile, it prefills a locked max send-to-self through the ordinary send scene (recommended-tone: funds stay spendable either way). iOS only until the Android SDK ships Ironwood.

## 4.50.0 (staging)

- added: Changelly swap provider
Expand Down
19 changes: 10 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"detect-bundler": "^1.1.0",
"disklet": "^0.6.0",
"edge-core-js": "^2.47.0",
"edge-currency-accountbased": "^4.86.0",
"edge-currency-accountbased": "https://github.com/EdgeApp/react-native-zcash/releases/download/ironwood-ffi-6e29ab42/edge-currency-accountbased-4.86.1-ironwood-ac7aa5d5.tgz",
"edge-currency-plugins": "^3.11.0",
"edge-exchange-plugins": "^2.51.0",
"edge-info-server": "^3.12.0",
Expand Down Expand Up @@ -170,7 +170,7 @@
"react-native-wheel-picker-android": "^2.0.6",
"react-native-worklets": "^0.6.1",
"react-native-zano": "^0.3.0",
"react-native-zcash": "^0.12.2",
"react-native-zcash": "https://github.com/EdgeApp/react-native-zcash/releases/download/ironwood-ffi-6e29ab42/react-native-zcash-0.12.1-ironwood-91f50eb.tgz",
"react-redux": "^8.1.1",
"redux": "^4.2.1",
"redux-thunk": "^2.3.0",
Expand Down
71 changes: 71 additions & 0 deletions src/__tests__/zcashMigration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from '@jest/globals'
import type { EdgeCurrencyWallet } from 'edge-core-js'

import { getZcashMigrationStatus } from '../util/zcashMigration'

Comment on lines +1 to +5
const goodStatus = {
state: 'required',
completedTransfers: 0,
totalTransfers: 0,
remainingOrchardZatoshi: '123',
hasOverdueTransfers: false,
isSynced: true
}

const makeFakeWallet = (opts: {
pluginId: string
otherMethods?: object
}): EdgeCurrencyWallet =>
({
currencyInfo: { pluginId: opts.pluginId },
otherMethods: opts.otherMethods ?? {}
} as any)

describe('zcashMigration util', () => {
it('returns status for a migration-capable zcash wallet', async () => {
const wallet = makeFakeWallet({
pluginId: 'zcash',
otherMethods: {
getMigrationStatus: async () => goodStatus
}
})
const status = await getZcashMigrationStatus(wallet)
expect(status?.state).toBe('required')
expect(status?.remainingOrchardZatoshi).toBe('123')
})

it('returns undefined for non-zcash wallets', async () => {
const wallet = makeFakeWallet({
pluginId: 'bitcoin',
otherMethods: { getMigrationStatus: async () => goodStatus }
})
expect(await getZcashMigrationStatus(wallet)).toBeUndefined()
})

it('returns undefined when the engine lacks the method (old accountbased)', async () => {
const wallet = makeFakeWallet({ pluginId: 'zcash' })
expect(await getZcashMigrationStatus(wallet)).toBeUndefined()
})

it('returns undefined when the engine call throws', async () => {
const wallet = makeFakeWallet({
pluginId: 'zcash',
otherMethods: {
getMigrationStatus: async () => {
throw new Error('engine broke')
}
}
})
expect(await getZcashMigrationStatus(wallet)).toBeUndefined()
})

it('returns undefined on malformed status shapes', async () => {
const wallet = makeFakeWallet({
pluginId: 'zcash',
otherMethods: {
getMigrationStatus: async () => ({ state: 'bogus' })
}
})
expect(await getZcashMigrationStatus(wallet)).toBeUndefined()
})
})
101 changes: 99 additions & 2 deletions src/components/themed/TransactionListTop.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { add, gt, mul } from 'biggystring'
import type { EdgeCurrencyWallet, EdgeTokenId } from 'edge-core-js'
import { add, div, gt, mul } from 'biggystring'
import type {
EdgeCurrencyWallet,
EdgeSpendInfo,
EdgeTokenId
} from 'edge-core-js'
import * as React from 'react'
import { View } from 'react-native'
import type { AirshipBridge } from 'react-native-airship'
Expand All @@ -24,6 +28,7 @@ import { useAsyncValue } from '../../hooks/useAsyncValue'
import { useHandler } from '../../hooks/useHandler'
import { useWalletName } from '../../hooks/useWalletName'
import { useWatch } from '../../hooks/useWatch'
import { useZcashMigrationStatus } from '../../hooks/useZcashMigrationStatus'
import { formatNumber, toPercentString } from '../../locales/intl'
import { lstrings } from '../../locales/strings'
import { getStakePlugins } from '../../plugins/stake-plugins/stakePlugins'
Expand Down Expand Up @@ -156,6 +161,7 @@ export const TransactionListTop: React.FC<Props> = props => {
const walletName = useWalletName(wallet)
const balanceMap = useWatch(wallet, 'balanceMap')
const syncStatus = useWatch(wallet, 'syncStatus')
const migrationStatus = useZcashMigrationStatus(wallet)

// Track sync card visibility with 1-second delay after sync completes:
const isSyncing = syncStatus.totalRatio < DONE_THRESHOLD
Expand Down Expand Up @@ -665,6 +671,96 @@ export const TransactionListTop: React.FC<Props> = props => {
)
}

/**
* Orchard -> Ironwood migration card (Zcash only; appears when the engine
* reports a sweep is worthwhile). Tapping prefills a locked max
* send-to-self in the ordinary send scene — post-activation, plain
* proposals route outputs and change into Ironwood, so no special
* transaction type exists. Recommended, not mandatory: Orchard stays
* spendable and drains passively through ordinary spends, and the card
* clears via the status poll once Orchard empties by any means.
*/
function renderZcashMigrationCard(): React.ReactElement | null {
if (tokenId != null || migrationStatus == null) return null
if (migrationStatus.state !== 'required') return null

// ZIP 318 requires the entry point to display the Orchard-pool-specific
// balance at risk — not the wallet's whole shielded balance, since only
// the Orchard funds cross the turnstile.
const orchardDisplayAmount = formatNumber(
div(
migrationStatus.remainingOrchardZatoshi,
displayDenomination.multiplier,
DECIMAL_PRECISION
),
{ toFixed: 6 }
)
const orchardBalanceText = `${orchardDisplayAmount} ${displayDenomination.name}`

const handleMigratePress = async (): Promise<void> => {
const addresses = await wallet.getAddresses({ tokenId: null })
const unifiedAddress = addresses.find(
address => address.addressType === 'unifiedAddress'
)
if (unifiedAddress == null) return
const spendInfo: EdgeSpendInfo = {
Comment on lines +702 to +706
tokenId: null,
spendTargets: [{ publicAddress: unifiedAddress.publicAddress }],
metadata: { notes: lstrings.zcash_migration_tx_notes },
// Top-level otherParams reaches the engine intact (per-target
// otherParams gets overwritten by the send scene's address handlers).
// The flag makes the engine quote and build an Orchard-ONLY sweep
// instead of an ordinary max send across every shielded pool.
otherParams: { ironwoodMigration: true }
}
// The locked amount tile makes the MAX modal unreachable, so prefill the
// engine's quote and lock it. For a migration spend that quote is the
// Orchard balance minus fee, and re-proposing is deterministic, so the
// amount shown here is the amount the user signs.
const migrationAmount = await wallet.getMaxSpendable(spendInfo)
navigation.push('send2', {
walletId: wallet.id,
tokenId: null,
spendInfo: {
...spendInfo,
spendTargets: [
{
publicAddress: unifiedAddress.publicAddress,
nativeAmount: migrationAmount
}
]
},
lockTilesMap: { address: true, amount: true, wallet: true },
infoTiles: [
{
label: lstrings.zcash_migration_info_tile_label,
value: lstrings.zcash_migration_info_tile_value
}
]
})
}

return (
<AlertCardUi4
body={sprintf(
lstrings.zcash_migration_recommended_body_1s,
orchardBalanceText
)}
marginRem={[0.5, 0.5, 0, 0.5]}
title={lstrings.zcash_migration_recommended_title}
type="warning"
button={{
label: lstrings.zcash_migration_recommended_button,
onPress: () => {
handleMigratePress().catch((error: unknown) => {
showError(error)
})
}
}}
/>
)
}

function renderButtons(): React.ReactElement {
const styles = getStyles(theme)
const hideStaking = !isStakingAvailable
Expand Down Expand Up @@ -795,6 +891,7 @@ export const TransactionListTop: React.FC<Props> = props => {
{!isStakingAvailable ? null : renderStakedBalance()}
</EdgeCard>
{renderSyncStatus()}
{renderZcashMigrationCard()}
{renderButtons()}
</>
)}
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/useZcashMigrationStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { EdgeCurrencyWallet } from 'edge-core-js'
import * as React from 'react'

import {
getZcashMigrationStatus,
type ZcashMigrationStatus
} from '../util/zcashMigration'
import { useRefresher } from './useRefresher'

/**
* Polls the Zcash Orchard -> Ironwood migration status for a wallet.
* Returns undefined (forever) for anything that is not a migration-capable
* Zcash wallet, so it is safe to call unconditionally from shared scenes.
*/
export function useZcashMigrationStatus(
wallet: EdgeCurrencyWallet,
delay: number = 10000
): ZcashMigrationStatus | undefined {
const refresher = React.useCallback(
async () => await getZcashMigrationStatus(wallet),
[wallet]
)
return useRefresher(refresher, undefined, delay)
}
15 changes: 15 additions & 0 deletions src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,21 @@ const strings = {
string_first_pivx_wallet_name: 'My PIVX',
string_first_zano_wallet_name: 'My Zano',
string_first_zcash_wallet_name: 'My Zcash',

// Zcash Orchard -> Ironwood (NU6.3) migration (v1: recommended sweep via
// the ordinary send scene — not framed as mandatory, because Orchard stays
// spendable post-fork and drains passively through ordinary spends).
// Per ZIP 318 the entry point states the Orchard-pool balance at risk, and
// per ZIP 315 it discloses that the migrated amount becomes public before
// any funds leave the pool.
zcash_migration_recommended_title: 'Move your Orchard funds to Ironwood',
zcash_migration_recommended_body_1s:
"Zcash's Ironwood network upgrade added a new shielded pool, and %s of your ZEC is still in the older Orchard pool. We recommend moving it with a single send to yourself. The amount you move will be publicly visible on the blockchain. Your funds stay safe and spendable either way.",
Comment on lines +775 to +776
zcash_migration_recommended_button: 'Move my funds',
zcash_migration_info_tile_label: 'Ironwood migration',
zcash_migration_info_tile_value:
'This send moves your Orchard-pool ZEC to your own address in the new Ironwood pool, leaving your other funds untouched. The amount is your Orchard balance minus the network fee, and it will be publicly visible on the blockchain.',
zcash_migration_tx_notes: 'Ironwood migration',
string_first_tron_wallet_name: 'My Tron',
string_first_doge_wallet_name: 'My Doge',
string_first_fantom_wallet_name: 'My Fantom',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/strings/enUS.json
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,12 @@
"string_first_pivx_wallet_name": "My PIVX",
"string_first_zano_wallet_name": "My Zano",
"string_first_zcash_wallet_name": "My Zcash",
"zcash_migration_recommended_title": "Move your Orchard funds to Ironwood",
"zcash_migration_recommended_body_1s": "Zcash's Ironwood network upgrade added a new shielded pool, and %s of your ZEC is still in the older Orchard pool. We recommend moving it with a single send to yourself. The amount you move will be publicly visible on the blockchain. Your funds stay safe and spendable either way.",
"zcash_migration_recommended_button": "Move my funds",
"zcash_migration_info_tile_label": "Ironwood migration",
"zcash_migration_info_tile_value": "This send moves your Orchard-pool ZEC to your own address in the new Ironwood pool, leaving your other funds untouched. The amount is your Orchard balance minus the network fee, and it will be publicly visible on the blockchain.",
"zcash_migration_tx_notes": "Ironwood migration",
"string_first_tron_wallet_name": "My Tron",
"string_first_doge_wallet_name": "My Doge",
"string_first_fantom_wallet_name": "My Fantom",
Expand Down
Loading