From 38dfefaef131cfe12524ff3aba28e37a0fe09c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Tue, 29 Oct 2024 13:50:52 +0100 Subject: [PATCH] Add support for swapping USDT --- client/src/PublicRequest.ts | 24 ++- src/components/BalanceDistributionBar.css | 4 + src/components/BalanceDistributionBar.js | 2 +- .../polygon/PolygonContractABIs.full.js.txt | 57 ++++++ src/lib/polygon/PolygonContractABIs.js | 8 + src/lib/swap/CryptoUtils.js | 11 +- src/request/sign-swap/SignSwap.css | 7 +- src/request/sign-swap/SignSwap.js | 130 ++++++++++-- src/request/sign-swap/SignSwapApi.js | 107 ++++++++-- src/request/swap-iframe/SwapIFrameApi.js | 188 +++++++++++++++++- src/translations/de.json | 3 - src/translations/en.json | 3 - src/translations/es.json | 3 - src/translations/fr.json | 3 - src/translations/nl.json | 3 - src/translations/pt.json | 3 - src/translations/ru.json | 3 - src/translations/uk.json | 3 - src/translations/zh.json | 3 - types/Keyguard.d.ts | 20 +- 20 files changed, 496 insertions(+), 89 deletions(-) diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index 227d92af0..d51552784 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -220,16 +220,16 @@ export type PolygonTransactionInfo = { amount?: number, /** - * The sender's nonce in the token contract, required when calling the - * contract function `swapWithApproval` for bridged USDC.e or `transferWithApproval` for bridged USDT. + * The sender's nonce in the token contract, required when calling the contract function `swapWithApproval` for + * bridged USDC.e or `transferWithApproval` and 'openWithApproval`for bridged USDT. */ approval?: { tokenNonce: number, }, /** - * The sender's nonce in the token contract, required when calling the - * contract function `transferWithPermit` for native USDC. + * The sender's nonce in the token contract, required when calling the contract functions `transferWithPermit` and + * `openWithPermit` for native USDC. */ permit?: { tokenNonce: number, @@ -290,6 +290,12 @@ export type SignSwapRequestCommon = SimpleRequest & { | 'approval' // HTLC opening for native USDC uses `permit`, not `approval` | 'amount' // Not used for HTLC opening - only for redeem and refund > + ) | ( + {type: 'USDT_MATIC'} + & Omit ) | ( {type: 'EUR'} & { @@ -323,7 +329,7 @@ export type SignSwapRequestCommon = SimpleRequest & { output: BitcoinTransactionChangeOutput, } ) | ( - {type: 'USDC_MATIC'} + {type: 'USDC_MATIC' | 'USDT_MATIC'} & Omit, }; @@ -408,7 +415,7 @@ export type SignSwapTransactionsRequest = { type: 'BTC', htlcScript: Uint8Array, } | { - type: 'USDC_MATIC', + type: 'USDC_MATIC' | 'USDT_MATIC', htlcData: string, } | { type: 'EUR', @@ -426,7 +433,7 @@ export type SignSwapTransactionsRequest = { transactionHash: string, outputIndex: number; } | { - type: 'USDC_MATIC', + type: 'USDC_MATIC' | 'USDT_MATIC', hash: string, timeout: number, htlcId: string, @@ -530,6 +537,7 @@ export type SignSwapTransactionsResult = { nim?: SignatureResult, btc?: SignedBitcoinTransaction, usdc?: SignedPolygonTransaction, + usdt?: SignedPolygonTransaction, eur?: string, // When funding EUR: empty string, when redeeming EUR: JWS of the settlement instructions refundTx?: string, }; diff --git a/src/components/BalanceDistributionBar.css b/src/components/BalanceDistributionBar.css index 603ee1025..af4b63ed8 100644 --- a/src/components/BalanceDistributionBar.css +++ b/src/components/BalanceDistributionBar.css @@ -37,6 +37,10 @@ background-color: #2775CA; /* USDC blue */ } +.balance-distribution-bar .bar.usdt { + background-color: #009393; /* USDT green */ +} + .balance-distribution-bar .bar:last-child { margin-right: 0; justify-content: flex-start; diff --git a/src/components/BalanceDistributionBar.js b/src/components/BalanceDistributionBar.js index 692e11b45..a6d116f55 100644 --- a/src/components/BalanceDistributionBar.js +++ b/src/components/BalanceDistributionBar.js @@ -3,7 +3,7 @@ /* global CryptoUtils */ /** @typedef {{address: string, balance: number, active: boolean, newBalance: number}} Segment */ -/** @typedef {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} Asset */ +/** @typedef {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} Asset */ class BalanceDistributionBar { // eslint-disable-line no-unused-vars /** diff --git a/src/lib/polygon/PolygonContractABIs.full.js.txt b/src/lib/polygon/PolygonContractABIs.full.js.txt index c4676ee34..2bb810fea 100644 --- a/src/lib/polygon/PolygonContractABIs.full.js.txt +++ b/src/lib/polygon/PolygonContractABIs.full.js.txt @@ -444,4 +444,61 @@ const PolygonContractABIsFull = { 'function wrappedChainToken() view returns (address)', 'receive() payable', ], + + BRIDGED_USDT_HTLC_CONTRACT_ABI: [ + 'constructor()', + 'event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue)', + 'event Open(bytes32 indexed id, address token, uint256 amount, address recipient, bytes32 hash, uint256 timeout)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + 'event Redeem(bytes32 indexed id, bytes32 secret)', + 'event Refund(bytes32 indexed id)', + 'event RequestTypeRegistered(bytes32 indexed typeHash, string typeStr)', + 'function EIP712_DOMAIN_TYPE() view returns (string)', + 'function deposits(address) view returns (uint256)', + 'function domains(bytes32) view returns (bool)', + 'function execute((address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, bytes32 domainSeparator, bytes32 requestTypeHash, bytes suffixData, bytes signature) payable returns (bool success, bytes ret)', + 'function getGasAndDataLimits() view returns ((uint256 acceptanceBudget, uint256 preRelayedCallGasLimit, uint256 postRelayedCallGasLimit, uint256 calldataSizeLimit) limits)', + 'function getHubAddr() view returns (address)', + 'function getMinimumRelayFee((uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData) view returns (uint256 amount)', + 'function getNonce(address from) view returns (uint256)', + 'function getRelayHubDeposit() view returns (uint256)', + 'function getRequiredRelayFee((uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData, bytes4 methodId) view returns (uint256 amount)', + 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256 gas)', + 'function htlcs(bytes32) view returns (address token, uint256 amount, address refund, address recipient, bytes32 hash, uint256 timeout)', + 'function isTrustedForwarder(address forwarder) view returns (bool)', + 'function open(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee)', + 'function openWithApproval(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function owner() view returns (address)', + 'function postRelayedCall(bytes context, bool success, uint256 gasUseWithoutPost, (uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData)', + 'function preRelayedCall(((address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, (uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData) relayRequest, bytes signature, bytes approvalData, uint256 maxPossibleGas) returns (bytes context, bool revertOnRecipientRevert)', + 'function redeem(bytes32 id, address target, bytes32 secret, uint256 fee)', + 'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)', + 'function refund(bytes32 id, address target, uint256 fee)', + 'function registerDomainSeparator(string name, string version)', + 'function registerRequestType(string typeName, string typeSuffix)', + 'function registerToken(address token, address pool)', + 'function registeredTokenPool(address) view returns (address)', + 'function registeredTokenPoolFee(address token) view returns (uint24 fee)', + 'function relayWithoutGsn(((address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, (uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData) relayRequest, bytes signature, bytes approvalData, address relay)', + 'function renounceOwnership()', + 'function requiredRelayGas() view returns (uint256)', + 'function setGasAndDataLimits((uint256 acceptanceBudget, uint256 preRelayedCallGasLimit, uint256 postRelayedCallGasLimit, uint256 calldataSizeLimit) limits)', + 'function setMaxRequiredRelayGas(uint256 gas)', + 'function setRelayHub(address hub)', + 'function setRequiredRelayGas(bytes4 methodId, uint256 gas)', + 'function setWrappedChainToken(address _wrappedChainToken)', + 'function transferOwnership(address newOwner)', + 'function trustedForwarder() view returns (address forwarder)', + 'function typeHashes(bytes32) view returns (bool)', + 'function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes _data)', + 'function unregisterToken(address token)', + 'function verify((address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) forwardRequest, bytes32 domainSeparator, bytes32 requestTypeHash, bytes suffixData, bytes signature) view', + 'function versionPaymaster() view returns (string)', + 'function versionRecipient() view returns (string)', + 'function withdraw(uint256 amount, address target)', + 'function withdrawRelayHubDeposit(uint256 amount, address target)', + 'function withdrawToken(address token, uint256 amount, address target)', + 'function wrappedChainToken() view returns (address)', + 'receive() payable', + ], }; diff --git a/src/lib/polygon/PolygonContractABIs.js b/src/lib/polygon/PolygonContractABIs.js index f9d2a7c79..b7af3bf64 100644 --- a/src/lib/polygon/PolygonContractABIs.js +++ b/src/lib/polygon/PolygonContractABIs.js @@ -36,5 +36,13 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars 'function transfer(address token, uint256 amount, address target, uint256 fee)', 'function transferWithApproval(address token, uint256 amount, address target, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', ], + + BRIDGED_USDT_HTLC_CONTRACT_ABI: [ + 'function open(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee)', + 'function openWithApproval(bytes32 id, address token, uint256 amount, address refundAddress, address recipientAddress, bytes32 hash, uint256 timeout, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + 'function redeem(bytes32 id, address target, bytes32 secret, uint256 fee)', + 'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)', + 'function refund(bytes32 id, address target, uint256 fee)', + ], }; /* eslint-enable max-len */ diff --git a/src/lib/swap/CryptoUtils.js b/src/lib/swap/CryptoUtils.js index 9742092d5..6a574845f 100644 --- a/src/lib/swap/CryptoUtils.js +++ b/src/lib/swap/CryptoUtils.js @@ -8,7 +8,7 @@ class CryptoUtils { // eslint-disable-line no-unused-vars /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} asset * @param {number} units * @returns {number} */ @@ -17,13 +17,14 @@ class CryptoUtils { // eslint-disable-line no-unused-vars case 'NIM': return Nimiq.Policy.lunasToCoins(units); case 'BTC': return BitcoinUtils.satoshisToCoins(units); case 'USDC_MATIC': return PolygonUtils.unitsToCoins(units); + case 'USDT_MATIC': return PolygonUtils.unitsToCoins(units); case 'EUR': return EuroUtils.centsToCoins(units); default: throw new Error(`Invalid asset ${asset}`); } } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} asset * @returns {number} */ static assetDecimals(asset) { @@ -31,20 +32,22 @@ class CryptoUtils { // eslint-disable-line no-unused-vars case 'NIM': return Math.log10(Nimiq.Policy.LUNAS_PER_COIN); case 'BTC': return Math.log10(BitcoinConstants.SATOSHIS_PER_COIN); case 'USDC_MATIC': return Math.log10(PolygonConstants.UNITS_PER_COIN); + case 'USDT_MATIC': return Math.log10(PolygonConstants.UNITS_PER_COIN); case 'EUR': return Math.log10(EuroConstants.CENTS_PER_COIN); default: throw new Error(`Invalid asset ${asset}`); } } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset - * @returns {'nim' | 'btc' | 'usdc' | 'eur'} + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} asset + * @returns {'nim' | 'btc' | 'usdc' | 'usdt' | 'eur'} */ static assetToCurrency(asset) { switch (asset) { case 'NIM': return 'nim'; case 'BTC': return 'btc'; case 'USDC_MATIC': return 'usdc'; + case 'USDT_MATIC': return 'usdt'; case 'EUR': return 'eur'; default: throw new Error(`Invalid asset ${asset}`); } diff --git a/src/request/sign-swap/SignSwap.css b/src/request/sign-swap/SignSwap.css index 9542d3273..4a6dd3ee8 100644 --- a/src/request/sign-swap/SignSwap.css +++ b/src/request/sign-swap/SignSwap.css @@ -5,6 +5,7 @@ .nim-symbol, .btc-symbol, .usdc-symbol, +.usdt-symbol, .eur-symbol { margin-left: 0.25em; } @@ -231,12 +232,14 @@ } .layout-slider .btc, -.layout-slider .usdc { +.layout-slider .usdc, +.layout-slider .usdt { --column-gap: 2rem; } .layout-slider .btc .identicon, -.layout-slider .usdc .identicon { +.layout-slider .usdc .identicon, +.layout-slider .usdt .identicon { width: 5.25rem; height: 5.25rem; margin: -0.25rem 0; diff --git a/src/request/sign-swap/SignSwap.js b/src/request/sign-swap/SignSwap.js index 29cc375fa..076a09f92 100644 --- a/src/request/sign-swap/SignSwap.js +++ b/src/request/sign-swap/SignSwap.js @@ -69,8 +69,9 @@ class SignSwap { case 'NIM': swapFromValue = fundTx.transaction.value + fundTx.transaction.fee; break; case 'BTC': swapFromValue = fundTx.inputs.reduce((sum, input) => sum + input.witnessUtxo.value, 0) - (fundTx.changeOutput ? fundTx.changeOutput.value : 0); break; - case 'USDC_MATIC': swapFromValue = fundTx.description.args.amount - .add(fundTx.description.args.fee).toNumber(); break; + case 'USDC_MATIC': + case 'USDT_MATIC': + swapFromValue = fundTx.description.args.amount.add(fundTx.description.args.fee).toNumber(); break; case 'EUR': swapFromValue = fundTx.amount + fundTx.fee; break; default: throw new Errors.KeyguardError('Invalid asset'); } @@ -80,7 +81,9 @@ class SignSwap { switch (redeemTx.type) { case 'NIM': swapToValue = redeemTx.transaction.value; break; case 'BTC': swapToValue = redeemTx.output.value; break; - case 'USDC_MATIC': swapToValue = redeemTx.amount; break; + case 'USDC_MATIC': + case 'USDT_MATIC': + swapToValue = redeemTx.amount; break; case 'EUR': swapToValue = redeemTx.amount - redeemTx.fee; break; default: throw new Errors.KeyguardError('Invalid asset'); } @@ -97,21 +100,21 @@ class SignSwap { $swapLeftValue.textContent = NumberFormatting.formatNumber( CryptoUtils.unitsToCoins(leftAsset, leftAmount), - leftAsset === 'USDC_MATIC' ? 2 : CryptoUtils.assetDecimals(leftAsset), - leftAsset === 'EUR' || leftAsset === 'USDC_MATIC' ? 2 : 0, + ['USDC_MATIC', 'USDT_MATIC'].includes(leftAsset) ? 2 : CryptoUtils.assetDecimals(leftAsset), + leftAsset === 'EUR' || ['USDC_MATIC', 'USDT_MATIC'].includes(leftAsset) ? 2 : 0, ); $swapRightValue.textContent = NumberFormatting.formatNumber( CryptoUtils.unitsToCoins(rightAsset, rightAmount), - rightAsset === 'USDC_MATIC' ? 2 : CryptoUtils.assetDecimals(rightAsset), - rightAsset === 'EUR' || rightAsset === 'USDC_MATIC' ? 2 : 0, + ['USDC_MATIC', 'USDT_MATIC'].includes(rightAsset) ? 2 : CryptoUtils.assetDecimals(rightAsset), + rightAsset === 'EUR' || ['USDC_MATIC', 'USDT_MATIC'].includes(rightAsset) ? 2 : 0, ); $swapValues.classList.add( `${CryptoUtils.assetToCurrency(fundTx.type)}-to-${CryptoUtils.assetToCurrency(redeemTx.type)}`, ); - /** @type {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} */ + /** @type {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} */ let exchangeBaseAsset; // If EUR is part of the swap, the other currency is the base asset if (fundTx.type === 'EUR') exchangeBaseAsset = redeemTx.type; @@ -180,10 +183,13 @@ class SignSwap { $leftLabel.textContent = request.fund.senderLabel; } else if (request.fund.type === 'BTC') { $leftIdenticon.innerHTML = TemplateTags.hasVars(0)``; - $leftLabel.textContent = I18n.translatePhrase('bitcoin'); + $leftLabel.textContent = 'Bitcoin'; } else if (request.fund.type === 'USDC_MATIC') { $leftIdenticon.innerHTML = TemplateTags.hasVars(0)``; - $leftLabel.textContent = I18n.translatePhrase('usd-coin'); + $leftLabel.textContent = 'USD Coin'; + } else if (request.fund.type === 'USDT_MATIC') { + $leftIdenticon.innerHTML = TemplateTags.hasVars(0)``; + $leftLabel.textContent = 'Tether USD'; } else if (request.fund.type === 'EUR') { $leftIdenticon.innerHTML = TemplateTags.hasVars(0)``; $leftLabel.textContent = request.fund.bankLabel || I18n.translatePhrase('sign-swap-your-bank'); @@ -195,10 +201,13 @@ class SignSwap { $rightLabel.textContent = request.redeem.recipientLabel; } else if (request.redeem.type === 'BTC') { $rightIdenticon.innerHTML = TemplateTags.hasVars(0)``; - $rightLabel.textContent = I18n.translatePhrase('bitcoin'); + $rightLabel.textContent = 'Bitcoin'; } else if (request.redeem.type === 'USDC_MATIC') { $rightIdenticon.innerHTML = TemplateTags.hasVars(0)``; - $rightLabel.textContent = I18n.translatePhrase('usd-coin'); + $rightLabel.textContent = 'USD Coin'; + } else if (request.redeem.type === 'USDT_MATIC') { + $rightIdenticon.innerHTML = TemplateTags.hasVars(0)``; + $rightLabel.textContent = 'Tether USD'; } else if (request.redeem.type === 'EUR') { $rightIdenticon.innerHTML = TemplateTags.hasVars(0)``; @@ -251,13 +260,19 @@ class SignSwap { if (leftAsset === 'BTC' || rightAsset === 'BTC') { (leftAsset === 'BTC' ? $leftIdenticon : $rightIdenticon) .innerHTML = TemplateTags.hasVars(0)``; - (leftAsset === 'BTC' ? $leftLabel : $rightLabel).textContent = I18n.translatePhrase('bitcoin'); + (leftAsset === 'BTC' ? $leftLabel : $rightLabel).textContent = 'Bitcoin'; } if (leftAsset === 'USDC_MATIC' || rightAsset === 'USDC_MATIC') { (leftAsset === 'USDC_MATIC' ? $leftIdenticon : $rightIdenticon) .innerHTML = TemplateTags.hasVars(0)``; - (leftAsset === 'USDC_MATIC' ? $leftLabel : $rightLabel).textContent = I18n.translatePhrase('usd-coin'); + (leftAsset === 'USDC_MATIC' ? $leftLabel : $rightLabel).textContent = 'USD Coin'; + } + + if (leftAsset === 'USDT_MATIC' || rightAsset === 'USDT_MATIC') { + (leftAsset === 'USDT_MATIC' ? $leftIdenticon : $rightIdenticon) + .innerHTML = TemplateTags.hasVars(0)``; + (leftAsset === 'USDT_MATIC' ? $leftLabel : $rightLabel).textContent = 'Tether USD'; } // Add signs in front of swap amounts @@ -377,6 +392,35 @@ class SignSwap { else rightSegments = segments; } + if (leftAsset === 'USDT_MATIC' || rightAsset === 'USDT_MATIC') { + const amount = leftAsset === 'USDT_MATIC' ? leftAmount : rightAmount; + + const newBalance = request.polygonAddresses[0].usdtBalance + + (amount * (fundTx.type === 'USDT_MATIC' ? -1 : 1)); + const newBalanceFormatted = NumberFormatting.formatNumber( + CryptoUtils.unitsToCoins('USDT_MATIC', newBalance), 2, 2, + ); + + if (leftAsset === 'USDT_MATIC') { + $leftNewBalance.textContent = `${newBalanceFormatted} USDT`; + $leftAccount.classList.add('usdt'); + } else if (rightAsset === 'USDT_MATIC') { + $rightNewBalance.textContent = `${newBalanceFormatted} USDT`; + $rightAccount.classList.add('usdt'); + } + + /** @type {Segment[]} */ + const segments = [{ + address: 'usdt', + balance: request.polygonAddresses[0].usdtBalance, + active: true, + newBalance, + }]; + + if (leftAsset === 'USDT_MATIC') leftSegments = segments; + else rightSegments = segments; + } + if (!leftSegments || !rightSegments) { throw new Errors.KeyguardError('Missing segments for balance distribution bar'); } @@ -409,7 +453,7 @@ class SignSwap { } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'USDT_MATIC' | 'EUR'} asset * @param {Parsed} request * @returns {number} */ @@ -437,11 +481,12 @@ class SignSwap { ? redeemTx.input.witnessUtxo.value + request.redeemFees.funding : 0; // Should never happen, if parsing works correctly case 'USDC_MATIC': - return fundTx.type === 'USDC_MATIC' - // When the user funds USDC, the service receives the HTLC balance - their network fee. + case 'USDT_MATIC': + return fundTx.type === asset + // When the user funds USDC/T, the service receives the HTLC balance - their network fee. ? fundTx.description.args.amount.toNumber() - request.fundFees.redeeming - : redeemTx.type === 'USDC_MATIC' - // When the user redeems USDC, the service lost the HTLC balance + their network fee. + : redeemTx.type === asset + // When the user redeems USDC/T, the service lost the HTLC balance + their network fee. // The transaction value is "HTLC balance - tx fee", therefore the "HTLC balance" // is the transaction value + tx fee. ? redeemTx.amount + redeemTx.description.args.fee.toNumber() + request.redeemFees.funding @@ -490,7 +535,7 @@ class SignSwap { const bitcoinKey = new BitcoinKey(key); const polygonKey = new PolygonKey(key); - /** @type {{nim: string, btc: string[], usdc: string, eur: string, btc_refund?: string}} */ + /** @type {{nim: string, btc: string[], usdc: string, usdt: string, eur: string, btc_refund?: string}} */ const privateKeys = {}; if (request.fund.type === 'NIM') { @@ -565,6 +610,46 @@ class SignSwap { privateKeys.usdc = wallet.privateKey; } + if (request.fund.type === 'USDT_MATIC') { + if (request.fund.description.name === 'openWithApproval') { + const { sigR, sigS, sigV } = await polygonKey.signUsdtApproval( + request.fund.keyPath, + new ethers.Contract( + CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_CONTRACT_ABI, + ), + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + request.fund.description.args.approval, + // Has been validated to be defined when function called is `openWithApproval` + /** @type {{ tokenNonce: number }} */ (request.fund.approval).tokenNonce, + request.fund.request.from, + ); + + const htlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + request.fund.request.data = htlcContract.interface.encodeFunctionData(request.fund.description.name, [ + /* bytes32 id */ request.fund.description.args.id, + /* address token */ request.fund.description.args.token, + /* uint256 amount */ request.fund.description.args.amount, + /* address refundAddress */ request.fund.description.args.refundAddress, + /* address recipientAddress */ request.fund.description.args.recipientAddress, + /* bytes32 hash */ request.fund.description.args.hash, + /* uint256 timeout */ request.fund.description.args.timeout, + /* uint256 fee */ request.fund.description.args.fee, + /* uint256 approval */ request.fund.description.args.approval, + /* bytes32 sigR */ sigR, + /* bytes32 sigS */ sigS, + /* uint8 sigV */ sigV, + ]); + } + + const wallet = polygonKey.deriveKeyPair(request.fund.keyPath); + privateKeys.usdt = wallet.privateKey; + } + if (request.fund.type === 'EUR') { // No signature required } @@ -594,6 +679,11 @@ class SignSwap { privateKeys.usdc = wallet.privateKey; } + if (request.redeem.type === 'USDT_MATIC') { + const wallet = polygonKey.deriveKeyPair(request.redeem.keyPath); + privateKeys.usdt = wallet.privateKey; + } + /** @type {string | undefined} */ let eurPubKey; diff --git a/src/request/sign-swap/SignSwapApi.js b/src/request/sign-swap/SignSwapApi.js index 4e09448c8..b3f36c60c 100644 --- a/src/request/sign-swap/SignSwapApi.js +++ b/src/request/sign-swap/SignSwapApi.js @@ -88,6 +88,21 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To relayData: this.parseOpenGsnRelayData(request.fund.relayData), permit: request.fund.permit, }; + } else if (request.fund.type === 'USDT_MATIC') { + const [forwardRequest, description] = this.parseOpenGsnForwardRequest( + request.fund, + ['open', 'openWithApproval'], + ); + + parsedRequest.fund = { + type: 'USDT_MATIC', + keyPath: this.parsePolygonPath(request.fund.keyPath, 'fund.keyPath'), + // eslint-disable-next-line object-shorthand + description: /** @type {PolygonOpenDescription | PolygonOpenWithApprovalDescription} */ (description), + request: forwardRequest, + relayData: this.parseOpenGsnRelayData(request.fund.relayData), + approval: request.fund.approval, + }; } else if (request.fund.type === 'EUR') { parsedRequest.fund = { type: 'EUR', @@ -128,14 +143,14 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To request.redeem.output, false, 'redeem.output', )), }; - } else if (request.redeem.type === 'USDC_MATIC') { + } else if (request.redeem.type === 'USDC_MATIC' || request.redeem.type === 'USDT_MATIC') { const [forwardRequest, description] = this.parseOpenGsnForwardRequest( request.redeem, ['redeem', 'redeemWithSecretInData'], ); parsedRequest.redeem = { - type: 'USDC_MATIC', + type: request.redeem.type, keyPath: this.parsePolygonPath(request.redeem.keyPath, 'fund.keyPath'), // eslint-disable-next-line object-shorthand description: /** @type {PolygonRedeemDescription | PolygonRedeemWithSecretInDataDescription} */ @@ -182,10 +197,10 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To if (request.layout === SignSwapApi.Layouts.SLIDER && parsedRequest.layout === SignSwapApi.Layouts.SLIDER) { // SLIDER layout is only allowed for crypto-to-crypto swaps - const assets = ['NIM', 'BTC', 'USDC_MATIC']; + const assets = ['NIM', 'BTC', 'USDC_MATIC', 'USDT_MATIC']; if (!assets.includes(parsedRequest.fund.type) || !assets.includes(parsedRequest.redeem.type)) { throw new Errors.InvalidRequestError( - 'The \'slider\' layout is only allowed for swaps between NIM, BTC and USDC', + 'The \'slider\' layout is only allowed for swaps between NIM, BTC, USDC and USDT', ); } @@ -198,9 +213,14 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To parsedRequest.bitcoinAccount = { balance: this.parsePositiveInteger(request.bitcoinAccount.balance, true, 'bitcoinAccount.balance'), }; - parsedRequest.polygonAddresses = request.polygonAddresses.map(({ address, usdcBalance }, index) => ({ + parsedRequest.polygonAddresses = request.polygonAddresses.map(({ + address, + usdcBalance, + usdtBalance, + }, index) => ({ address: this.parsePolygonAddress(address, `polygonAddresses[${index}].address`), - usdcBalance: this.parsePositiveInteger(usdcBalance, true, `polygonAddresses[${index}].balance`), + usdcBalance: this.parsePositiveInteger(usdcBalance, true, `polygonAddresses[${index}].usdcBalance`), + usdtBalance: this.parsePositiveInteger(usdtBalance, true, `polygonAddresses[${index}].usdtBalance`), })); // Verify that used Nimiq address is in nimiqAddresses[] and has enough balance @@ -250,6 +270,12 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To .add(parsedRequest.fund.description.args.fee).toNumber() ) { throw new Errors.InvalidRequestError('The sending USDC address does not have enough balance'); + } else if ( + parsedRequest.fund.type === 'USDT_MATIC' + && activePolygonAddress.usdtBalance < parsedRequest.fund.description.args.amount + .add(parsedRequest.fund.description.args.fee).toNumber() + ) { + throw new Errors.InvalidRequestError('The sending USDT address does not have enough balance'); } } } @@ -306,12 +332,19 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To // eslint-disable-next-line valid-jsdoc /** * - * @param {Omit} request - * @param {Array<'open' | 'openWithPermit' | 'redeem' | 'redeemWithSecretInData'>} allowedMethods + * @param {Omit} request + * @param {Array< + * | 'open' + * | 'openWithPermit' + * | 'openWithApproval' + * | 'redeem' + * | 'redeemWithSecretInData' + * >} allowedMethods * @returns {[ * KeyguardRequest.OpenGsnForwardRequest, - * PolygonOpenDescription + * | PolygonOpenDescription * | PolygonOpenWithPermitDescription + * | PolygonOpenWithApprovalDescription * | PolygonRedeemDescription * | PolygonRedeemWithSecretInDataDescription, * ]} @@ -319,22 +352,32 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To parseOpenGsnForwardRequest(request, allowedMethods) { const forwardRequest = this.parseOpenGsnForwardRequestRoot(request.request); - if (forwardRequest.to !== CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS) { + if ( + forwardRequest.to !== CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS + && forwardRequest.to !== CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS + ) { throw new Errors.InvalidRequestError('request.to address is not allowed'); } - const usdcHtlcContract = new ethers.Contract( - CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS, - PolygonContractABIs.NATIVE_USDC_HTLC_CONTRACT_ABI, - ); + const htlcContract = { + [CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS]: () => new ethers.Contract( + CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.NATIVE_USDC_HTLC_CONTRACT_ABI, + ), + [CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS]: () => new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ), + }[forwardRequest.to](); // eslint-disable-next-line operator-linebreak const description = /** @type {PolygonOpenDescription * | PolygonOpenWithPermitDescription + * | PolygonOpenWithApprovalDescription * | PolygonRedeemDescription * | PolygonRedeemWithSecretInDataDescription} - */ (usdcHtlcContract.interface.parseTransaction({ + */ (htlcContract.interface.parseTransaction({ data: forwardRequest.data, value: forwardRequest.value, })); @@ -343,19 +386,37 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid'); } - if (description.name === 'open' || description.name === 'openWithPermit') { - if (description.args.token !== CONFIG.NATIVE_USDC_CONTRACT_ADDRESS) { - throw new Errors.InvalidRequestError('Invalid USDC token contract in request data'); + if ( + description.name === 'open' + || description.name === 'openWithPermit' + || description.name === 'openWithApproval' + ) { + if ( + (description.name === 'open' && forwardRequest.to === CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS) + || description.name === 'openWithPermit' + ) { + if (description.args.token !== CONFIG.NATIVE_USDC_CONTRACT_ADDRESS) { + throw new Errors.InvalidRequestError('Invalid USDC token contract in request data'); + } + } + + if ( + (description.name === 'open' && forwardRequest.to === CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS) + || description.name === 'openWithApproval' + ) { + if (description.args.token !== CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS) { + throw new Errors.InvalidRequestError('Invalid USDT token contract in request data'); + } } if (description.args.refundAddress !== forwardRequest.from) { - throw new Errors.InvalidRequestError('USDC HTLC refund address must be same as sender'); + throw new Errors.InvalidRequestError('HTLC refund address must be same as sender'); } } if (description.name === 'redeem' || description.name === 'redeemWithSecretInData') { if (description.args.target !== forwardRequest.from) { - throw new Errors.InvalidRequestError('USDC HTLC target address must be same as sender'); + throw new Errors.InvalidRequestError('HTLC target address must be same as sender'); } } @@ -365,6 +426,12 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To + '"openWithPermit"'); } + // Check that approval object exists when method is 'openWithApproval', and unset for other methods. + if ((description.name === 'openWithApproval') !== !!request.approval) { + throw new Errors.InvalidRequestError('`approval` object is only allowed for contract method ' + + '"openWithApproval"'); + } + return [forwardRequest, description]; } diff --git a/src/request/swap-iframe/SwapIFrameApi.js b/src/request/swap-iframe/SwapIFrameApi.js index fa72f57ff..b2b8ec558 100644 --- a/src/request/swap-iframe/SwapIFrameApi.js +++ b/src/request/swap-iframe/SwapIFrameApi.js @@ -43,6 +43,7 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint * nim: string, * btc: string[], * usdc: string, + * usdt: string, * eur: string, * btc_refund?: string, * }, request: any}} */ @@ -67,6 +68,11 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint if (privateKeys.usdc.length !== 66) throw new Error('Invalid USDC key stored in SessionStorage'); } + if (request.fund.type === 'USDT_MATIC' || request.redeem.type === 'USDT_MATIC') { + if (!privateKeys.usdt) throw new Error('No USDT key stored in SessionStorage'); + if (privateKeys.usdt.length !== 66) throw new Error('Invalid USDT key stored in SessionStorage'); + } + if (request.redeem.type === 'EUR') { if (!privateKeys.eur) throw new Error('No EUR key stored in SessionStorage'); if (privateKeys.eur.length !== 64) throw new Error('Invalid EUR key stored in SessionStorage'); @@ -96,6 +102,17 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint value: storedRawRequest.fund.request.value, }); } + if (storedRawRequest.fund.type === 'USDT_MATIC') { + const usdtHtlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + storedRawRequest.fund.description = usdtHtlcContract.interface.parseTransaction({ + data: storedRawRequest.fund.request.data, + value: storedRawRequest.fund.request.value, + }); + } if (storedRawRequest.redeem.type === 'NIM') { storedRawRequest.redeem.transaction = Nimiq.Transaction.fromPlain(storedRawRequest.redeem.transaction); @@ -111,6 +128,17 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint value: storedRawRequest.redeem.request.value, }); } + if (storedRawRequest.redeem.type === 'USDT_MATIC') { + const usdtHtlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + storedRawRequest.redeem.description = usdtHtlcContract.interface.parseTransaction({ + data: storedRawRequest.redeem.request.data, + value: storedRawRequest.redeem.request.value, + }); + } /** @type {Parsed} */ const storedRequest = storedRawRequest; @@ -230,10 +258,13 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint PolygonContractABIs.NATIVE_USDC_HTLC_CONTRACT_ABI, ); - const description = /** @type {PolygonOpenDescription} */ (usdcHtlcContract.interface.parseTransaction({ - data: request.fund.htlcData, - value: 0, - })); + + const description = /** @type {PolygonOpenDescription | PolygonOpenWithPermitDescription} */ ( + usdcHtlcContract.interface.parseTransaction({ + data: request.fund.htlcData, + value: 0, + }) + ); // The htlcData given by Fastspot and forwarded here is always for the open() function, not for // openWithPermit(). The permit, if requested, is added below, from the stored request where @@ -274,6 +305,58 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint }; } + if (request.fund.type === 'USDT_MATIC' && storedRequest.fund.type === 'USDT_MATIC') { + const usdtHtlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + const description = /** @type {PolygonOpenDescription | PolygonOpenWithApprovalDescription} */ ( + usdtHtlcContract.interface.parseTransaction({ + data: request.fund.htlcData, + value: 0, + }) + ); + + // The htlcData given by Fastspot and forwarded here is always for the open() function, not for + // openWithPermit(). The permit, if requested, is added below, from the stored request where + // the user gave their authorization. + if (description.name !== 'open') { + throw new Errors.InvalidRequestError('Invalid method in HTLC data'); + } + + // Verify already known parts of the data + if (description.args.token !== CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS) { + throw new Errors.InvalidRequestError('Invalid USDT token contract in HTLC data'); + } + + if (!description.args.amount.eq(storedRequest.fund.description.args.amount)) { + throw new Errors.InvalidRequestError('Invalid amount in HTLC data'); + } + + if (description.args.refundAddress !== storedRequest.fund.request.from) { + throw new Errors.InvalidRequestError('USDT HTLC refund address must be same as sender'); + } + + fund = { + type: 'USDT_MATIC', + description, + }; + } + + if (request.redeem.type === 'USDT_MATIC' && storedRequest.redeem.type === 'USDT_MATIC') { + redeem = { + type: 'USDT_MATIC', + htlcId: `0x${Nimiq.BufferUtils.toHex(Nimiq.BufferUtils.fromAny( + request.redeem.htlcId.replace(/^0x/i, ''), + ))}`, + htlcDetails: { + hash: Nimiq.BufferUtils.toHex(Nimiq.BufferUtils.fromAny(request.redeem.hash)), + timeoutTimestamp: this.parsePositiveInteger(request.redeem.timeout, false, 'redeem.timeout'), + }, + }; + } + if (request.fund.type === 'EUR' && storedRequest.fund.type === 'EUR') { fund = { type: 'EUR', @@ -597,6 +680,59 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint }; } + if (parsedRequest.fund.type === 'USDT_MATIC' && storedRequest.fund.type === 'USDT_MATIC') { + const usdtHtlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + // Place contract details into existing function call data + storedRequest.fund.request.data = usdtHtlcContract.interface.encodeFunctionData( + storedRequest.fund.description.name, + [ + /* bytes32 id */ parsedRequest.fund.description.args.id, + /* address token */ parsedRequest.fund.description.args.token, + /* uint256 amount */ parsedRequest.fund.description.args.amount, + /* address refundAddress */ parsedRequest.fund.description.args.refundAddress, + /* address recipientAddress */ parsedRequest.fund.description.args.recipientAddress, + /* bytes32 hash */ parsedRequest.fund.description.args.hash, + /* uint256 timeout */ parsedRequest.fund.description.args.timeout, + + /* uint256 fee */ storedRequest.fund.description.args.fee, + ...(storedRequest.fund.description.name === 'openWithApproval' ? [ + /* uint256 approval */ storedRequest.fund.description.args.approval, + /* bytes32 sigR */ storedRequest.fund.description.args.sigR, + /* bytes32 sigS */ storedRequest.fund.description.args.sigS, + /* uint8 sigV */ storedRequest.fund.description.args.sigV, + ] : []), + ], + ); + + const typedData = new OpenGSN.TypedRequestData( + CONFIG.POLYGON_CHAIN_ID, + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + { + request: storedRequest.fund.request, + relayData: storedRequest.fund.relayData, + }, + ); + + const { EIP712Domain, ...cleanedTypes } = typedData.types; + + const wallet = new ethers.Wallet(privateKeys.usdt); + + const signature = await wallet._signTypedData( + typedData.domain, + /** @type {Record} */ (/** @type {unknown} */ (cleanedTypes)), + typedData.message, + ); + + result.usdt = { + message: typedData.message, + signature, + }; + } + if (parsedRequest.fund.type === 'EUR' && storedRequest.fund.type === 'EUR') { // Nothing to do for funding EUR result.eur = ''; @@ -741,6 +877,50 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint }; } + if (parsedRequest.redeem.type === 'USDT_MATIC' && storedRequest.redeem.type === 'USDT_MATIC') { + const usdtHtlcContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_HTLC_CONTRACT_ABI, + ); + + // Place contract details into existing function call data + storedRequest.redeem.request.data = usdtHtlcContract.interface.encodeFunctionData( + storedRequest.redeem.description.name, + [ + /* bytes32 id */ parsedRequest.redeem.htlcId, + /* address target */ storedRequest.redeem.description.args.target, + ...(storedRequest.redeem.description.name === 'redeem' ? [ + /* bytes32 secret */ storedRequest.redeem.description.args.secret, + ] : []), + /* uint256 fee */ storedRequest.redeem.description.args.fee, + ], + ); + + const typedData = new OpenGSN.TypedRequestData( + CONFIG.POLYGON_CHAIN_ID, + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + { + request: storedRequest.redeem.request, + relayData: storedRequest.redeem.relayData, + }, + ); + + const { EIP712Domain, ...cleanedTypes } = typedData.types; + + const wallet = new ethers.Wallet(privateKeys.usdt); + + const signature = await wallet._signTypedData( + typedData.domain, + /** @type {Record} */ (/** @type {unknown} */ (cleanedTypes)), + typedData.message, + ); + + result.usdt = { + message: typedData.message, + signature, + }; + } + if (parsedRequest.redeem.type === 'EUR' && storedRequest.redeem.type === 'EUR') { await loadNimiq(); diff --git a/src/translations/de.json b/src/translations/de.json index 3fc97fcb2..b104d92da 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Login-Datei-{accountLabel}.png", "login-file-default-account-label": "{color} Konto", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Unbeschriftet", - "usd-coin": "USD Coin", - "derive-btc-xpub-heading": "Füge deinem Account\nBitcoin hinzu", "derive-btc-xpub-text": "Einfach zwischen NIM, dem superperformanten Zahlungscoin und BTC, dem Goldstandard der Krytowährungen tauschen.", diff --git a/src/translations/en.json b/src/translations/en.json index ea60c6524..d3926d550 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Login-File-{accountLabel}.png", "login-file-default-account-label": "{color} Account", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Unlabelled", - "usd-coin": "USD Coin", - "derive-btc-xpub-heading": "Add Bitcoin\nto your account", "derive-btc-xpub-text": "Easily swap between NIM, the super performant payment coin and BTC, the gold standard of crypto.", diff --git a/src/translations/es.json b/src/translations/es.json index d133e85a4..604055793 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Archivo-Sesion-{accountLabel}.png", "login-file-default-account-label": "Cuenta {color}", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Sin etiqueta", - "usd-coin": "USD Coin", - "derive-btc-xpub-heading": "Agregar Bitcoin\na su Cuenta", "derive-btc-xpub-text": "Fácilmente intecambie entre NIM, la moneda de pago con super rendimiento y BTC, el estándar de oro en cripto.", diff --git a/src/translations/fr.json b/src/translations/fr.json index 9ecab2a13..5dd3c4799 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -220,11 +220,8 @@ "login-file-filename": "Fichier-de-Connexion-Nimiq-{accountLabel}.png", "login-file-default-account-label": "Compte {color}", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Non-libellé", - "usd-coin": "USD Coin", - "derive-btc-xpub-heading": "Ajouter Bitcoin\nà votre compte", "derive-btc-xpub-text": "Swappez facilement entre NIM, la monnaie de paiement super performante et BTC, l'étalon-or de la crypto.", diff --git a/src/translations/nl.json b/src/translations/nl.json index 062fdf867..da60b27a3 100644 --- a/src/translations/nl.json +++ b/src/translations/nl.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Login-File-{accountLabel}.png", "login-file-default-account-label": "{color} Account", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Ongelabeld", - "usd-coin": "USD Munt", - "derive-btc-xpub-heading": "Voeg Bitcoin\ntoe aan je account", "derive-btc-xpub-text": "Wissel eenvoudig tussen NIM, de superpresterende betaalmunt en BTC, de gouden standaard van crypto.", diff --git a/src/translations/pt.json b/src/translations/pt.json index 92368b6a4..a6655c9cd 100644 --- a/src/translations/pt.json +++ b/src/translations/pt.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Ficheiro-Login-{accountLabel}.png", "login-file-default-account-label": "Conta {color}", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Sem marcador", - "usd-coin": "Moeda USD", - "derive-btc-xpub-heading": "Adiciona Bitcoin\nna tua conta", "derive-btc-xpub-text": "Faz uma troca fácil entre NIM, a moeda de pagamento de alto desempenho e BTC, o ouro das criptomoedas.", diff --git a/src/translations/ru.json b/src/translations/ru.json index 027d2e7da..63a2f17d3 100644 --- a/src/translations/ru.json +++ b/src/translations/ru.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-Файл-Авторизации-{accountLabel}.png", "login-file-default-account-label": "{color} Аккаунт", - "bitcoin": "Bitcoin", "bitcoin-recipient-unlabelled": "Немаркированный", - "usd-coin": "USD Coin", - "derive-btc-xpub-heading": "Добавьте Биткоин\nна свой аккаунт", "derive-btc-xpub-text": "NIM - супербыстрая платёжная монета. BTC - золотой криптовалютный стандарт. Меняйте NIM на BTC - и обратно. Это легко и просто!", diff --git a/src/translations/uk.json b/src/translations/uk.json index d87e8fd59..4e9250cfe 100644 --- a/src/translations/uk.json +++ b/src/translations/uk.json @@ -220,11 +220,8 @@ "login-file-filename": "Файл-Ключ-Німік-{accountLabel}.png", "login-file-default-account-label": "{color} Рахунок", - "bitcoin": "Біткоїн", "bitcoin-recipient-unlabelled": "Без назви", - "usd-coin": "USD Монета", - "derive-btc-xpub-heading": "Додати Біткоїн\nдо вашого рахунку", "derive-btc-xpub-text": "З легкістю обмінюйте NIM - ефективну платіжну монету та BTC - золотий стандарт криптовалюти.", diff --git a/src/translations/zh.json b/src/translations/zh.json index 70c90c539..5f88b5922 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -220,11 +220,8 @@ "login-file-filename": "Nimiq-登录文件-{accountLabel}.png", "login-file-default-account-label": "{color}帐户", - "bitcoin": "比特币", "bitcoin-recipient-unlabelled": "未标签", - "usd-coin": "USD 币", - "derive-btc-xpub-heading": "添加比特币\n到你的帐户", "derive-btc-xpub-text": "在超级高性能的支付硬币:NIM,与加密的黄金标准:比特币之间轻松交换。", diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 9920b27f7..6e87d7d13 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -118,6 +118,13 @@ type PolygonOpenWithPermitDescription = ethers.utils.TransactionDescription & { readonly args: PolygonOpenWithPermitArgs, }; +interface PolygonOpenWithApprovalArgs extends PolygonOpenArgs, PolygonTokenApproval {} + +type PolygonOpenWithApprovalDescription = ethers.utils.TransactionDescription & { + readonly name: 'openWithApproval', + readonly args: PolygonOpenWithApprovalArgs, +}; + interface PolygonRedeemArgs extends ReadonlyArray { readonly id: string, readonly target: string, @@ -219,6 +226,9 @@ type ConstructSwap = Transform< } | Transform | Transform | { type: 'EUR', amount: number, @@ -242,7 +252,7 @@ type ConstructSwap = Transform< }, output: KeyguardRequest.BitcoinTransactionChangeOutput, } | Transform | { @@ -335,6 +345,7 @@ type Parsed = polygonAddresses: Array<{ address: string, usdcBalance: number, // smallest unit of USDC (= 0.000001 USDC) + usdtBalance: number, // smallest unit of USDT (= 0.000001 USDT) }> } : T extends Is ? @@ -352,7 +363,10 @@ type Parsed = htlcAddress: string, } | { type: 'USDC_MATIC', - description: PolygonOpenDescription, + description: PolygonOpenDescription | PolygonOpenWithPermitDescription, + } | { + type: 'USDT_MATIC', + description: PolygonOpenDescription | PolygonOpenWithApprovalDescription, } | { type: 'EUR', htlcDetails: EurHtlcContents, @@ -371,7 +385,7 @@ type Parsed = outputIndex: number, outputScript: Buffer, } | { - type: 'USDC_MATIC', + type: 'USDC_MATIC' | 'USDT_MATIC', htlcId: string, htlcDetails: { hash: string,