Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow l2 withdrawal (funds stuck in l2 that didnt reach evm) #7734

Merged
merged 17 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
8 changes: 8 additions & 0 deletions packages/desktop/components/modals/AccountActionsMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
modal?.close()
}

function onWithdrawFromL2Click(): void {
openPopup({ id: PopupId.WithdrawFromL2 })
modal?.close()
}

function onVerifyAddressClick(): void {
const ADDRESS_INDEX = 0
checkOrConnectLedger(() => {
Expand Down Expand Up @@ -79,6 +84,9 @@
onClick={onViewAddressHistoryClick}
/>
{/if}
{#if $activeProfile?.network?.id === NetworkId.Shimmer || $activeProfile?.network?.id === NetworkId.Testnet}
<MenuItem icon={Icon.Transfer} title={localize('actions.withdrawFromL2')} onClick={onWithdrawFromL2Click} />
{/if}
<MenuItem icon={Icon.Customize} title={localize('actions.customizeAcount')} onClick={onCustomiseAccountClick} />
{#if $isActiveLedgerProfile}
<MenuItem
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/components/popups/Popup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import VestingCollectPopup from './VestingCollectPopup.svelte'
import PayoutDetailsPopup from './PayoutDetailsPopup.svelte'
import VestingRewardsFinderPopup from './VestingRewardsFinderPopup.svelte'
import WithdrawFromL2Popup from './WithdrawFromL2Popup.svelte'

export let id: PopupId
export let props: any
Expand Down Expand Up @@ -144,6 +145,7 @@
[PopupId.VestingCollect]: VestingCollectPopup,
[PopupId.PayoutDetails]: PayoutDetailsPopup,
[PopupId.VestingRewardsFinder]: VestingRewardsFinderPopup,
[PopupId.WithdrawFromL2]: WithdrawFromL2Popup,
}

function onKey(event: KeyboardEvent): void {
Expand Down
161 changes: 161 additions & 0 deletions packages/desktop/components/popups/WithdrawFromL2Popup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<script lang="ts">
import { PopupId, closePopup, openPopup } from '@auxiliary/popup'
import { getSelectedAccount } from '@core/account'
import { localize } from '@core/i18n'
import { getArchivedBaseTokens } from '@core/layer-2/helpers/getArchivedBaseTokens'
import { getBaseToken, getCoinType, activeProfile, isActiveLedgerProfile } from '@core/profile'
import { truncateString } from '@core/utils'
import { formatTokenAmountPrecise, getRequiredStorageDepositForMinimalBasicOutput } from '@core/wallet'
import { Button, FontWeight, KeyValueBox, Spinner, Text, TextType } from 'shared/components'
import { onMount } from 'svelte'
import { getLayer2WithdrawRequest } from '@core/layer-2/utils'
import { withdrawL2Funds } from '@core/layer-2/helpers/widthdrawL2Funds'
import { getL2ReceiptByRequestId } from '@core/layer-2/helpers/getL2ReceiptByRequestId'
import { showAppNotification } from '@auxiliary/notification'
import { Bip44 } from '@iota/sdk/out/types'
import { displayNotificationForLedgerProfile, ledgerNanoStatus } from '@core/ledger'
import { getEstimatedGasForOffLedgerRequest, getNonceForWithdrawRequest } from '@core/layer-2/helpers'

export let password: string
export let withdrawableAmount: number
export let nonce: string

const bip44Chain: Bip44 = {
coinType: Number(getCoinType()),
account: getSelectedAccount().index,
change: 0,
addressIndex: 0,
}

let address: string | undefined = undefined
let isWithdrawing = false
const { isStrongholdLocked } = $activeProfile

$: password && nonce && !$isStrongholdLocked && withdrawFromL2()

function onCancelClick(): void {
closePopup()
}

function onWithdrawFromL2Click(): void {
openUnlockStrongholdPopup()
}

async function withdrawFromL2(): Promise<void> {
if ($isActiveLedgerProfile && !$ledgerNanoStatus.connected) {
displayNotificationForLedgerProfile('warning')
return
}
isWithdrawing = true
let withdrawRequest = await getLayer2WithdrawRequest(password, withdrawableAmount.toString(), nonce, bip44Chain)
// get gas estimate for request with hardcoded amounts
const gasEstimatePayload = await getEstimatedGasForOffLedgerRequest(withdrawRequest.request)
const minRequiredStorageDeposit: number = await getRequiredStorageDepositForMinimalBasicOutput()
// The "+1" is due to an optimization in WASP nodes.
const gasEstimate = gasEstimatePayload.gasFeeCharged + 1
if (withdrawableAmount > Number(minRequiredStorageDeposit) + Number(gasEstimate)) {
// Create new withdraw request with correct gas budget
withdrawRequest = await getLayer2WithdrawRequest(
password,
(withdrawableAmount - gasEstimate).toString(),
nonce,
bip44Chain,
gasEstimate.toString()
)
} else {
showErrorNotification(localize('error.send.notEnoughBalance'))
return
}

try {
await withdrawL2Funds(withdrawRequest.request)
const receipt = await getL2ReceiptByRequestId(withdrawRequest.requestId)

isWithdrawing = false
if (receipt?.errorMessage) {
showErrorNotification(receipt?.errorMessage)
} else {
closePopup()
}
} catch (error) {
isWithdrawing = false
showErrorNotification(error)
}
}

function openUnlockStrongholdPopup(): void {
openPopup({
id: PopupId.UnlockStronghold,
props: {
onSuccess: (password: string) => {
openPopup({
id: PopupId.WithdrawFromL2,
props: {
password,
withdrawableAmount,
},
})
},
onCancelled: function () {
openPopup({
id: PopupId.WithdrawFromL2,
props: {
withdrawableAmount,
},
})
},
returnPassword: true,
subtitle: localize('popups.password.backup'),
},
})
}
function showErrorNotification(error): void {
if ($isActiveLedgerProfile) {
displayNotificationForLedgerProfile('error', true, true, error)
} else {
showAppNotification({
type: 'error',
message: error,
alert: true,
})
}
}

onMount(async () => {
address = getSelectedAccount().depositAddress
nonce = await getNonceForWithdrawRequest(address)
withdrawableAmount = await getArchivedBaseTokens(address)
})
</script>

<div class="flex flex-col space-y-6">
<Text type={TextType.h3} fontWeight={FontWeight.semibold} lineHeight="6">
{localize('popups.withdrawFromL2.title')}
</Text>
<Text fontSize="15" color="gray-700" classes="text-left">{localize('popups.withdrawFromL2.body')}</Text>
{#if address}
<KeyValueBox
classes="flex items-center w-full py-4"
keyText={truncateString(address, 15, 15)}
valueText={formatTokenAmountPrecise(withdrawableAmount, getBaseToken())}
/>
{:else}
<div class="flex items-center justify-center">
<Spinner />
</div>
{/if}
<div class="flex flex-row flex-nowrap w-full space-x-4 mt-6">
<Button classes="w-full" outline onClick={onCancelClick} disabled={isWithdrawing}>
{localize('actions.cancel')}
</Button>
<Button
classes="w-full"
onClick={onWithdrawFromL2Click}
disabled={!withdrawableAmount}
isBusy={isWithdrawing}
busyMessage={localize('popups.withdrawFromL2.withdrawing')}
>
{localize('popups.withdrawFromL2.withdraw')}
</Button>
</div>
</div>
5 changes: 5 additions & 0 deletions packages/desktop/electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ try {
async migrateDbChrysalisToStardust(path, pinCode) {
return IotaSdk.migrateDbChrysalisToStardust(path, pinCode)
},
async signEd25519(secretManagerOptions, message, chain) {
const secretManager = new IotaSdk.SecretManager(secretManagerOptions)
const signature = await secretManager.signEd25519(message, chain)
return signature
},
})
contextBridge.exposeInMainWorld('__ELECTRON__', ElectronApi)
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/lib/auxiliary/popup/enums/popup-id.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ export enum PopupId {
VestingCollect = 'vestingCollect',
PayoutDetails = 'payoutDetails',
VestingRewardsFinder = 'vestingRewardsFinder',
WithdrawFromL2 = 'withdrawFromL2',
}
1 change: 1 addition & 0 deletions packages/shared/lib/core/layer-2/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './isc-magic-contract-address.constant'
export * from './layer2-tokens-poll-interval.constant'
export * from './target-contracts.constant'
export * from './transfer-allowance.constant'
export * from './withdraw.constant'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WITHDRAW = 0x9dcc0f41
28 changes: 28 additions & 0 deletions packages/shared/lib/core/layer-2/helpers/getArchivedBaseTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { get } from 'svelte/store'
import { activeProfile } from '@core/profile'
import { DEFAULT_CHAIN_CONFIGURATIONS } from '@core/network'

interface ArchivedResponse {
baseTokens: number
}

export async function getArchivedBaseTokens(address: string): Promise<number> {
const defaultChainConfig = DEFAULT_CHAIN_CONFIGURATIONS[get(activeProfile)?.network?.id]
const URL = `${defaultChainConfig?.archiveEndpoint}/v1/chains/${defaultChainConfig?.aliasAddress}/core/accounts/account/${address}/balance`

try {
const archivedResponse: ArchivedResponse = await fetch(URL).then((response) => {
if (response.status >= 400) {
return response.json().then((err) => {
throw new Error(`Message: ${err.Message}, Error: ${err.Error}`)
})
}

return response.json()
})

return archivedResponse.baseTokens
} catch (_) {
return 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { get } from 'svelte/store'
import { activeProfile } from '@core/profile'
import { DEFAULT_CHAIN_CONFIGURATIONS } from '@core/network'
import BigInteger from 'big-integer'
import { HexEncodedString } from '@iota/sdk'

interface GasEstimatePayload {
gasBurned?: number
gasFeeCharged?: number
}

export async function getEstimatedGasForOffLedgerRequest(requestHex: HexEncodedString): Promise<GasEstimatePayload> {
const defaultChainConfig = DEFAULT_CHAIN_CONFIGURATIONS[get(activeProfile)?.network?.id]
const URL = `${defaultChainConfig?.archiveEndpoint}/v1/chains/${defaultChainConfig?.aliasAddress}/estimategas-offledger`

const requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestBytes: requestHex,
}),
}

try {
const response = await fetch(URL, requestOptions)

if (response.status >= 400) {
return response.json().then((err) => {
throw new Error(`Message: ${err.Message}, Error: ${err.Error}`)
})
}
if (response.status === 200) {
const data = await response.json()
const gasBurned = BigInteger(data.gasBurned as string).toJSNumber()
const gasFeeCharged = BigInteger(data.gasFeeCharged as string).toJSNumber()

return { gasBurned, gasFeeCharged }
}
} catch (error) {
console.error(error)
throw new Error(error.message)
}
return {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { get } from 'svelte/store'
import { activeProfile } from '@core/profile'
import { DEFAULT_CHAIN_CONFIGURATIONS } from '@core/network'

const TIMEOUT_SECONDS = 5
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function getL2ReceiptByRequestId(requestId: string): Promise<any> {
const defaultChainConfig = DEFAULT_CHAIN_CONFIGURATIONS[get(activeProfile)?.network?.id]
let URL = `${defaultChainConfig?.archiveEndpoint}/v1/chains/${defaultChainConfig?.aliasAddress}/requests/${requestId}/wait?`

const queryParams: {
[key: string]: number | boolean
} = {
timeoutSeconds: TIMEOUT_SECONDS,
waitForL1Confirmation: true,
}

const queryString = Object.keys(queryParams)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
.join('&')

URL += queryString

const requestOptions: RequestInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
accept: 'application/json',
},
}

try {
const response = await fetch(URL, requestOptions)

if (response.status >= 400) {
return response.json().then((err) => {
throw new Error(`Message: ${err.Message}, Error: ${err.Error}`)
})
}
return response.json()
} catch (error) {
console.error(error)
throw new Error(error.message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { get } from 'svelte/store'
import { activeProfile } from '@core/profile'
import { DEFAULT_CHAIN_CONFIGURATIONS } from '@core/network'

interface NonceResponse {
nonce: string
}

export async function getNonceForWithdrawRequest(address: string): Promise<string> {
const defaultChainConfig = DEFAULT_CHAIN_CONFIGURATIONS[get(activeProfile)?.network?.id]
const URL = `${defaultChainConfig?.archiveEndpoint}/v1/chains/${defaultChainConfig?.aliasAddress}/core/accounts/account/${address}/nonce`

try {
const nonceResponse: NonceResponse = await fetch(URL).then((response) => {
if (response.status >= 400) {
return response.json().then((err) => {
throw new Error(`Message: ${err.Message}, Error: ${err.Error}`)
})
}

return response.json()
})

return nonceResponse.nonce
} catch (error) {
console.error(error)
throw new Error(error.message)
}
}
5 changes: 5 additions & 0 deletions packages/shared/lib/core/layer-2/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export * from './encodeAddress'
export * from './encodeAssetAllowance'
export * from './encodeSmartContractParameters'
export * from './getArchivedBaseTokens'
export * from './getL2ReceiptByRequestId'
export * from './widthdrawL2Funds'
export * from './getNonceForWithdrawRequest'
export * from './getEstimatedGasForOffLedgerRequest'
Loading
Loading