diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index fb0ae14ef..227d92af0 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -221,7 +221,7 @@ export type PolygonTransactionInfo = { /** * The sender's nonce in the token contract, required when calling the - * contract function `swapWithApproval` for bridged USDC.e. + * contract function `swapWithApproval` for bridged USDC.e or `transferWithApproval` for bridged USDT. */ approval?: { tokenNonce: number, diff --git a/src/assets/icons/usdt.svg b/src/assets/icons/usdt.svg new file mode 100644 index 000000000..58065072f --- /dev/null +++ b/src/assets/icons/usdt.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common.css b/src/common.css index e960977b5..657c5c789 100644 --- a/src/common.css +++ b/src/common.css @@ -345,6 +345,10 @@ body.loading .page:target .page-footer > .loading-spinner { content: "USDC"; } +.usdt-symbol::before { + content: "USDT"; +} + .eur-symbol::before { content: "EUR"; } diff --git a/src/components/PolygonAddressInfo.js b/src/components/PolygonAddressInfo.js index bf9718c9c..986cb9d14 100644 --- a/src/components/PolygonAddressInfo.js +++ b/src/components/PolygonAddressInfo.js @@ -7,7 +7,7 @@ class PolygonAddressInfo { // eslint-disable-line no-unused-vars /** * @param {string} address * @param {string} [label] - * @param {'none' | 'usdc' | 'usdc_dark' | 'unknown'} [logo = 'none'] + * @param {'none' | 'usdc' | 'usdc_dark' | 'usdt' | 'unknown'} [logo = 'none'] */ constructor(address, label, logo = 'none') { this._address = address; @@ -37,7 +37,12 @@ class PolygonAddressInfo { // eslint-disable-line no-unused-vars $avatar.classList.add('unlabelled'); } $el.appendChild($avatar); - } else if (this._logo === 'usdc' || this._logo === 'usdc_dark' || this._logo === 'unknown') { + } else if ( + this._logo === 'usdc' + || this._logo === 'usdc_dark' + || this._logo === 'usdt' + || this._logo === 'unknown' + ) { const $img = document.createElement('img'); $img.classList.add('logo'); $img.src = `../../assets/icons/${this._logo}.svg`; diff --git a/src/config/config.local.js b/src/config/config.local.js index c14a2d6bb..b624ce37c 100644 --- a/src/config/config.local.js +++ b/src/config/config.local.js @@ -16,14 +16,18 @@ const CONFIG = { // eslint-disable-line no-unused-vars BTC_NETWORK: /** @type {'MAIN' | 'TEST'} */ ('TEST'), // BitcoinConstants is not included in the common bundle ROOT_REDIRECT: 'https://wallet.nimiq-testnet.com', - POLYGON_CHAIN_ID: 80001, - BRIDGED_USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23', + POLYGON_CHAIN_ID: 80002, // Amoy testnet + BRIDGED_USDC_CONTRACT_ADDRESS: '', /** @deprecated */ - BRIDGED_USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', + BRIDGED_USDC_HTLC_CONTRACT_ADDRESS: '', - NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97', - NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1 - NATIVE_USDC_HTLC_CONTRACT_ADDRESS: '0xA9fAbABE97375565e4A9Ac69A57Df33c91FCB897', + NATIVE_USDC_CONTRACT_ADDRESS: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582', + NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '', + NATIVE_USDC_HTLC_CONTRACT_ADDRESS: '', - USDC_SWAP_CONTRACT_ADDRESS: '0xf4a619F6561CeE543BDa9BBA0cAC68758B607714', + USDC_SWAP_CONTRACT_ADDRESS: '', + + BRIDGED_USDT_CONTRACT_ADDRESS: '0x1616d425Cd540B256475cBfb604586C8598eC0FB', + BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS: '', + BRIDGED_USDT_HTLC_CONTRACT_ADDRESS: '', }; diff --git a/src/config/config.mainnet.js b/src/config/config.mainnet.js index 22adf1f46..c2efa113b 100644 --- a/src/config/config.mainnet.js +++ b/src/config/config.mainnet.js @@ -16,4 +16,8 @@ const CONFIG = { // eslint-disable-line no-unused-vars NATIVE_USDC_HTLC_CONTRACT_ADDRESS: '0x0cFD862bE942846Cebad797d7c1BC6e47714959b', USDC_SWAP_CONTRACT_ADDRESS: '0xfAbBed813017bF535b40013c13b8702638aC25CD', + + BRIDGED_USDT_CONTRACT_ADDRESS: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS: '0x98E69a6927747339d5E543586FC0262112eBe4BD', + BRIDGED_USDT_HTLC_CONTRACT_ADDRESS: '0xF615bD7EA00C4Cc7F39Faad0895dB5f40891359f', }; diff --git a/src/config/config.testnet.js b/src/config/config.testnet.js index 310139c30..1f504d37a 100644 --- a/src/config/config.testnet.js +++ b/src/config/config.testnet.js @@ -7,13 +7,17 @@ const CONFIG = { // eslint-disable-line no-unused-vars BTC_NETWORK: 'TEST', // BitcoinConstants is not included in the common bundle ROOT_REDIRECT: 'https://wallet.nimiq-testnet.com', - POLYGON_CHAIN_ID: 80001, - BRIDGED_USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23', - BRIDGED_USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', + POLYGON_CHAIN_ID: 80002, + BRIDGED_USDC_CONTRACT_ADDRESS: '', + BRIDGED_USDC_HTLC_CONTRACT_ADDRESS: '', - NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97', - NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1 - NATIVE_USDC_HTLC_CONTRACT_ADDRESS: '0xA9fAbABE97375565e4A9Ac69A57Df33c91FCB897', + NATIVE_USDC_CONTRACT_ADDRESS: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582', + NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '', + NATIVE_USDC_HTLC_CONTRACT_ADDRESS: '', - USDC_SWAP_CONTRACT_ADDRESS: '0xf4a619F6561CeE543BDa9BBA0cAC68758B607714', + USDC_SWAP_CONTRACT_ADDRESS: '', + + BRIDGED_USDT_CONTRACT_ADDRESS: '0x1616d425Cd540B256475cBfb604586C8598eC0FB', + BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS: '', + BRIDGED_USDT_HTLC_CONTRACT_ADDRESS: '', }; diff --git a/src/lib/polygon/PolygonContractABIs.full.js.txt b/src/lib/polygon/PolygonContractABIs.full.js.txt index f2eb333c5..c4676ee34 100644 --- a/src/lib/polygon/PolygonContractABIs.full.js.txt +++ b/src/lib/polygon/PolygonContractABIs.full.js.txt @@ -1,5 +1,4 @@ -/* eslint-disable max-len */ -const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars +const PolygonContractABIsFull = { BRIDGED_USDC_CONTRACT_ABI: [ 'event Approval(address indexed owner, address indexed spender, uint256 value)', 'event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce)', @@ -353,5 +352,96 @@ const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars "function withdrawToken(address token, uint256 amount, address target)", "function wrappedChainToken() view returns (address)", ], + + BRIDGED_USDT_CONTRACT_ABI: [ + 'constructor()', + 'event Approval(address indexed owner, address indexed spender, uint256 value)', + 'event MetaTransactionExecuted(address userAddress, address relayerAddress, bytes functionSignature)', + 'event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)', + 'event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)', + 'event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)', + 'event Transfer(address indexed from, address indexed to, uint256 value)', + 'function CHILD_CHAIN_ID() view returns (uint256)', + 'function CHILD_CHAIN_ID_BYTES() view returns (bytes)', + 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)', + 'function DEPOSITOR_ROLE() view returns (bytes32)', + 'function ERC712_VERSION() view returns (string)', + 'function ROOT_CHAIN_ID() view returns (uint256)', + 'function ROOT_CHAIN_ID_BYTES() view returns (bytes)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function changeName(string name_)', + 'function decimals() view returns (uint8)', + 'function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)', + 'function deposit(address user, bytes depositData)', + 'function executeMetaTransaction(address userAddress, bytes functionSignature, bytes32 sigR, bytes32 sigS, uint8 sigV) payable returns (bytes)', + 'function getChainId() pure returns (uint256)', + 'function getDomainSeperator() view returns (bytes32)', + 'function getNonce(address user) view returns (uint256 nonce)', + 'function getRoleAdmin(bytes32 role) view returns (bytes32)', + 'function getRoleMember(bytes32 role, uint256 index) view returns (address)', + 'function getRoleMemberCount(bytes32 role) view returns (uint256)', + 'function grantRole(bytes32 role, address account)', + 'function hasRole(bytes32 role, address account) view returns (bool)', + 'function increaseAllowance(address spender, uint256 addedValue) returns (bool)', + 'function initialize(string name_, string symbol_, uint8 decimals_, address childChainManager)', + 'function name() view returns (string)', + 'function renounceRole(bytes32 role, address account)', + 'function revokeRole(bytes32 role, address account)', + 'function symbol() view returns (string)', + 'function totalSupply() view returns (uint256)', + 'function transfer(address recipient, uint256 amount) returns (bool)', + 'function transferFrom(address sender, address recipient, uint256 amount) returns (bool)', + 'function withdraw(uint256 amount)', + ] + + BRIDGED_USDT_TRANSFER_CONTRACT_ABI: [ + 'constructor()', + 'event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + '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 isTrustedForwarder(address forwarder) view returns (bool)', + '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 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 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 transfer(address token, uint256 amount, address target, uint256 fee)', + 'function transferOwnership(address newOwner)', + 'function transferWithApproval(address token, uint256 amount, address target, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', + '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', + ], }; -/* eslint-enable max-len */ diff --git a/src/lib/polygon/PolygonContractABIs.js b/src/lib/polygon/PolygonContractABIs.js index ceefefcdc..f9d2a7c79 100644 --- a/src/lib/polygon/PolygonContractABIs.js +++ b/src/lib/polygon/PolygonContractABIs.js @@ -27,5 +27,14 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars 'function swap(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee)', 'function swapWithApproval(address token, uint256 amount, address pool, uint256 targetAmount, uint256 fee, uint256 approval, bytes32 sigR, bytes32 sigS, uint8 sigV)', ], + + BRIDGED_USDT_CONTRACT_ABI: [ + 'function approve(address spender, uint256 amount) returns (bool)', + ], + + BRIDGED_USDT_TRANSFER_CONTRACT_ABI: [ + '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)', + ], }; /* eslint-enable max-len */ diff --git a/src/lib/polygon/PolygonKey.js b/src/lib/polygon/PolygonKey.js index 9f0e6efed..60ceea884 100644 --- a/src/lib/polygon/PolygonKey.js +++ b/src/lib/polygon/PolygonKey.js @@ -93,6 +93,53 @@ class PolygonKey { // eslint-disable-line no-unused-vars return this._signatureToParts(signature); } + /** + * @param {string} path + * @param {ethers.Contract} usdtContract + * @param {string} forwarderContractAddress + * @param {ethers.BigNumber} approvalAmount + * @param {number} tokenNonce + * @param {string} fromAddress + * @returns {Promise<{sigR: string, sigS: string, sigV: number}>} + */ + async signUsdtApproval(path, usdtContract, forwarderContractAddress, approvalAmount, tokenNonce, fromAddress) { + const functionSignature = usdtContract.interface.encodeFunctionData( + 'approve', + [forwarderContractAddress, approvalAmount], + ); + + // TODO: Make the domain parameters configurable in the request? + const domain = { + name: '(PoS) Tether USD', // This is currently the same for testnet and mainnet + version: '1', // This is currently the same for testnet and mainnet + verifyingContract: CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS, + salt: ethers.utils.hexZeroPad(ethers.utils.hexlify(CONFIG.POLYGON_CHAIN_ID), 32), + }; + + const types = { + MetaTransaction: [ + { name: 'nonce', type: 'uint256' }, + { name: 'from', type: 'address' }, + { name: 'functionSignature', type: 'bytes' }, + ], + }; + + const message = { + nonce: tokenNonce, + from: fromAddress, + functionSignature, + }; + + const signature = await this.signTypedData( + path, + domain, + types, + message, + ); + + return this._signatureToParts(signature); + } + /** * @param {string} path * @param {string} forwarderContractAddress diff --git a/src/request/sign-polygon-transaction/SignPolygonTransaction.css b/src/request/sign-polygon-transaction/SignPolygonTransaction.css index 70a616b54..1155c2597 100644 --- a/src/request/sign-polygon-transaction/SignPolygonTransaction.css +++ b/src/request/sign-polygon-transaction/SignPolygonTransaction.css @@ -49,7 +49,7 @@ font-size: 5rem; } -#confirm-transaction .total .usdc-symbol { +#confirm-transaction .total .stablecoin-symbol { margin-left: 1rem; font-weight: 700; } diff --git a/src/request/sign-polygon-transaction/SignPolygonTransaction.js b/src/request/sign-polygon-transaction/SignPolygonTransaction.js index 518049d99..d8f850ec6 100644 --- a/src/request/sign-polygon-transaction/SignPolygonTransaction.js +++ b/src/request/sign-polygon-transaction/SignPolygonTransaction.js @@ -30,19 +30,42 @@ class SignPolygonTransaction { const relayRequest = request.request; + /** @type {'usdc' | 'usdt' | undefined} */ + let stablecoin; + if ([ + CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS, + CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS, + CONFIG.BRIDGED_USDC_HTLC_CONTRACT_ADDRESS, + CONFIG.USDC_SWAP_CONTRACT_ADDRESS, + ].includes(relayRequest.to)) { + stablecoin = 'usdc'; + } else if ([ + CONFIG.BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS, + CONFIG.BRIDGED_USDT_HTLC_CONTRACT_ADDRESS, + ].includes(relayRequest.to)) { + stablecoin = 'usdt'; + } + + const $stablecoinSymbols = /** @type {NodeListOf} */ ( + this.$el.querySelectorAll('.stablecoin-symbol') + ); + $stablecoinSymbols.forEach($symbol => { + $symbol.classList.add(`${stablecoin}-symbol`); + }); + const $sender = /** @type {HTMLLinkElement} */ (this.$el.querySelector('.accounts .sender')); if (['redeem', 'redeemWithSecretInData', 'refund'].includes(request.description.name)) { new PolygonAddressInfo(relayRequest.to, request.senderLabel, 'unknown').renderTo($sender); } else if (request.description.name === 'swap' || request.description.name === 'swapWithApproval') { new PolygonAddressInfo(relayRequest.from, 'USDC.e', 'usdc_dark').renderTo($sender); } else { - new PolygonAddressInfo(relayRequest.from, request.keyLabel, 'usdc').renderTo($sender); + new PolygonAddressInfo(relayRequest.from, request.keyLabel, stablecoin).renderTo($sender); } const $recipient = /** @type {HTMLLinkElement} */ (this.$el.querySelector('.accounts .recipient')); if (['redeem', 'redeemWithSecretInData', 'refund'].includes(request.description.name)) { const recipientAddress = /** @type {string} */ (request.description.args.target); - new PolygonAddressInfo(recipientAddress, request.keyLabel, 'usdc').renderTo($recipient); + new PolygonAddressInfo(recipientAddress, request.keyLabel, stablecoin).renderTo($recipient); } else if (request.description.name === 'swap' || request.description.name === 'swapWithApproval') { new PolygonAddressInfo(relayRequest.from, 'USDC', 'usdc').renderTo($recipient); } else { @@ -149,6 +172,37 @@ class SignPolygonTransaction { ]); } + if (request.description.name === 'transferWithApproval') { + const { sigR, sigS, sigV } = await polygonKey.signUsdtApproval( + request.keyPath, + new ethers.Contract( + CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_CONTRACT_ABI, + ), + transferContract, + request.description.args.approval, + // Has been validated to be defined when function called is `swapWithApproval` + /** @type {{ tokenNonce: number }} */ (request.approval).tokenNonce, + request.request.from, + ); + + const swapContract = new ethers.Contract( + transferContract, + PolygonContractABIs.BRIDGED_USDT_TRANSFER_CONTRACT_ABI, + ); + + request.request.data = swapContract.interface.encodeFunctionData(request.description.name, [ + /* address token */ request.description.args.token, + /* uint256 amount */ request.description.args.amount, + /* address target */ request.description.args.target, + /* uint256 fee */ request.description.args.fee, + /* uint256 approval */ request.description.args.approval, + /* bytes32 sigR */ sigR, + /* bytes32 sigS */ sigS, + /* uint8 sigV */ sigV, + ]); + } + if (request.description.name === 'swapWithApproval') { const { sigR, sigS, sigV } = await polygonKey.signUsdcApproval( request.keyPath, diff --git a/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js b/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js index 885528129..f5b04a438 100644 --- a/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js +++ b/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js @@ -59,6 +59,7 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { * KeyguardRequest.OpenGsnForwardRequest, * PolygonTransferDescription * | PolygonTransferWithPermitDescription + * | PolygonTransferWithApprovalDescription * | PolygonRedeemDescription * | PolygonRedeemWithSecretInDataDescription * | PolygonRefundDescription @@ -72,6 +73,7 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { /** * @type {PolygonTransferDescription * | PolygonTransferWithPermitDescription + * | PolygonTransferWithApprovalDescription * | PolygonRedeemDescription * | PolygonRedeemWithSecretInDataDescription * | PolygonRefundDescription @@ -100,6 +102,26 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { if (!['transfer', 'transferWithPermit'].includes(description.name)) { throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid'); } + } else if (forwardRequest.to === CONFIG.BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS) { + const bridgedUsdtTransferContract = new ethers.Contract( + CONFIG.BRIDGED_USDT_TRANSFER_CONTRACT_ADDRESS, + PolygonContractABIs.BRIDGED_USDT_TRANSFER_CONTRACT_ABI, + ); + + description = /** @type {PolygonTransferDescription | PolygonTransferWithApprovalDescription} */ ( + bridgedUsdtTransferContract.interface.parseTransaction({ + data: forwardRequest.data, + value: forwardRequest.value, + }) + ); + + if (description.args.token !== CONFIG.BRIDGED_USDT_CONTRACT_ADDRESS) { + throw new Errors.InvalidRequestError('Invalid bridged USDT token contract in request data'); + } + + if (!['transfer', 'transferWithApproval'].includes(description.name)) { + throw new Errors.InvalidRequestError('Requested Polygon contract method is invalid'); + } } else if (forwardRequest.to === CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS) { const usdcHtlcContract = new ethers.Contract( CONFIG.NATIVE_USDC_HTLC_CONTRACT_ADDRESS, @@ -186,10 +208,14 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { + '"transferWithPermit"'); } - // Check that approval object exists when method is 'swapWithApproval', and unset for other methods. - if ((description.name === 'swapWithApproval') !== !!request.approval) { - throw new Errors.InvalidRequestError('`approval` object is only allowed for contract method ' - + '"swapWithApproval"'); + // Check that approval object exists when method is 'transferWithApproval' or 'swapWithApproval', and unset for + // other methods. + if (( + description.name === 'transferWithApproval' + || description.name === 'swapWithApproval' + ) !== !!request.approval) { + throw new Errors.InvalidRequestError('`approval` object is only allowed for contract methods ' + + '"transferWithApproval" and "swapWithApproval"'); } return [forwardRequest, description]; diff --git a/src/request/sign-polygon-transaction/index.html b/src/request/sign-polygon-transaction/index.html index 70daf6df8..01ec0966d 100644 --- a/src/request/sign-polygon-transaction/index.html +++ b/src/request/sign-polygon-transaction/index.html @@ -91,11 +91,11 @@

Confirm Transaction

- +
diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index e98ca1ac8..9920b27f7 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -55,14 +55,14 @@ type ParsedBitcoinTransactionInput = { address: string, }; -interface PolygonUsdcApproval { +interface PolygonTokenApproval { readonly approval: ethers.BigNumber, // amount to be approved readonly sigR: string, readonly sigS: string, readonly sigV: ethers.BigNumber, } -interface PolygonUsdcPermit { +interface PolygonTokenPermit { readonly value: ethers.BigNumber, // amount to be approved readonly sigR: string, readonly sigS: string, @@ -81,13 +81,20 @@ type PolygonTransferDescription = ethers.utils.TransactionDescription & { readonly args: PolygonTransferArgs, }; -interface PolygonTransferWithPermitArgs extends PolygonTransferArgs, PolygonUsdcPermit {} +interface PolygonTransferWithPermitArgs extends PolygonTransferArgs, PolygonTokenPermit {} type PolygonTransferWithPermitDescription = ethers.utils.TransactionDescription & { readonly name: 'transferWithPermit', readonly args: PolygonTransferWithPermitArgs, }; +interface PolygonTransferWithApprovalArgs extends PolygonTransferArgs, PolygonTokenApproval {} + +type PolygonTransferWithApprovalDescription = ethers.utils.TransactionDescription & { + readonly name: 'transferWithApproval', + readonly args: PolygonTransferWithApprovalArgs, +}; + interface PolygonOpenArgs extends ReadonlyArray { readonly id: string, readonly token: string, @@ -104,7 +111,7 @@ type PolygonOpenDescription = ethers.utils.TransactionDescription & { readonly args: PolygonOpenArgs, }; -interface PolygonOpenWithPermitArgs extends PolygonOpenArgs, PolygonUsdcPermit {} +interface PolygonOpenWithPermitArgs extends PolygonOpenArgs, PolygonTokenPermit {} type PolygonOpenWithPermitDescription = ethers.utils.TransactionDescription & { readonly name: 'openWithPermit', @@ -158,7 +165,7 @@ type PolygonSwapDescription = ethers.utils.TransactionDescription & { readonly args: PolygonSwapArgs, }; -interface PolygonSwapWithApprovalArgs extends PolygonSwapArgs, PolygonUsdcApproval {} +interface PolygonSwapWithApprovalArgs extends PolygonSwapArgs, PolygonTokenApproval {} type PolygonSwapWithApprovalDescription = ethers.utils.TransactionDescription & { readonly name: 'swapWithApproval', @@ -306,6 +313,7 @@ type Parsed = KeyId2KeyInfo & { description: PolygonTransferDescription | PolygonTransferWithPermitDescription + | PolygonTransferWithApprovalDescription | PolygonRedeemDescription | PolygonRedeemWithSecretInDataDescription | PolygonRefundDescription