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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@nimiq/core": "^2.21.0",
"@nimiq/electrum-client": "https://github.com/nimiq/electrum-client#build",
"@nimiq/fastspot-api": "^1.10.3",
"@nimiq/gasless-sdk": "file:../gas-abstraction/sdk",
"@nimiq/hub-api": "^1.15.0",
"@nimiq/iqons": "^1.5.2",
"@nimiq/libswap": "^1.5.2",
Expand Down
24 changes: 9 additions & 15 deletions src/components/layouts/Network.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref, computed } from '@vue/composition-api';
import { InfoCircleIcon, CircleSpinner } from '@nimiq/vue-components';
import { calculateFee as calculatePolygonFee } from '@/ethers';
import { isGaslessTransferConfigured, quoteGaslessFee } from '@/lib/usdc/Gasless';
import { estimateFees } from '@/lib/BitcoinTransactionUtils';
import { getElectrumClient } from '@/electrum';
import { useI18n } from '@/lib/useI18n';
Expand Down Expand Up @@ -165,20 +165,14 @@ export default defineComponent({
btcFee.value = estimateFees(2, 2, fees[12]);
}

if (config.polygon.enabled && stablecoin.value) {
switch (stablecoin.value) { // eslint-disable-line default-case
case CryptoCurrency.USDC:
polygonFee.value = (await calculatePolygonFee(
config.polygon.usdc.tokenContract,
'transferWithPermit',
)).fee.toNumber();
break;
case CryptoCurrency.USDT:
polygonFee.value = (await calculatePolygonFee(
config.polygon.usdt_bridged.tokenContract,
'transferWithApproval',
)).fee.toNumber();
break;
if (config.polygon.enabled && stablecoin.value && isGaslessTransferConfigured()) {
try {
const quote = await quoteGaslessFee(stablecoin.value === CryptoCurrency.USDC
? config.polygon.usdc.tokenContract
: config.polygon.usdt_bridged.tokenContract);
polygonFee.value = Number(quote.fee);
} catch (error) {
console.warn('Failed to fetch the gasless transfer fee', error); // eslint-disable-line no-console
}
}
}
Expand Down
8 changes: 3 additions & 5 deletions src/components/modals/MoonpayModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,14 @@ export default defineComponent({
throw new Error('Invalid network ID given by Moonpay');
}

const { tokenContract } = config.polygon.usdt_bridged;
const { tokenContract } = config.polygon.usdc;

if (properties.cryptoCurrency.contractAddress !== tokenContract.toLowerCase()) {
throw new Error('Invalid USDC contract address given by Moonpay');
}

const { accountUsdcBalance } = usePolygonAddressStore();
// TODO: Preselect a relay to be able to check balance against the fee as well
// The relay fee is checked against the balance when sending
if (accountUsdcBalance.value < value) {
throw new Error('Insufficient USDC balance');
}
Expand All @@ -298,7 +298,6 @@ export default defineComponent({
normalizedDepositAddress,
value,
'Moonpay',
// relay,
);

if (!tx) {
Expand Down Expand Up @@ -336,7 +335,7 @@ export default defineComponent({
}

const { accountUsdtBridgedBalance } = usePolygonAddressStore();
// TODO: Preselect a relay to be able to check balance against the fee as well
// The relay fee is checked against the balance when sending
if (accountUsdtBridgedBalance.value < value) {
throw new Error('Insufficient USDT balance');
}
Expand All @@ -346,7 +345,6 @@ export default defineComponent({
normalizedDepositAddress,
value,
'Moonpay',
// relay,
);

if (!tx) {
Expand Down
48 changes: 33 additions & 15 deletions src/components/modals/StablecoinSendModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,14 @@ import { computed, defineComponent, onBeforeUnmount, ref, Ref, watch } from '@vu
import { useRouter, RouteName } from '@/router';
import { useI18n } from '@/lib/useI18n';
import { nextTick } from '@/lib/nextTick';
import type { TransferRequest } from '@nimiq/gasless-sdk/core';
import { useConfig } from '../../composables/useConfig';
import { useWindowSize } from '../../composables/useWindowSize';
import { useFlaggedAddressCheck, FlaggedAddressInfo } from '../../composables/useFlaggedAddressCheck';
import { sendPolygonTransaction } from '../../hub';
import { loadEthersLibrary, calculateFee } from '../../ethers';
import { loadEthersLibrary } from '../../ethers';
import { CryptoCurrency, FiatCurrency, FIAT_CURRENCIES_OFFERED, ENV_MAIN } from '../../lib/Constants';
import type { RelayServerInfo } from '../../lib/usdc/OpenGSN';
import { quoteGaslessFee, GaslessFeeQuote, GaslessTransferError } from '../../lib/usdc/Gasless';
import {
isValidDomain as isValidUnstoppableDomain,
resolve as resolveUnstoppableDomain,
Expand Down Expand Up @@ -513,7 +514,11 @@ export default defineComponent({
const feeLoading = ref(true); // Only true for first fee loading, not for updates.
const feeError = ref<string>(null);

const relay = ref<RelayServerInfo | null>(null);
const feeQuote = ref<GaslessFeeQuote | null>(null);
/**
* A payment that failed. Retrying it must keep its nonce (a correction), so that at most one version executes.
*/
const failedPayment = ref<TransferRequest | null>(null);

// Use 2.00 USDC/T as safe fallback fee
const maxSendableAmount = computed(() => {
Expand Down Expand Up @@ -708,22 +713,33 @@ export default defineComponent({
statusTitle.value = $t('Sending Transaction') as string;
statusMessage.value = '';

const token = stablecoin.value === CryptoCurrency.USDC
? config.polygon.usdc.tokenContract
: config.polygon.usdt_bridged.tokenContract;
const recipient = recipientInfo.value!.address;
// A correction must pay the same token to the same recipient. Otherwise, it is a new payment.
const corrects = failedPayment.value
&& failedPayment.value.token.toLowerCase() === token.toLowerCase()
&& failedPayment.value.to.toLowerCase() === recipient.toLowerCase()
? failedPayment.value
: undefined;

try {
const tx = await sendPolygonTransaction(
stablecoin.value === CryptoCurrency.USDC
? config.polygon.usdc.tokenContract
: config.polygon.usdt_bridged.tokenContract,
recipientInfo.value!.address,
token,
recipient,
amount.value,
recipientInfo.value!.label,
relay.value || undefined,
feeQuote.value || undefined,
corrects,
);

if (!tx) {
statusScreenOpened.value = false;
return;
}

failedPayment.value = null;
saveRecipientLabel();

if (stablecoin.value === CryptoCurrency.USDC) {
Expand Down Expand Up @@ -752,6 +768,10 @@ export default defineComponent({
}, SUCCESS_REDIRECT_DELAY);
} catch (error: any) {
reportToSentry(error);
if (error instanceof GaslessTransferError) {
// Null if the payment's outcome is final
failedPayment.value = error.payment;
}

// Show error screen
statusState.value = State.WARNING;
Expand Down Expand Up @@ -820,20 +840,18 @@ export default defineComponent({
window.clearTimeout(feeUpdateTimeout); // Reset potentially existing update timeout.
feeUpdateTimeout = 0; // 0: timer is to be started after the initial update
try {
const method = stablecoin.value === CryptoCurrency.USDC ? 'transferWithPermit' : 'transferWithApproval';
const feeInformation = await calculateFee(
const quote = await quoteGaslessFee(
stablecoin.value === CryptoCurrency.USDC
? config.polygon.usdc.tokenContract
: config.polygon.usdt_bridged.tokenContract,
method,
relay.value || undefined,
);
fee.value = Math.max(feeInformation.fee.toNumber(), 0.01);
relay.value = feeInformation.relay;
fee.value = Number(quote.fee);
feeQuote.value = quote;
feeLoading.value = false;
feeError.value = null;
if (feeUpdateTimeout === 0) {
// Schedule next update in 20s if timer is still to be started and has not been started yet.
// Schedule next update in 20s if timer is still to be started and has not been started yet. The
// relay's quotes are valid for 120s.
feeUpdateTimeout = window.setTimeout(startFeeUpdates, 20e3);
}
} catch (e: unknown) {
Expand Down
28 changes: 21 additions & 7 deletions src/config/config.local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ export default {

polygon: {
enabled: false,
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
// Set to true to keep stablecoin swaps visible, but show a maintenance warning. Swaps are still relayed via
// OpenGSN.
isGasAbstractionUnderMaintenance: false,
networkId: 80002,
rpcEndpoint: 'wss://polygon-amoy.g.alchemy.com/v2/#ALCHEMY_API_KEY#',
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
isGaslessTransferUnderMaintenance: false,
networkId: 31337, // The local Anvil stack of the gas abstraction, see `gasless` below
rpcEndpoint: 'http://localhost:8545',
rpcMaxBlockRange: 648_000, // 15 days - Maximum supported range by Alchemy?
// eslint-disable-next-line max-len
// rpcEndpoint: 'wss://shy-sparkling-wind.matic-testnet.discover.quiknode.pro/4461ca78cea96dd6a168a58d8fc30a021cabf01d/',
Expand Down Expand Up @@ -75,16 +78,27 @@ export default {
earliestHistoryScanHeight: 13320830, // Block when Wallet was switched to Amoy testnet
},
usdc: {
tokenContract: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582',
tokenContract: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // MockUSDC
transferContract: '', // v1
htlcContract: '',
earliestHistoryScanHeight: 13320830, // Block when Wallet was switched to Amoy testnet
earliestHistoryScanHeight: 0,
},
usdt_bridged: {
tokenContract: '0x1616d425Cd540B256475cBfb604586C8598eC0FB',
tokenContract: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', // MockUSDT0
transferContract: '',
htlcContract: '',
earliestHistoryScanHeight: 13320830, // Block when USDT was added to the Wallet
earliestHistoryScanHeight: 0,
},
// Gasless USDC/USDT0 transfers on the local Anvil stack of https://github.com/NimiqToolbox/gas-abstraction
// (`make stack-up`, with the Anvil and relay ports made reachable from the host), see networkId, rpcEndpoint
// and the token addresses above. The deployment addresses are deterministic on a fresh chain.
gasless: {
relayUrl: 'http://localhost:8080',
transferContract: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
relays: ['0xBcd4042DE499D14e55001CcbB24a551F3b954096'] as string[], // Anvil account 10
deployBlock: 0,
maxFee: 5e6,
maxAcceptableFee: 0.5e6,
},
usdcConversion: {
swapContract: '',
Expand Down
16 changes: 15 additions & 1 deletion src/config/config.mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ export default {

polygon: {
enabled: true,
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
// Set to true to keep stablecoin swaps visible, but show a maintenance warning. Swaps are still relayed via
// OpenGSN.
isGasAbstractionUnderMaintenance: true,
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
// TODO: Set to false once the gasless relay address and URL are configured below.
isGaslessTransferUnderMaintenance: true,
networkId: 137,
rpcEndpoint: 'wss://polygon-mainnet.g.alchemy.com/v2/#ALCHEMY_API_KEY#',
rpcMaxBlockRange: 648_000, // 15 days - Maximum supported range by Alchemy?
Expand All @@ -76,6 +80,16 @@ export default {
htlcContract: '0xF615bD7EA00C4Cc7F39Faad0895dB5f40891359f',
earliestHistoryScanHeight: 63189500, // Block when USDT was added to the Wallet
},
// Gasless USDC/USDT0 transfers, see https://github.com/NimiqToolbox/gas-abstraction
gasless: {
relayUrl: '', // TODO: Add the production relay URL
transferContract: '0xA0df3CdF124d7101a67ebE5b1b97D222505De9D6',
// Relay addresses the user may sign for. More than one allows a relay key rotation.
relays: [] as string[], // TODO: Add the production relay address
deployBlock: 94577424,
maxFee: 5e6, // The contract's MAX_FEE: 5.00 USDC/USDT
maxAcceptableFee: 0.5e6, // The highest relay fee the user is asked to sign, same as in the Keyguard
},
usdcConversion: {
swapContract: '0xfAbBed813017bF535b40013c13b8702638aC25CD',
swapPoolContract: '0xD36ec33c8bed5a9F7B6630855f1533455b98a418',
Expand Down
14 changes: 13 additions & 1 deletion src/config/config.testnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ export default {

polygon: {
enabled: false,
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
// Set to true to keep stablecoin swaps visible, but show a maintenance warning. Swaps are still relayed via
// OpenGSN.
isGasAbstractionUnderMaintenance: false,
// Set to true to keep sending visible, but show a maintenance warning instead of the send modal.
isGaslessTransferUnderMaintenance: false,
networkId: 80002,
rpcEndpoint: 'wss://polygon-amoy.g.alchemy.com/v2/#ALCHEMY_API_KEY#',
rpcMaxBlockRange: 648_000, // 15 days - Maximum supported range by Alchemy?
Expand All @@ -64,6 +67,15 @@ export default {
htlcContract: '',
earliestHistoryScanHeight: 13320830, // Block when USDT was added to the Wallet
},
// Gasless transfers are not deployed on Amoy
gasless: {
relayUrl: '',
transferContract: '',
relays: [] as string[],
deployBlock: 0,
maxFee: 5e6,
maxAcceptableFee: 0.5e6,
},
usdcConversion: {
swapContract: '', // v2
swapPoolContract: '',
Expand Down
Loading
Loading