From fb4226949723c1bd901793a976e976e705b071fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Thu, 16 Nov 2023 18:53:23 -0600 Subject: [PATCH] Add support for sending native USDC with transferWithPermit --- client/src/PublicRequest.ts | 8 ++ src/config/config.local.js | 3 + src/config/config.mainnet.js | 3 + src/config/config.testnet.js | 3 + src/lib/polygon/PolygonContractABIs.full.js | 118 ++++++++++++++++++ src/lib/polygon/PolygonContractABIs.js | 7 ++ src/lib/polygon/PolygonKey.js | 49 ++++++++ .../SignPolygonTransaction.js | 39 +++++- .../SignPolygonTransactionApi.js | 46 ++++++- types/Keyguard.d.ts | 15 +++ 10 files changed, 284 insertions(+), 7 deletions(-) diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index cda4faee5..5ff9ed9ec 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -226,6 +226,14 @@ export type PolygonTransactionInfo = { approval?: { tokenNonce: number, }, + + /** + * The sender's nonce in the token contract, required when calling the + * contract function `transferWithPermit`. + */ + permit?: { + tokenNonce: number, + }, }; export type SignPolygonTransactionRequest = Omit & PolygonTransactionInfo & { diff --git a/src/config/config.local.js b/src/config/config.local.js index e8f29e1de..4eee1fb41 100644 --- a/src/config/config.local.js +++ b/src/config/config.local.js @@ -20,4 +20,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23', USDC_TRANSFER_CONTRACT_ADDRESS: '0x2805f3187dcDfa424EFA8c55Db6012Cf08Fa6eEc', // v3 USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', + + NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97', + NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1 }; diff --git a/src/config/config.mainnet.js b/src/config/config.mainnet.js index 00260aa77..2deaa0f16 100644 --- a/src/config/config.mainnet.js +++ b/src/config/config.mainnet.js @@ -11,4 +11,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_CONTRACT_ADDRESS: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', USDC_TRANSFER_CONTRACT_ADDRESS: '0x98E69a6927747339d5E543586FC0262112eBe4BD', USDC_HTLC_CONTRACT_ADDRESS: '0xF615bD7EA00C4Cc7F39Faad0895dB5f40891359f', + + NATIVE_USDC_CONTRACT_ADDRESS: '', + NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '', // v1 }; diff --git a/src/config/config.testnet.js b/src/config/config.testnet.js index a3715f712..cf13e7271 100644 --- a/src/config/config.testnet.js +++ b/src/config/config.testnet.js @@ -11,4 +11,7 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_CONTRACT_ADDRESS: '0x0FA8781a83E46826621b3BC094Ea2A0212e71B23', USDC_TRANSFER_CONTRACT_ADDRESS: '0x2805f3187dcDfa424EFA8c55Db6012Cf08Fa6eEc', USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', + + NATIVE_USDC_CONTRACT_ADDRESS: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97', + NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS: '0x5D101A320547f8D640c44fDfe5d1f35224f00B8B', // v1 }; diff --git a/src/lib/polygon/PolygonContractABIs.full.js b/src/lib/polygon/PolygonContractABIs.full.js index 3266a464c..0f731aa89 100644 --- a/src/lib/polygon/PolygonContractABIs.full.js +++ b/src/lib/polygon/PolygonContractABIs.full.js @@ -177,5 +177,123 @@ const PolygonContractABIsFull = { // eslint-disable-line no-unused-vars 'function withdrawToken(address token, uint256 amount, address target)', 'function wrappedChainToken() view returns (address)', ], + + NATIVE_USDC_CONTRACT_ABI: [ + 'event Approval(address indexed owner, address indexed spender, uint256 value)', + 'event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce)', + 'event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)', + 'event Blacklisted(address indexed _account)', + 'event BlacklisterChanged(address indexed newBlacklister)', + 'event Burn(address indexed burner, uint256 amount)', + 'event MasterMinterChanged(address indexed newMasterMinter)', + 'event Mint(address indexed minter, address indexed to, uint256 amount)', + 'event MinterConfigured(address indexed minter, uint256 minterAllowedAmount)', + 'event MinterRemoved(address indexed oldMinter)', + 'event OwnershipTransferred(address previousOwner, address newOwner)', + 'event Pause()', + 'event PauserChanged(address indexed newAddress)', + 'event RescuerChanged(address indexed newRescuer)', + 'event Transfer(address indexed from, address indexed to, uint256 value)', + 'event UnBlacklisted(address indexed _account)', + 'event Unpause()', + 'function CANCEL_AUTHORIZATION_TYPEHASH() view returns (bytes32)', + 'function DOMAIN_SEPARATOR() view returns (bytes32)', + 'function PERMIT_TYPEHASH() view returns (bytes32)', + 'function RECEIVE_WITH_AUTHORIZATION_TYPEHASH() view returns (bytes32)', + 'function TRANSFER_WITH_AUTHORIZATION_TYPEHASH() view returns (bytes32)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'function approve(address spender, uint256 value) returns (bool)', + 'function authorizationState(address authorizer, bytes32 nonce) view returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function blacklist(address _account)', + 'function blacklister() view returns (address)', + 'function burn(uint256 _amount)', + 'function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)', + 'function configureMinter(address minter, uint256 minterAllowedAmount) returns (bool)', + 'function currency() view returns (string)', + 'function decimals() view returns (uint8)', + 'function decreaseAllowance(address spender, uint256 decrement) returns (bool)', + 'function increaseAllowance(address spender, uint256 increment) returns (bool)', + 'function initialize(string tokenName, string tokenSymbol, string tokenCurrency, uint8 tokenDecimals, address newMasterMinter, address newPauser, address newBlacklister, address newOwner)', + 'function initializeV2(string newName)', + 'function initializeV2_1(address lostAndFound)', + 'function isBlacklisted(address _account) view returns (bool)', + 'function isMinter(address account) view returns (bool)', + 'function masterMinter() view returns (address)', + 'function mint(address _to, uint256 _amount) returns (bool)', + 'function minterAllowance(address minter) view returns (uint256)', + 'function name() view returns (string)', + 'function nonces(address owner) view returns (uint256)', + 'function owner() view returns (address)', + 'function pause()', + 'function paused() view returns (bool)', + 'function pauser() view returns (address)', + 'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)', + 'function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)', + 'function removeMinter(address minter) returns (bool)', + 'function rescueERC20(address tokenContract, address to, uint256 amount)', + 'function rescuer() view returns (address)', + 'function symbol() view returns (string)', + 'function totalSupply() view returns (uint256)', + 'function transfer(address to, uint256 value) returns (bool)', + 'function transferFrom(address from, address to, uint256 value) returns (bool)', + 'function transferOwnership(address newOwner)', + 'function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)', + 'function unBlacklist(address _account)', + 'function unpause()', + 'function updateBlacklister(address _newBlacklister)', + 'function updateMasterMinter(address _newMasterMinter)', + 'function updatePauser(address _newPauser)', + 'function updateRescuer(address newRescuer)', + 'function version() view returns (string)', + ], + + NATIVE_USDC_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(tuple(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 (tuple(uint256 acceptanceBudget, uint256 preRelayedCallGasLimit, uint256 postRelayedCallGasLimit, uint256 calldataSizeLimit) limits)', + 'function getHubAddr() view returns (address)', + 'function getMinimumRelayFee(tuple(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(tuple(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, tuple(uint256 gasPrice, uint256 pctRelayFee, uint256 baseRelayFee, address relayWorker, address paymaster, address forwarder, bytes paymasterData, uint256 clientId) relayData)', + 'function preRelayedCall(tuple(tuple(address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data, uint256 validUntil) request, tuple(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(tuple(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 transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 value, 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(tuple(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)', + ], }; /* eslint-enable max-len */ diff --git a/src/lib/polygon/PolygonContractABIs.js b/src/lib/polygon/PolygonContractABIs.js index 5bfa71c81..30516a83c 100644 --- a/src/lib/polygon/PolygonContractABIs.js +++ b/src/lib/polygon/PolygonContractABIs.js @@ -16,5 +16,12 @@ const PolygonContractABIs = { // eslint-disable-line no-unused-vars 'function redeemWithSecretInData(bytes32 id, address target, uint256 fee)', 'function refund(bytes32 id, address target, uint256 fee)', ], + + NATIVE_USDC_CONTRACT_ABI: [], + + NATIVE_USDC_TRANSFER_CONTRACT_ABI: [ + 'function transfer(address token, uint256 amount, address target, uint256 fee)', + 'function transferWithPermit(address token, uint256 amount, address target, uint256 fee, uint256 value, 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 5297b3dff..93b15c82e 100644 --- a/src/lib/polygon/PolygonKey.js +++ b/src/lib/polygon/PolygonKey.js @@ -97,6 +97,55 @@ class PolygonKey { // eslint-disable-line no-unused-vars return { sigR, sigS, sigV }; } + /** + * @param {string} path + * @param {string} forwarderContractAddress + * @param {ethers.BigNumber} approvalAmount + * @param {number} tokenNonce + * @param {string} ownerAddress + * @returns {Promise<{sigR: string, sigS: string, sigV: number}>} + */ + async signUsdcPermit(path, forwarderContractAddress, approvalAmount, tokenNonce, ownerAddress) { + // TODO: Make the domain parameters configurable in the request? + const domain = { + name: 'USD Coin', // This is currently the same for testnet and mainnet + version: '2', // This is currently the same for testnet and mainnet + verifyingContract: CONFIG.NATIVE_USDC_CONTRACT_ADDRESS, + chainId: CONFIG.POLYGON_CHAIN_ID, + }; + + const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }; + + const message = { + owner: ownerAddress, + spender: forwarderContractAddress, + value: approvalAmount, + nonce: tokenNonce, + deadline: ethers.constants.MaxUint256, + }; + + const signature = await this.signTypedData( + path, + domain, + types, + message, + ); + + const sigR = signature.slice(0, 66); // 0x prefix plus 32 bytes = 66 characters + const sigS = `0x${signature.slice(66, 130)}`; // 32 bytes = 64 characters + const sigV = parseInt(signature.slice(130, 132), 16); // last byte = 2 characters + + return { sigR, sigS, sigV }; + } + /** * @param {string} path * @param {Uint8Array} message - A byte array diff --git a/src/request/sign-polygon-transaction/SignPolygonTransaction.js b/src/request/sign-polygon-transaction/SignPolygonTransaction.js index 93c5e4568..f86197079 100644 --- a/src/request/sign-polygon-transaction/SignPolygonTransaction.js +++ b/src/request/sign-polygon-transaction/SignPolygonTransaction.js @@ -117,14 +117,18 @@ class SignPolygonTransaction { const polygonKey = new PolygonKey(key); + let transferContract = ''; + if (request.description.name === 'transferWithApproval') { + transferContract = CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS; + const { sigR, sigS, sigV } = await polygonKey.signUsdcApproval( request.keyPath, new ethers.Contract( CONFIG.USDC_CONTRACT_ADDRESS, PolygonContractABIs.USDC_CONTRACT_ABI, ), - CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS, + transferContract, request.description.args.approval, // Has been validated to be defined when function called is `transferWithApproval` /** @type {{ tokenNonce: number }} */ (request.approval).tokenNonce, @@ -132,7 +136,7 @@ class SignPolygonTransaction { ); const usdcTransfer = new ethers.Contract( - CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS, + transferContract, PolygonContractABIs.USDC_TRANSFER_CONTRACT_ABI, ); @@ -148,11 +152,40 @@ class SignPolygonTransaction { ]); } + if (request.description.name === 'transferWithPermit') { + transferContract = CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS; + + const { sigR, sigS, sigV } = await polygonKey.signUsdcPermit( + request.keyPath, + transferContract, + request.description.args.value, + // Has been validated to be defined when function called is `transferWithPermit` + /** @type {{ tokenNonce: number }} */ (request.permit).tokenNonce, + request.request.from, + ); + + const nativeUsdcTransfer = new ethers.Contract( + transferContract, + PolygonContractABIs.NATIVE_USDC_TRANSFER_CONTRACT_ABI, + ); + + request.request.data = nativeUsdcTransfer.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 value */ request.description.args.value, + /* bytes32 sigR */ sigR, + /* bytes32 sigS */ sigS, + /* uint8 sigV */ sigV, + ]); + } + const typedData = new OpenGSN.TypedRequestData( CONFIG.POLYGON_CHAIN_ID, request.description.name === 'refund' ? CONFIG.USDC_HTLC_CONTRACT_ADDRESS - : CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS, + : transferContract, { request: request.request, relayData: request.relayData, diff --git a/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js b/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js index af4db928b..c158e07c7 100644 --- a/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js +++ b/src/request/sign-polygon-transaction/SignPolygonTransactionApi.js @@ -24,7 +24,7 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { parsedRequest.keyPath = this.parsePolygonPath(request.keyPath, 'keyPath'); [parsedRequest.request, parsedRequest.description] = this.parseOpenGsnForwardRequest( request, - ['transfer', 'transferWithApproval', 'refund'], + ['transfer', 'transferWithApproval', 'transferWithPermit', 'refund'], ); parsedRequest.relayData = this.parseOpenGsnRelayData(request.relayData); parsedRequest.senderLabel = this.parseLabel(request.senderLabel); // Used for HTLC refunds @@ -41,6 +41,15 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { ), }; } + if (request.permit !== undefined) { + parsedRequest.permit = { + tokenNonce: this.parsePositiveInteger( + request.permit.tokenNonce, + true, + 'permit.tokenNonce', + ), + }; + } return parsedRequest; } @@ -49,16 +58,24 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { /** * * @param {KeyguardRequest.PolygonTransactionInfo} request - * @param {Array<'transfer' | 'transferWithApproval' | 'refund'>} allowedMethods + * @param {Array<'transfer' | 'transferWithApproval' | 'transferWithPermit' | 'refund'>} allowedMethods * @returns {[ * KeyguardRequest.OpenGsnForwardRequest, - * PolygonTransferDescription | PolygonTransferWithApprovalDescription | PolygonRefundDescription, + * PolygonTransferDescription + * | PolygonTransferWithApprovalDescription + * | PolygonTransferWithPermitDescription + * | PolygonRefundDescription, * ]} */ parseOpenGsnForwardRequest(request, allowedMethods) { const forwardRequest = this.parseOpenGsnForwardRequestRoot(request.request); - /** @type {PolygonTransferDescription | PolygonTransferWithApprovalDescription | PolygonRefundDescription} */ + /** + * @type {PolygonTransferDescription + * | PolygonTransferWithApprovalDescription + * | PolygonTransferWithPermitDescription + * | PolygonRefundDescription} + */ let description; if (forwardRequest.to === CONFIG.USDC_TRANSFER_CONTRACT_ADDRESS) { @@ -76,6 +93,21 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { if (description.args.token !== CONFIG.USDC_CONTRACT_ADDRESS) { throw new Errors.InvalidRequestError('Invalid USDC token contract in request data'); } + } else if (forwardRequest.to === CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS) { + const nativeUsdcTransferContract = new ethers.Contract( + CONFIG.NATIVE_USDC_TRANSFER_CONTRACT_ADDRESS, + PolygonContractABIs.NATIVE_USDC_TRANSFER_CONTRACT_ABI, + ); + + /** @type {PolygonTransferDescription | PolygonTransferWithPermitDescription} */ + description = (nativeUsdcTransferContract.interface.parseTransaction({ + data: forwardRequest.data, + value: forwardRequest.value, + })); + + if (description.args.token !== CONFIG.NATIVE_USDC_CONTRACT_ADDRESS) { + throw new Errors.InvalidRequestError('Invalid native USDC token contract in request data'); + } } else if (forwardRequest.to === CONFIG.USDC_HTLC_CONTRACT_ADDRESS) { const usdcHtlcContract = new ethers.Contract( CONFIG.USDC_HTLC_CONTRACT_ADDRESS, @@ -106,6 +138,12 @@ class SignPolygonTransactionApi extends PolygonRequestParserMixin(TopLevelApi) { + '"transferWithApproval"'); } + // Check that permit object exists when method is 'transferWithPermit', and unset for other methods. + if ((description.name === 'transferWithPermit') !== !!request.permit) { + throw new Errors.InvalidRequestError('`permit` object is only allowed for contract method ' + + '"transferWithPermit"'); + } + return [forwardRequest, description]; } diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 3959d1e3f..dde5d8a0a 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -62,6 +62,13 @@ interface PolygonUsdcApproval { readonly sigV: ethers.BigNumber, } +interface PolygonUsdcPermit { + readonly value: ethers.BigNumber, // amount to be approved + readonly sigR: string, + readonly sigS: string, + readonly sigV: ethers.BigNumber, +} + interface PolygonTransferArgs extends ReadonlyArray { readonly token: string, readonly amount: ethers.BigNumber, @@ -81,6 +88,13 @@ type PolygonTransferWithApprovalDescription = ethers.utils.TransactionDescriptio readonly args: PolygonTransferWithApprovalArgs, }; +interface PolygonTransferWithPermitArgs extends PolygonTransferArgs, PolygonUsdcPermit {} + +type PolygonTransferWithPermitDescription = ethers.utils.TransactionDescription & { + readonly name: 'transferWithPermit', + readonly args: PolygonTransferWithPermitArgs, +}; + interface PolygonOpenArgs extends ReadonlyArray { readonly id: string, readonly token: string, @@ -279,6 +293,7 @@ type Parsed = KeyId2KeyInfo & { description: PolygonTransferDescription | PolygonTransferWithApprovalDescription + | PolygonTransferWithPermitDescription | PolygonRefundDescription } : T extends Is ? KeyId2KeyInfo>