Publish sdk (#9)

* cleanups, comments
* rename "erc4337/common" to "account-abstraction/utils"
* added debug messages, comments, renamed newProvider to wrapProvider
This commit is contained in:
Dror Tirosh
2022-10-05 21:11:49 +03:00
committed by GitHub
parent b5cebc3630
commit 9cfced946a
37 changed files with 193 additions and 71 deletions

5
.gitignore vendored
View File

@@ -6,20 +6,17 @@ typechain
packages/bundler/src/typechain-types
cache
artifacts
/packages/hardhat/types/
.DS_Store
/.idea/bundler.iml
/.idea/modules.xml
/packages/bundler/tsconfig.tsbuildinfo
/packages/hardhat/deployments/
tsconfig.tsbuildinfo
/packages/common/src/types/
/packages/utils/src/types/
/.idea/codeStyles/Project.xml
/.idea/codeStyles/codeStyleConfig.xml
/.idea/inspectionProfiles/Project_Default.xml
/packages/bundler/typechain-types/
/packages/bundler/deployments/
**/dist/
/packages/aactf/src/types/
/packages/bundler/src/types/
yarn-error.log

View File

@@ -9,7 +9,6 @@ A basic eip4337 "bundler"
- eth_supportedEntryPoints to report the bundler's supported entry points
- eth_chainId
usage:
1. start hardhat-node with `yarn hardhat-node` or geth
In another Window:
@@ -24,5 +23,12 @@ In another Window:
- sends a transaction (which also creates the wallet)
- sends another transaction, on this existing wallet
- (uses account[0] or mnemonic file for funding, and creating deployer if needed)
```
## sdk
SDK to create and send UserOperations
see [SDK Readme](./packages/sdk/README.md)
## utils
internal utility methods/test contracts, used by other packages.

View File

@@ -1,13 +1,7 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// TODO: get hardhat types from '@account-abstraction' and '@erc43337/common' package directly
// only to import the file in hardhat compilation
import "@erc4337/common/contracts/test/SampleRecipient.sol";
import "@erc4337/common/contracts/test/SingletonFactory.sol";
// import contracts to get their type info.
import "@account-abstraction/utils/contracts/test/SampleRecipient.sol";
import "@account-abstraction/utils/contracts/test/SingletonFactory.sol";
import "@account-abstraction/contracts/samples/SimpleWalletDeployer.sol";
contract Import {
SampleRecipient sampleRecipient;
SingletonFactory singletonFactory;
}

View File

@@ -2,6 +2,7 @@
"name": "@account-abstraction/bundler",
"version": "0.2.0",
"license": "MIT",
"private": true,
"files": [
"dist/src/",
"dist/index.js",
@@ -24,7 +25,7 @@
},
"dependencies": {
"@account-abstraction/contracts": "^0.2.0",
"@erc4337/common": "0.2.0",
"@account-abstraction/utils": "0.2.0",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@types/cors": "^2.8.12",

View File

@@ -6,7 +6,7 @@ import { Provider } from '@ethersproject/providers'
import { Wallet, utils } from 'ethers'
import { hexlify, parseEther } from 'ethers/lib/utils'
import { erc4337RuntimeVersion } from '@erc4337/common'
import { erc4337RuntimeVersion } from '@account-abstraction/utils'
import { BundlerConfig } from './BundlerConfig'
import { UserOpMethodHandler } from './UserOpMethodHandler'

View File

@@ -5,9 +5,9 @@ import { BundlerConfig } from './BundlerConfig'
import { EntryPoint } from './types'
import { UserOperationStruct } from './types/contracts/BundlerHelper'
import { hexValue, resolveProperties } from 'ethers/lib/utils'
import { rethrowError } from '@erc4337/common'
import { rethrowError } from '@account-abstraction/utils'
import { calcPreVerificationGas } from '@account-abstraction/sdk/dist/src/calcPreVerificationGas'
import { postExecutionDump } from '@erc4337/common/dist/src/postExecCheck'
import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck'
export class UserOpMethodHandler {
constructor (

View File

@@ -2,7 +2,7 @@ import ow from 'ow'
import fs from 'fs'
import { Command } from 'commander'
import { erc4337RuntimeVersion } from '@erc4337/common'
import { erc4337RuntimeVersion } from '@account-abstraction/utils'
import { ethers, Wallet } from 'ethers'
import { BaseProvider } from '@ethersproject/providers'

View File

@@ -10,7 +10,7 @@ import { JsonRpcProvider } from '@ethersproject/providers'
import { SimpleWalletDeployer__factory } from '@account-abstraction/contracts'
import { formatEther, keccak256, parseEther } from 'ethers/lib/utils'
import { Command } from 'commander'
import { erc4337RuntimeVersion } from '@erc4337/common'
import { erc4337RuntimeVersion } from '@account-abstraction/utils'
import fs from 'fs'
import { HttpRpcClient } from '@account-abstraction/sdk/dist/src/HttpRpcClient'
import { SimpleWalletAPI } from '@account-abstraction/sdk'

View File

@@ -4,10 +4,10 @@ import hre, { ethers } from 'hardhat'
import sinon from 'sinon'
import * as SampleRecipientArtifact
from '@erc4337/common/artifacts/contracts/test/SampleRecipient.sol/SampleRecipient.json'
from '@account-abstraction/utils/artifacts/contracts/test/SampleRecipient.sol/SampleRecipient.json'
import { BundlerConfig } from '../src/BundlerConfig'
import { ERC4337EthersProvider, ERC4337EthersSigner, ClientConfig, newProvider } from '@account-abstraction/sdk'
import { ERC4337EthersProvider, ERC4337EthersSigner, ClientConfig, wrapProvider } from '@account-abstraction/sdk'
import { Signer, Wallet } from 'ethers'
import { runBundler } from '../src/runBundler'
import { BundlerServer } from '../src/BundlerServer'
@@ -86,7 +86,7 @@ describe('Flow', function () {
// use this as signer (instead of node's first account)
const ownerAccount = Wallet.createRandom()
erc4337Provider = await newProvider(
erc4337Provider = await wrapProvider(
ethers.provider,
// new JsonRpcProvider('http://localhost:8545/'),
config,

View File

@@ -17,7 +17,7 @@ import {
import { SimpleWalletAPI } from '@account-abstraction/sdk'
import { DeterministicDeployer } from '@account-abstraction/sdk/src/DeterministicDeployer'
import { Wallet } from 'ethers'
import { postExecutionDump } from '@erc4337/common/dist/src/postExecCheck'
import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck'
describe('UserOpMethodHandler', function () {
const helloWorld = 'hello world'

View File

@@ -1,11 +0,0 @@
// define the same export types as used by export typechain/ethers
import { BigNumberish } from 'ethers'
import { BytesLike } from '@ethersproject/bytes'
export type address = string
export type uint256 = BigNumberish
export type uint64 = BigNumberish
export type bytes = BytesLike
export type bytes32 = BytesLike

61
packages/sdk/README.md Normal file
View File

@@ -0,0 +1,61 @@
# SDK to create and send UserOperation
This package provides 2 APIs for using UserOperations:
- Low-level "walletAPI"
- High-level Provider
## LowLevel API:
### BaseWalletAPI
An abstract base-class to create UserOperation for a contract wallet.
### SimpleWalletAPI
An implementation of the BaseWalletAPi, for the SimpleWallet sample of account-abstraction.
```typescript
owner = provider.getSigner()
const walletAPI = new SimpleWalletAPI({
provider,
entryPointAddress,
owner,
factoryAddress
})
const op = await walletAPi.createSignedUserOp({
target: recipient.address,
data: recipient.interface.encodeFunctionData('something', ['hello'])
})
```
## High-Level Provider API
A simplified mode that doesn't require a different wallet extension.
Instead, the current provider's account is used as wallet owner by calling its "Sign Message" operation.
This can only work for wallets that use an EIP-191 ("Ethereum Signed Message") signatures (like our sample SimpleWallet)
Also, the UX is not great (the user is asked to sign a hash, and even the wallet address is not mentioned, only the signer)
```typescript
import { wrapProvider } from '@account-abstraction/sdk'
//use this account as wallet-owner (which will be used to sign the requests)
const signer = provider.getSigner()
const config = {
chainId: await provider.getNetwork().then(net => net.chainId),
entryPointAddress,
bundlerUrl: 'http://localhost:3000/rpc'
}
const aaProvider = await wrapProvider(provider, config, aasigner)
const walletAddress = await aaProvider.getSigner().getAddress()
// send some eth to the wallet Address: wallet should have some balance to pay for its own creation, and for calling methods.
const myContract = new Contract(abi, aaProvider)
// this method will get called from the wallet address, through account-abstraction EntryPoint
await myContract.someMethod()
```

View File

@@ -4,7 +4,7 @@
"main": "./dist/src/index.js",
"license": "MIT",
"files": [
"dist/*",
"dist/src",
"README.md"
],
"scripts": {
@@ -18,12 +18,14 @@
},
"dependencies": {
"@account-abstraction/contracts": "^0.2.0",
"@erc4337/common": "0.2.0",
"@account-abstraction/utils": "0.2.0",
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/networks": "^5.7.0",
"@ethersproject/properties": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@types/debug": "^4.1.7",
"debug": "^4.3.4",
"ethers": "^5.7.0"
},
"devDependencies": {

View File

@@ -8,7 +8,7 @@ import {
import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp'
import { resolveProperties } from 'ethers/lib/utils'
import { PaymasterAPI } from './PaymasterAPI'
import { getRequestId, NotPromise, packUserOp } from '@erc4337/common'
import { getRequestId, NotPromise, packUserOp } from '@account-abstraction/utils'
import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas'
export interface BaseApiParams {
@@ -60,6 +60,10 @@ export abstract class BaseWalletAPI {
}
async init (): Promise<this> {
if (await this.provider.getCode(this.entryPointAddress) === '0x') {
throw new Error(`entryPoint not deployed at ${this.entryPointAddress}`)
}
await this.getWalletAddress()
return this
}

View File

@@ -1,6 +1,27 @@
/**
* configuration params for wrapProvider
*/
export interface ClientConfig {
paymasterAddress?: string
/**
* the entry point to use
*/
entryPointAddress: string
/**
* url to the bundler
*/
bundlerUrl: string
/**
* chainId of current network. used to validate against the bundler's chainId
*/
chainId: number
/**
* if set, use this pre-deployed wallet.
* (if not set, use getSigner().getAddress() to query the "counterfactual" address of wallet.
* you may need to fund this address so the wallet can pay for its own creation)
*/
walletAddres?: string
/**
* if set, use this paymaster
*/
paymasterAddress?: string
}

View File

@@ -8,8 +8,10 @@ import { ERC4337EthersSigner } from './ERC4337EthersSigner'
import { UserOperationEventListener } from './UserOperationEventListener'
import { HttpRpcClient } from './HttpRpcClient'
import { EntryPoint, UserOperationStruct } from '@account-abstraction/contracts'
import { getRequestId } from '@erc4337/common'
import { getRequestId } from '@account-abstraction/utils'
import { BaseWalletAPI } from './BaseWalletAPI'
import Debug from 'debug'
const debug = Debug('aa.provider')
export class ERC4337EthersProvider extends BaseProvider {
initializedBlockNumber!: number
@@ -43,6 +45,7 @@ export class ERC4337EthersProvider extends BaseProvider {
}
async perform (method: string, params: any): Promise<any> {
debug('perform', method, params)
if (method === 'sendTransaction' || method === 'getTransactionReceipt') {
// TODO: do we need 'perform' method to be available at all?
// there is nobody out there to use it for ERC-4337 methods yet, we have nothing to override in fact.

View File

@@ -21,6 +21,8 @@ export class ERC4337EthersSigner extends Signer {
defineReadOnly(this, 'provider', erc4337provider)
}
address?: string
// This one is called by Contract. It signs the request and passes in to Provider to be sent.
async sendTransaction (transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
const tx: TransactionRequest = await this.populateTransaction(transaction)
@@ -78,7 +80,10 @@ export class ERC4337EthersSigner extends Signer {
}
async getAddress (): Promise<string> {
return await this.erc4337provider.getSenderWalletAddress()
if (this.address == null) {
this.address = await this.erc4337provider.getSenderWalletAddress()
}
return this.address
}
async signMessage (message: Bytes | string): Promise<string> {

View File

@@ -1,8 +1,10 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { ethers } from 'ethers'
import { hexValue, resolveProperties } from 'ethers/lib/utils'
import { UserOperationStruct } from '@account-abstraction/contracts'
import Debug from 'debug'
const debug = Debug('aa.rpc')
export class HttpRpcClient {
private readonly userOpJsonRpcProvider: JsonRpcProvider
@@ -42,7 +44,10 @@ export class HttpRpcClient {
}
return [key, val]
})
.reduce((set, [k, v]) => ({ ...set, [k]: v }), {})
.reduce((set, [k, v]) => ({
...set,
[k]: v
}), {})
const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress]
await this.printUserOperation(jsonRequestData)
@@ -52,7 +57,7 @@ export class HttpRpcClient {
private async printUserOperation ([userOp1, entryPointAddress]: [UserOperationStruct, string]): Promise<void> {
const userOp = await resolveProperties(userOp1)
console.log('sending eth_sendUserOperation', {
debug('sending eth_sendUserOperation', {
...userOp
// initCode: (userOp.initCode ?? '').length,
// callData: (userOp.callData ?? '').length

View File

@@ -8,8 +8,17 @@ import { ERC4337EthersProvider } from './ERC4337EthersProvider'
import { HttpRpcClient } from './HttpRpcClient'
import { DeterministicDeployer } from './DeterministicDeployer'
import { Signer } from '@ethersproject/abstract-signer'
import Debug from 'debug'
export async function newProvider (
const debug = Debug('aa.wrapProvider')
/**
* wrap an existing provider to tunnel requests through Account Abstraction.
* @param originalProvider the normal provider
* @param config see ClientConfig for more info
* @param originalSigner use this signer as the owner. of this wallet. By default, use the provider's signer
*/
export async function wrapProvider (
originalProvider: JsonRpcProvider,
config: ClientConfig,
originalSigner: Signer = originalProvider.getSigner()
@@ -24,6 +33,7 @@ export async function newProvider (
factoryAddress: simpleWalletDeployer
})
const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, 31337)
debug('config=', config)
return await new ERC4337EthersProvider(
config,
originalSigner,

View File

@@ -2,6 +2,9 @@ import { BigNumberish, Event } from 'ethers'
import { TransactionReceipt } from '@ethersproject/providers'
import { EntryPoint } from '@account-abstraction/contracts'
import { defaultAbiCoder } from 'ethers/lib/utils'
import Debug from 'debug'
const debug = Debug('aa.listener')
const DEFAULT_TRANSACTION_TIMEOUT: number = 10000
@@ -64,7 +67,7 @@ export class UserOperationEventListener {
const transactionReceipt = await event.getTransactionReceipt()
transactionReceipt.transactionHash = this.requestId
console.log('got event with status=', event.args.success, 'gasUsed=', transactionReceipt.gasUsed)
debug('got event with status=', event.args.success, 'gasUsed=', transactionReceipt.gasUsed)
// before returning the receipt, update the status from the event.
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions

View File

@@ -1,5 +1,5 @@
import { UserOperationStruct } from '@account-abstraction/contracts'
import { NotPromise, packUserOp } from '@erc4337/common'
import { NotPromise, packUserOp } from '@account-abstraction/utils'
import { arrayify, hexlify } from 'ethers/lib/utils'
export interface GasOverheads {

View File

@@ -1,6 +1,6 @@
export { SimpleWalletAPI } from './SimpleWalletAPI'
export { PaymasterAPI } from './PaymasterAPI'
export { newProvider } from './Provider'
export { wrapProvider } from './Provider'
export { ERC4337EthersSigner } from './ERC4337EthersSigner'
export { ERC4337EthersProvider } from './ERC4337EthersProvider'
export { ClientConfig } from './ClientConfig'

View File

@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { SampleRecipient__factory } from '@erc4337/common/dist/src/types'
import { SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types'
import { ethers } from 'hardhat'
import { hexValue } from 'ethers/lib/utils'
import { DeterministicDeployer } from '../src/DeterministicDeployer'

View File

@@ -10,9 +10,9 @@ import { expect } from 'chai'
import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'
import { ethers } from 'hardhat'
import { SimpleWalletAPI } from '../src'
import { SampleRecipient, SampleRecipient__factory } from '@erc4337/common/dist/src/types'
import { SampleRecipient, SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types'
import { DeterministicDeployer } from '../src/DeterministicDeployer'
import { rethrowError } from '@erc4337/common'
import { rethrowError } from '@account-abstraction/utils'
const provider = ethers.provider
const signer = provider.getSigner()

View File

@@ -1,6 +1,6 @@
import { SampleRecipient, SampleRecipient__factory } from '@erc4337/common/dist/src/types'
import { SampleRecipient, SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types'
import { ethers } from 'hardhat'
import { ClientConfig, ERC4337EthersProvider, newProvider } from '../src'
import { ClientConfig, ERC4337EthersProvider, wrapProvider } from '../src'
import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts'
import { expect } from 'chai'
import { parseEther } from 'ethers/lib/utils'
@@ -23,7 +23,7 @@ describe('ERC4337EthersSigner, Provider', function () {
bundlerUrl: ''
}
const aasigner = Wallet.createRandom()
aaProvider = await newProvider(provider, config, aasigner)
aaProvider = await wrapProvider(provider, config, aasigner)
const beneficiary = provider.getSigner().getAddress()
// for testing: bypass sending through a bundler, and send directly to our entrypoint..

4
packages/utils/README.md Normal file
View File

@@ -0,0 +1,4 @@
account-abstraction utils
methods for processing UserOperations

View File

@@ -15,8 +15,8 @@ contract SampleRecipient {
emit Sender(tx.origin, msg.sender, message);
}
// solhint-disable-next-line
function reverting() public {
(this); // make it non-pure..
revert( "test revert");
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "@erc4337/common",
"name": "@account-abstraction/utils",
"version": "0.2.0",
"main": "./dist/src/index.js",
"license": "MIT",
@@ -18,7 +18,6 @@
"dependencies": {
"@account-abstraction/contracts": "^0.2.0",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@openzeppelin/contracts": "^4.7.3",
"ethers": "^5.7.0"

View File

@@ -1,4 +1,4 @@
import { arrayify, defaultAbiCoder, hexConcat, keccak256 } from 'ethers/lib/utils'
import { defaultAbiCoder, hexConcat, keccak256 } from 'ethers/lib/utils'
import { UserOperationStruct } from '@account-abstraction/contracts'
import { abi as entryPointAbi } from '@account-abstraction/contracts/artifacts/IEntryPoint.json'
import { ethers } from 'ethers'
@@ -17,6 +17,12 @@ function encode (typevalues: Array<{ type: string, val: any }>, forSignature: bo
return defaultAbiCoder.encode(types, values)
}
/**
* pack the userOperation
* @param op
* @param forSignature "true" if the hash is needed to calculate the getRequestId()
* "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain.
*/
export function packUserOp (op: NotPromise<UserOperationStruct>, forSignature = true): string {
if (forSignature) {
// lighter signature scheme (must match UserOperation#pack): do encode a zero-length signature, but strip afterwards the appended zero-length value
@@ -138,6 +144,15 @@ export function packUserOp (op: NotPromise<UserOperationStruct>, forSignature =
return encode(typevalues, forSignature)
}
/**
* calculate the requestId of a given userOperation.
* The requestId is a hash of all UserOperation fields, except the "signature" field.
* The entryPoint uses this value in the emitted UserOperationEvent.
* A wallet may use this value as the hash to sign (the SampleWallet uses this method)
* @param op
* @param entryPoint
* @param chainId
*/
export function getRequestId (op: NotPromise<UserOperationStruct>, entryPoint: string, chainId: number): string {
const userOpHash = keccak256(packUserOp(op, true))
const enc = defaultAbiCoder.encode(
@@ -146,10 +161,6 @@ export function getRequestId (op: NotPromise<UserOperationStruct>, entryPoint: s
return keccak256(enc)
}
export function getRequestIdForSigning (op: NotPromise<UserOperationStruct>, entryPoint: string, chainId: number): Uint8Array {
return arrayify(getRequestId(op, entryPoint, chainId))
}
const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10) // 0x08c379a0
const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,address,string)')).slice(0, 10) // 0x00fa072b
@@ -159,11 +170,6 @@ interface DecodedError {
paymaster?: string
}
function dump<T> (x: T): T {
console.log('dump=', x)
return x
}
/**
* decode bytes thrown by revert as Error(message) or FailedOp(opIndex,paymaster,message)
*/
@@ -171,7 +177,7 @@ export function decodeErrorReason (error: string): DecodedError | undefined {
console.log('decoding', error)
if (error.startsWith(ErrorSig)) {
const [message] = defaultAbiCoder.decode(['string'], '0x' + error.substring(10))
return dump({ message })
return { message }
} else if (error.startsWith(FailedOpSig)) {
let [opIndex, paymaster, message] = defaultAbiCoder.decode(['uint256', 'address', 'string'], '0x' + error.substring(10))
message = `FailedOp: ${message as string}`
@@ -180,11 +186,11 @@ export function decodeErrorReason (error: string): DecodedError | undefined {
} else {
paymaster = undefined
}
return dump({
return {
message,
opIndex,
paymaster
})
}
}
}

View File

@@ -2102,6 +2102,13 @@
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
"@types/debug@^4.1.7":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
dependencies:
"@types/ms" "*"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -2214,6 +2221,11 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*", "@types/node@>=12.0.0":
version "18.7.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154"