Skip to content

Commit

Permalink
Merge pull request #2332 from dusk-network/feature-2303
Browse files Browse the repository at this point in the history
web-wallet: fix rounding errors in migration amount input
  • Loading branch information
deuch13 authored Sep 11, 2024
2 parents 888aaf0 + 3413f8f commit ddf8f00
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 41 deletions.
2 changes: 2 additions & 0 deletions web-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix Receive tab content overflows [#1901]
- Add missing "Soehne Mono" and its `@font-face` definition [#2071]
- The sync promise should be set to `null` after aborting a sync [#2118]
- Fix rounding errors in migration amount input [#2303]

## [0.5.0] - 2024-03-27

Expand Down Expand Up @@ -251,6 +252,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#2118]: https://github.com/dusk-network/rusk/issues/2175
[#2196]: https://github.com/dusk-network/rusk/issues/2196
[#2014]: https://github.com/dusk-network/rusk/issues/2014
[#2303]: https://github.com/dusk-network/rusk/issues/2303

<!-- VERSIONS -->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { allowance, approve } from "$lib/migration/migration";
import { createDataStore } from "$lib/dusk/svelte-stores";
/** @type {number} */
/** @type {bigint} */
export let amount;
/** @type {HexString} */
Expand Down Expand Up @@ -41,7 +41,7 @@
chainContract,
migrationContract
);
return Number(allowedAmount) >= amount;
return allowedAmount >= amount;
} catch (e) {
return false;
}
Expand All @@ -53,11 +53,7 @@
hasApprovedCoin = await checkAllowance();
if (!hasApprovedCoin) {
const txHash = await approve(
migrationContract,
chainContract,
amount.toString()
);
const txHash = await approve(migrationContract, chainContract, amount);
if (isHex(txHash)) {
dispatch("incrementStep");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { migrate } from "$lib/migration/migration";
import { createDataStore } from "$lib/dusk/svelte-stores";
/** @type {number} */
/** @type {bigint} */
export let amount;
/** @type {string} */
Expand All @@ -33,12 +33,7 @@
/** @param {number} id - the chain id of the selected smart contract */
async function handleMigration(id) {
const txHash = await migrate(
amount.toString(),
id,
currentAddress,
migrationContract
);
const txHash = await migrate(amount, id, currentAddress, migrationContract);
if (isHex(txHash)) {
migrationHash = txHash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
<script>
import { mdiArrowLeft, mdiArrowRight, mdiWalletOutline } from "@mdi/js";
import { getAccount, switchChain } from "@wagmi/core";
import { formatUnits } from "viem";
import { formatUnits, parseUnits } from "viem";
import { onDestroy, onMount } from "svelte";
import { tokens } from "./tokenConfig";
import { calculateAdaptiveCharCount, middleEllipsis } from "$lib/dusk/string";
import { getDecimalSeparator } from "$lib/dusk/number";
import {
calculateAdaptiveCharCount,
cleanNumberString,
middleEllipsis,
} from "$lib/dusk/string";
import {
AppAnchor,
AppAnchorButton,
Expand Down Expand Up @@ -41,12 +46,11 @@
const options = ["ERC-20", "BEP-20"];
const minAmount = 1n ** 9n;
// The minimum allowed amount to be migrated expressed as a string
const minAmount = "0.000000000000000001";
const ercDecimals = 18;
const decimalMultiplier = 10n ** BigInt(ercDecimals);
/** @type {TokenNames} */
let selectedChain = erc20.name;
Expand All @@ -56,8 +60,8 @@
/** @type {bigint} */
let connectedWalletBalance;
/** @type {number} */
let amount;
/** @type {string} */
let amount = "";
/** @type {HTMLInputElement | null} */
let amountInput;
Expand All @@ -78,8 +82,10 @@
$: ({ currentAddress } = $walletStore);
$: isAmountValid =
!!amount &&
BigInt(amount) * decimalMultiplier >= minAmount &&
BigInt(amount) * decimalMultiplier <= connectedWalletBalance;
parseUnits(amount.replace(",", "."), ercDecimals) >=
parseUnits(minAmount, ercDecimals) &&
parseUnits(amount.replace(",", "."), ercDecimals) <= connectedWalletBalance;
$: amount = cleanNumberString(amount, getDecimalSeparator());
/**
* Triggers the switchChain event and reverts the ExclusiveChoice UI selected option if an error is thrown
Expand Down Expand Up @@ -252,8 +258,7 @@
ercDecimals
);
}
amount = Number(formatUnits(connectedWalletBalance, ercDecimals));
amount = formatUnits(connectedWalletBalance, ercDecimals);
}}
text="USE MAX"
disabled={isInputDisabled}
Expand All @@ -264,10 +269,7 @@
className="migrate__input-field"
bind:value={amount}
required
type="number"
min={Number(formatUnits(minAmount, ercDecimals))}
max={Number(formatUnits(connectedWalletBalance, ercDecimals))}
step={Number(formatUnits(minAmount, ercDecimals))}
type="text"
placeholder="Amount"
disabled={isInputDisabled}
/>
Expand All @@ -287,13 +289,13 @@
on:errorApproval={() => {
isInputDisabled = false;
}}
{amount}
amount={parseUnits(amount.replace(",", "."), ercDecimals)}
chainContract={tokens[network][selectedChain].contract}
migrationContract={tokens[network][selectedChain].migrationContract}
/>
{:else}
<ExecuteMigration
{amount}
amount={parseUnits(amount.replace(",", "."), ercDecimals)}
{currentAddress}
migrationContract={tokens[network][selectedChain].migrationContract}
/>
Expand Down
8 changes: 8 additions & 0 deletions web-wallet/src/lib/dusk/number/getDecimalSeparator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @returns {string}
*/
const getDecimalSeparator = () => {
return (0.1).toLocaleString().slice(1, 2);
};

export default getDecimalSeparator;
1 change: 1 addition & 0 deletions web-wallet/src/lib/dusk/number/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as createNumberFormatter } from "./createNumberFormatter";
export { default as getDecimalSeparator } from "./getDecimalSeparator";
28 changes: 28 additions & 0 deletions web-wallet/src/lib/dusk/string/__tests__/cleanNumberString.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { cleanNumberString } from "../";

describe("calculateAdaptiveCharCount", () => {
it("should return a valid string represantation of a number when the decimal separator is `,`", () => {
expect(cleanNumberString("12153,566,68468,,,351", ",")).toBe(
"12153,56668468351"
);
});

it("should return a valid string represantation of a number when the decimal separator is `.`", () => {
expect(cleanNumberString("100.00..549..6.548", ".")).toBe("100.005496548");
});

it("should return an empty string if an empty string is passed", () => {
expect(cleanNumberString("", ".")).toBe("");
});

it("should return an empty string if a string with non valid characters is passed", () => {
expect(cleanNumberString("asdsasd/*-,/?!@#$%^&*()_=+", ".")).toBe("");
});

it("should return a valid string represantation of a number if a string containing non valid characters is passed", () => {
expect(
cleanNumberString("1321651.0518asds592asd/*-,/?!@#$%^&*()_=+", ".")
).toBe("1321651.0518592");
});
});
16 changes: 16 additions & 0 deletions web-wallet/src/lib/dusk/string/cleanNumberString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Cleans the input string
* Returns a valid number in string form with the correct decimal separator according to the locale
*
* @param {string} amount
* @param {string} separator
* @returns {string}
*/
const cleanNumberString = (amount, separator) => {
const regex = new RegExp(`[^\\d${separator}]+`, "g"); // Remove any character that are not digits or the decimal separator
const regex2 = new RegExp(`(?<=\\${separator}.*)\\${separator}`, "g"); // Remove all but the first decimal separator

return amount.replace(regex, "").replace(regex2, "");
};

export default cleanNumberString;
1 change: 1 addition & 0 deletions web-wallet/src/lib/dusk/string/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as calculateAdaptiveCharCount } from "./calculateAdaptiveCharCount";
export { default as cleanNumberString } from "./cleanNumberString";
export { default as hexStringToBytes } from "./hexStringToBytes";
export { default as makeClassName } from "./makeClassName";
export { default as middleEllipsis } from "./middleEllipsis";
Expand Down
16 changes: 6 additions & 10 deletions web-wallet/src/lib/migration/migration.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { readContract, simulateContract, writeContract } from "@wagmi/core";
import { formatUnits, parseUnits } from "viem";
import ERC20Abi from "./abi/erc_bep_20.json";
import migrationABI from "./abi/migrationABI.json";
import { wagmiConfig } from "./walletConnection";

const stableCoinDecimals = 18;

/**
* Retrieves the allowance amount for a stable coin for a given spender and account.
*
* @param {HexString} userAddress - The address of the user to check if it gave an allowance to the migration contract.
* @param {HexString} stableCoinAddress - contract address for the coin.
* @param {HexString} migrationContract - migration contract address for the the coin.
* @returns {Promise<string>} - A promise that resolves to the allowed amount.
* @returns {Promise<bigint>} - A promise that resolves to the allowed amount.
* @throws {Error} - Throws an error if there is an issue with retrieving the allowance.
*/
export const allowance = async (
Expand All @@ -29,7 +26,7 @@ export const allowance = async (
functionName: "allowance",
})
);
return formatUnits(balance, stableCoinDecimals);
return balance;
} catch (e) {
const errorMessage =
e instanceof Error
Expand All @@ -42,19 +39,18 @@ export const allowance = async (
/**
* Approves a spender to transfer a specified amount of stable coin on behalf of the caller.
*
* @param {string} value - The amount of stable coin to approve.
* @param {bigint} value - The amount of stable coin to approve.
* @param {HexString} stableCoinAddress - contract address for the coin.
* @param {HexString} migrationContract - migration contract address for the the coin.
* @returns {Promise<string>} - Transaction hash.
* @throws {Error} - Throws an error if there is an issue with the approval transaction.
*/
export const approve = async (migrationContract, stableCoinAddress, value) => {
try {
const convertedValue = parseUnits(value, stableCoinDecimals);
return await writeContract(wagmiConfig, {
abi: ERC20Abi,
address: stableCoinAddress,
args: [migrationContract, convertedValue],
args: [migrationContract, value],
functionName: "approve",
});
} catch (e) {
Expand Down Expand Up @@ -97,7 +93,7 @@ export const getBalanceOfCoin = async (userAddress, stableCoinAddress) => {
/**
* Migrates the approved amount to the given account
*
* @param {string} amount - the amount to be migrated
* @param {bigint} amount - the amount to be migrated
* @param {number} chainId - the id of the smart contract
* @param {string} mainnetDuskAddress - the wallet address where tokens should be migrated to
* @param {HexString} migrationContract - the migration contract address
Expand All @@ -112,7 +108,7 @@ export const migrate = async (
const { request } = await simulateContract(wagmiConfig, {
abi: migrationABI,
address: migrationContract,
args: [parseUnits(amount, 18), mainnetDuskAddress],
args: [amount, mainnetDuskAddress],
chainId: chainId,
functionName: "migrate",
});
Expand Down

0 comments on commit ddf8f00

Please sign in to comment.