Commit 414be841 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge branch 'develop' into inphi/proxyd-hdr

parents af40ffd8 1c6d89be
---
'@eth-optimism/core-utils': patch
---
Improved docstrings for BCFG typings
---
'@eth-optimism/integration-tests': minor
---
Updates to work with a live network
---
'@eth-optimism/batch-submitter-service': patch
---
Adds confirmation depth awareness to txmgr
---
'@eth-optimism/l2geth': patch
---
Add a better error message for when the sequencer url is not configured when proxying user requests to the sequencer for `eth_sendRawTransaction` when running as a verifier/replica
---
'@eth-optimism/proxyd': minor
---
Add caching for block-dependent RPCs
---
'@eth-optimism/proxyd': minor
---
proxyd: Cache block-dependent RPCs
---
'@eth-optimism/integration-tests': patch
---
Use hardhat-ethers for importing factories in integration tests
---
'@eth-optimism/l2geth': patch
---
Add reinitialize-by-url command, add dump chain state command
---
'@eth-optimism/core-utils': patch
---
Cleans up the internal file and folder structure for the typings exported by core-utils
---
'@eth-optimism/integration-tests': patch
---
Split OVMMulticall.sol into Multicall.sol & OVMContext.sol
---
'@eth-optimism/batch-submitter-service': minor
---
Add multi-tx support, clear pending txs on startup
---
'@eth-optimism/l2geth': patch
---
Fix blocknumber monotonicity logging bug
---
'@eth-optimism/proxyd': minor
---
Add integration tests and batching
# @eth-optimism/batch-submitter-service # @eth-optimism/batch-submitter-service
## 0.1.0
### Minor Changes
- 356b7271: Add multi-tx support, clear pending txs on startup
### Patch Changes
- 85aa148d: Adds confirmation depth awareness to txmgr
## 0.0.2 ## 0.0.2
### Patch Changes ### Patch Changes
- d6e0de5a: Fix metrics server - d6e0de5a: Fix metrics server
{ {
"name": "@eth-optimism/batch-submitter-service", "name": "@eth-optimism/batch-submitter-service",
"version": "0.0.2", "version": "0.1.0",
"private": true, "private": true,
"devDependencies": {} "devDependencies": {}
} }
# @eth-optimism/proxyd # @eth-optimism/proxyd
## 3.6.0
### Minor Changes
- 096c5f20: proxyd: Allow cached RPCs to be evicted by redis
- 71d64834: Add caching for block-dependent RPCs
- fd2e1523: proxyd: Cache block-dependent RPCs
- 1760613c: Add integration tests and batching
## 3.5.0 ## 3.5.0
### Minor Changes ### Minor Changes
......
...@@ -2,6 +2,7 @@ package proxyd ...@@ -2,6 +2,7 @@ package proxyd
import ( import (
"context" "context"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang/snappy" "github.com/golang/snappy"
...@@ -16,6 +17,8 @@ type Cache interface { ...@@ -16,6 +17,8 @@ type Cache interface {
const ( const (
// assuming an average RPCRes size of 3 KB // assuming an average RPCRes size of 3 KB
memoryCacheLimit = 4096 memoryCacheLimit = 4096
// Set a large ttl to avoid expirations. However, a ttl must be set for volatile-lru to take effect.
redisTTL = 30 * 7 * 24 * time.Hour
) )
type cache struct { type cache struct {
...@@ -67,7 +70,7 @@ func (c *redisCache) Get(ctx context.Context, key string) (string, error) { ...@@ -67,7 +70,7 @@ func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
} }
func (c *redisCache) Put(ctx context.Context, key string, value string) error { func (c *redisCache) Put(ctx context.Context, key string, value string) error {
err := c.rdb.Set(ctx, key, value, 0).Err() err := c.rdb.SetEX(ctx, key, value, redisTTL).Err()
if err != nil { if err != nil {
RecordRedisError("CacheSet") RecordRedisError("CacheSet")
} }
......
{ {
"name": "@eth-optimism/proxyd", "name": "@eth-optimism/proxyd",
"version": "3.5.0", "version": "3.6.0",
"private": true, "private": true,
"dependencies": {} "dependencies": {}
} }
# @eth-optimism/integration-tests # @eth-optimism/integration-tests
## 0.5.0
### Minor Changes
- c1e923f9: Updates to work with a live network
### Patch Changes
- 968fb38d: Use hardhat-ethers for importing factories in integration tests
- a7fbafa8: Split OVMMulticall.sol into Multicall.sol & OVMContext.sol
## 0.4.2 ## 0.4.2
### Patch Changes ### Patch Changes
......
{ {
"private": true, "private": true,
"name": "@eth-optimism/integration-tests", "name": "@eth-optimism/integration-tests",
"version": "0.4.2", "version": "0.5.0",
"description": "[Optimism] Integration tests", "description": "[Optimism] Integration tests",
"scripts": { "scripts": {
"lint": "yarn lint:fix && yarn lint:check", "lint": "yarn lint:fix && yarn lint:check",
...@@ -28,9 +28,9 @@ ...@@ -28,9 +28,9 @@
"url": "https://github.com/ethereum-optimism/optimism.git" "url": "https://github.com/ethereum-optimism/optimism.git"
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/contracts": "0.5.8", "@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@eth-optimism/message-relayer": "0.2.12", "@eth-optimism/message-relayer": "0.2.13",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.4.5", "@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0", "@ethersproject/transactions": "^5.4.0",
......
# Changelog # Changelog
## 0.5.8
### Patch Changes
- 949916f8: Add a better error message for when the sequencer url is not configured when proxying user requests to the sequencer for `eth_sendRawTransaction` when running as a verifier/replica
- 300f79bf: Fix nonce issue
- ae96d784: Add reinitialize-by-url command, add dump chain state command
- c7569a16: Fix blocknumber monotonicity logging bug
## 0.5.7 ## 0.5.7
### Patch Changes ### Patch Changes
......
...@@ -208,14 +208,13 @@ func (st *StateTransition) buyGas() error { ...@@ -208,14 +208,13 @@ func (st *StateTransition) buyGas() error {
func (st *StateTransition) preCheck() error { func (st *StateTransition) preCheck() error {
// Make sure this transaction's nonce is correct. // Make sure this transaction's nonce is correct.
if st.msg.CheckNonce() { if st.msg.CheckNonce() {
if rcfg.UsingOVM {
if st.msg.QueueOrigin() == types.QueueOriginL1ToL2 {
return st.buyGas()
}
}
nonce := st.state.GetNonce(st.msg.From()) nonce := st.state.GetNonce(st.msg.From())
if nonce < st.msg.Nonce() { if nonce < st.msg.Nonce() {
if rcfg.UsingOVM {
// The nonce never increments for L1ToL2 txs
if st.msg.QueueOrigin() == types.QueueOriginL1ToL2 {
return st.buyGas()
}
}
return ErrNonceTooHigh return ErrNonceTooHigh
} else if nonce > st.msg.Nonce() { } else if nonce > st.msg.Nonce() {
return ErrNonceTooLow return ErrNonceTooLow
......
{ {
"name": "@eth-optimism/l2geth", "name": "@eth-optimism/l2geth",
"version": "0.5.7", "version": "0.5.8",
"private": true, "private": true,
"devDependencies": {} "devDependencies": {}
} }
...@@ -32,6 +32,10 @@ docker-compose \ ...@@ -32,6 +32,10 @@ docker-compose \
*Note*: This generates a large amount of log data which docker stores by default. See [Disk Usage](#disk-usage). *Note*: This generates a large amount of log data which docker stores by default. See [Disk Usage](#disk-usage).
Also note that Docker Desktop only allocates 2GB of memory by default, which isn't enough to run the docker-compose services reliably.
To allocate more memory, go to Settings > Resources in the Docker UI and use the slider to change the value (_4GB recommended_). Make sure to click Apply & Restart for the changes to take effect.
To start the stack with monitoring enabled, just add the metric composition file. To start the stack with monitoring enabled, just add the metric composition file.
``` ```
docker-compose \ docker-compose \
......
# Changelog # Changelog
## 0.4.14
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.4.13 ## 0.4.13
### Patch Changes ### Patch Changes
......
{ {
"private": true, "private": true,
"name": "@eth-optimism/batch-submitter", "name": "@eth-optimism/batch-submitter",
"version": "0.4.13", "version": "0.4.14",
"description": "[Optimism] Service for submitting transactions and transaction results", "description": "[Optimism] Service for submitting transactions and transaction results",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -34,8 +34,8 @@ ...@@ -34,8 +34,8 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8", "@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@eth-optimism/ynatm": "^0.2.2", "@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.4.1", "@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/providers": "^5.4.5", "@ethersproject/providers": "^5.4.5",
......
# Changelog # Changelog
## 0.5.9
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
## 0.5.8 ## 0.5.8
### Patch Changes ### Patch Changes
......
{ {
"name": "@eth-optimism/contracts", "name": "@eth-optimism/contracts",
"version": "0.5.8", "version": "0.5.9",
"description": "[Optimism] L1 and L2 smart contracts for Optimism", "description": "[Optimism] L1 and L2 smart contracts for Optimism",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
"url": "https://github.com/ethereum-optimism/optimism.git" "url": "https://github.com/ethereum-optimism/optimism.git"
}, },
"dependencies": { "dependencies": {
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.4.1", "@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1", "@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/hardware-wallets": "^5.4.0" "@ethersproject/hardware-wallets": "^5.4.0"
......
# @eth-optimism/core-utils # @eth-optimism/core-utils
## 0.7.4
### Patch Changes
- ba96a455: Improved docstrings for BCFG typings
- c3e85fef: Cleans up the internal file and folder structure for the typings exported by core-utils
## 0.7.3 ## 0.7.3
### Patch Changes ### Patch Changes
......
{ {
"name": "@eth-optimism/core-utils", "name": "@eth-optimism/core-utils",
"version": "0.7.3", "version": "0.7.4",
"description": "[Optimism] Core typescript utilities", "description": "[Optimism] Core typescript utilities",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
......
# data transport layer # data transport layer
## 0.5.12
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.5.11 ## 0.5.11
### Patch Changes ### Patch Changes
......
{ {
"private": true, "private": true,
"name": "@eth-optimism/data-transport-layer", "name": "@eth-optimism/data-transport-layer",
"version": "0.5.11", "version": "0.5.12",
"description": "[Optimism] Service for shuttling data from L1 into L2", "description": "[Optimism] Service for shuttling data from L1 into L2",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -37,8 +37,8 @@ ...@@ -37,8 +37,8 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8", "@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@ethersproject/providers": "^5.4.5", "@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0", "@ethersproject/transactions": "^5.4.0",
"@sentry/node": "^6.3.1", "@sentry/node": "^6.3.1",
......
# @eth-optimism/message-relayer # @eth-optimism/message-relayer
## 0.2.13
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.2.12 ## 0.2.12
### Patch Changes ### Patch Changes
......
{ {
"name": "@eth-optimism/message-relayer", "name": "@eth-optimism/message-relayer",
"version": "0.2.12", "version": "0.2.13",
"description": "[Optimism] Service for automatically relaying L2 to L1 transactions", "description": "[Optimism] Service for automatically relaying L2 to L1 transactions",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8", "@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@sentry/node": "^6.3.1", "@sentry/node": "^6.3.1",
"bcfg": "^0.1.6", "bcfg": "^0.1.6",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"@discoveryjs/json-ext": "^0.5.3", "@discoveryjs/json-ext": "^0.5.3",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abi": "^5.5.0", "@ethersproject/abi": "^5.5.0",
"@ethersproject/bignumber": "^5.5.0", "@ethersproject/bignumber": "^5.5.0",
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
"ethers": "^5.4.5", "ethers": "^5.4.5",
"lint-staged": "11.0.0", "lint-staged": "11.0.0",
"mocha": "^9.1.2", "mocha": "^9.1.2",
"node-fetch": "2.6.5", "node-fetch": "2.6.7",
"solc": "0.8.7-fixed", "solc": "0.8.7-fixed",
"ts-mocha": "^8.0.0", "ts-mocha": "^8.0.0",
"ts-node": "^10.0.0" "ts-node": "^10.0.0"
......
# @eth-optimism/replica-healthcheck # @eth-optimism/replica-healthcheck
## 0.3.4
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
## 0.3.3 ## 0.3.3
### Patch Changes ### Patch Changes
......
{ {
"private": true, "private": true,
"name": "@eth-optimism/replica-healthcheck", "name": "@eth-optimism/replica-healthcheck",
"version": "0.3.3", "version": "0.3.4",
"description": "[Optimism] Service for monitoring the health of replica nodes", "description": "[Optimism] Service for monitoring the health of replica nodes",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"ethers": "^5.4.5", "ethers": "^5.4.5",
"express": "^4.17.1", "express": "^4.17.1",
......
# @eth-optimism/sdk # @eth-optimism/sdk
## 0.0.5
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.0.4 ## 0.0.4
### Patch Changes ### Patch Changes
......
{ {
"name": "@eth-optimism/sdk", "name": "@eth-optimism/sdk",
"version": "0.0.4", "version": "0.0.5",
"description": "[Optimism] Tools for working with Optimism", "description": "[Optimism] Tools for working with Optimism",
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
...@@ -59,8 +59,8 @@ ...@@ -59,8 +59,8 @@
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, },
"dependencies": { "dependencies": {
"@eth-optimism/contracts": "0.5.8", "@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.3", "@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0", "@ethersproject/abstract-signer": "^5.5.0",
"ethers": "^5.5.2" "ethers": "^5.5.2"
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
L1ToL2Overrides,
MessageLike,
NumberLike,
MessageDirection,
} from './interfaces'
export class CrossChainMessenger implements ICrossChainMessenger {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
/**
* Creates a new CrossChainMessenger instance.
*
* @param opts Options for the messenger.
* @param opts.provider CrossChainProvider to use to send messages.
* @param opts.l1Signer Signer to use to send messages on L1.
* @param opts.l2Signer Signer to use to send messages on L2.
*/
constructor(opts: {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
}) {
this.provider = opts.provider
this.l1Signer = opts.l1Signer
this.l2Signer = opts.l2Signer
}
public async sendMessage(
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.l1Signer.sendTransaction(tx)
} else {
return this.l2Signer.sendTransaction(tx)
}
}
public async resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async finalizeMessage(
message: MessageLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async depositETH(
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async withdrawETH(
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
populateTransaction = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.sendMessage(
message.target,
message.message,
overrides?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message))
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
this.l2Signer
).populateTransaction.sendMessage(
message.target,
message.message,
0 // Gas limit goes unused when sending from L2 to L1
)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
}
estimateGas = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.l1Provider.estimateGas(tx)
} else {
return this.provider.l2Provider.estimateGas(tx)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
}
}
...@@ -12,11 +12,13 @@ import { ...@@ -12,11 +12,13 @@ import {
OEContracts, OEContracts,
OEContractsLike, OEContractsLike,
MessageLike, MessageLike,
MessageRequestLike,
TransactionLike, TransactionLike,
AddressLike, AddressLike,
NumberLike, NumberLike,
ProviderLike, ProviderLike,
CrossChainMessage, CrossChainMessage,
CrossChainMessageRequest,
MessageDirection, MessageDirection,
MessageStatus, MessageStatus,
TokenBridgeMessage, TokenBridgeMessage,
...@@ -24,6 +26,8 @@ import { ...@@ -24,6 +26,8 @@ import {
MessageReceiptStatus, MessageReceiptStatus,
CustomBridges, CustomBridges,
CustomBridgesLike, CustomBridgesLike,
StateRoot,
StateRootBatch,
} from './interfaces' } from './interfaces'
import { import {
toProvider, toProvider,
...@@ -50,6 +54,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -50,6 +54,7 @@ export class CrossChainProvider implements ICrossChainProvider {
* @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url. * @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain. * @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.contracts Optional contract address overrides. * @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/ */
constructor(opts: { constructor(opts: {
l1Provider: ProviderLike l1Provider: ProviderLike
...@@ -336,7 +341,44 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -336,7 +341,44 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> { public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
throw new Error('Not implemented') const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.FAILED_L1_TO_L2_MESSAGE
}
}
} else {
if (receipt === null) {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.READY_FOR_RELAY
}
}
}
} }
public async getMessageReceipt( public async getMessageReceipt(
...@@ -421,12 +463,21 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -421,12 +463,21 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
public async estimateL2MessageGasLimit( public async estimateL2MessageGasLimit(
message: MessageLike, message: MessageRequestLike,
opts?: { opts?: {
bufferPercent?: number bufferPercent?: number
from?: string
} }
): Promise<BigNumber> { ): Promise<BigNumber> {
const resolved = await this.toCrossChainMessage(message) let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
if ((message as CrossChainMessage).messageNonce === undefined) {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
from = opts?.from || (resolved as CrossChainMessage).sender
}
// L2 message gas estimation is only used for L1 => L2 messages. // L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) { if (resolved.direction === MessageDirection.L2_TO_L1) {
...@@ -434,7 +485,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -434,7 +485,7 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
const estimate = await this.l2Provider.estimateGas({ const estimate = await this.l2Provider.estimateGas({
from: resolved.sender, from,
to: resolved.target, to: resolved.target,
data: resolved.message, data: resolved.message,
}) })
...@@ -450,9 +501,162 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -450,9 +501,162 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented') throw new Error('Not implemented')
} }
public async estimateMessageWaitTimeBlocks( public async getChallengePeriodSeconds(): Promise<number> {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
public async getMessageStateRoot(
message: MessageLike message: MessageLike
): Promise<number> { ): Promise<StateRoot | null> {
throw new Error('Not implemented') const resolved = await this.toCrossChainMessage(message)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// We need the block number of the transaction that triggered the message so we can look up the
// state root batch that corresponds to that block number.
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Every block has exactly one transaction in it. Since there's a genesis block, the
// transaction index will always be one less than the block number.
const messageTxIndex = messageTxReceipt.blockNumber - 1
// Pull down the state root batch, we'll try to pick out the specific state root that
// corresponds to our message.
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
messageTxIndex
)
// No state root batch, no state root.
if (stateRootBatch === null) {
return null
}
// We have a state root batch, now we need to find the specific state root for our transaction.
// First we need to figure out the index of the state root within the batch we found. This is
// going to be the original transaction index offset by the total number of previous state
// roots.
const indexInBatch =
messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
// Just a sanity check.
if (stateRootBatch.stateRoots.length <= indexInBatch) {
// Should never happen!
throw new Error(`state root does not exist in batch`)
}
return {
blockNumber: stateRootBatch.blockNumber,
header: stateRootBatch.header,
stateRoot: stateRootBatch.stateRoots[indexInBatch],
}
}
public async getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<ethers.Event | null> {
const events = await this.contracts.l1.StateCommitmentChain.queryFilter(
this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
batchIndex
)
)
if (events.length === 0) {
return null
} else if (events.length > 1) {
// Should never happen!
throw new Error(`found more than one StateBatchAppended event`)
} else {
return events[0]
}
}
public async getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<ethers.Event | null> {
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await this.contracts.l1.StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, transactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
batchEvent = await this.getStateBatchAppendedEventByBatchIndex(
middleOfBounds
)
if (isEventHi(batchEvent, transactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, transactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
public async getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null> {
const stateBatchAppendedEvent =
await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex)
if (stateBatchAppendedEvent === null) {
return null
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
const [stateRoots] =
this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
stateBatchTransaction.data
)
return {
blockNumber: stateBatchAppendedEvent.blockNumber,
stateRoots,
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
}
} }
} }
export * from './interfaces' export * from './interfaces'
export * from './utils' export * from './utils'
export * from './cross-chain-provider' export * from './cross-chain-provider'
export * from './cross-chain-messenger'
import { Overrides, Signer } from 'ethers' import { Overrides, Signer, BigNumber } from 'ethers'
import { import {
TransactionRequest, TransactionRequest,
TransactionResponse, TransactionResponse,
...@@ -22,9 +22,14 @@ export interface ICrossChainMessenger { ...@@ -22,9 +22,14 @@ export interface ICrossChainMessenger {
provider: ICrossChainProvider provider: ICrossChainProvider
/** /**
* Signer that will carry out L1/L2 transactions. * Signer that will carry out L1 transactions.
*/ */
signer: Signer l1Signer: Signer
/**
* Signer that will carry out L2 transactions.
*/
l2Signer: Signer
/** /**
* Sends a given cross chain message. Where the message is sent depends on the direction attached * Sends a given cross chain message. Where the message is sent depends on the direction attached
...@@ -107,7 +112,7 @@ export interface ICrossChainMessenger { ...@@ -107,7 +112,7 @@ export interface ICrossChainMessenger {
sendMessage: ( sendMessage: (
message: CrossChainMessageRequest, message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides overrides?: L1ToL2Overrides
) => Promise<TransactionResponse> ) => Promise<TransactionRequest>
/** /**
* Generates a transaction that resends a given cross chain message. Only applies to L1 to L2 * Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
...@@ -178,7 +183,7 @@ export interface ICrossChainMessenger { ...@@ -178,7 +183,7 @@ export interface ICrossChainMessenger {
sendMessage: ( sendMessage: (
message: CrossChainMessageRequest, message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides overrides?: L1ToL2Overrides
) => Promise<TransactionResponse> ) => Promise<BigNumber>
/** /**
* Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages. * Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
...@@ -192,7 +197,7 @@ export interface ICrossChainMessenger { ...@@ -192,7 +197,7 @@ export interface ICrossChainMessenger {
message: MessageLike, message: MessageLike,
messageGasLimit: NumberLike, messageGasLimit: NumberLike,
overrides?: Overrides overrides?: Overrides
): Promise<TransactionRequest> ): Promise<BigNumber>
/** /**
* Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages. * Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
...@@ -204,7 +209,7 @@ export interface ICrossChainMessenger { ...@@ -204,7 +209,7 @@ export interface ICrossChainMessenger {
finalizeMessage( finalizeMessage(
message: MessageLike, message: MessageLike,
overrides?: Overrides overrides?: Overrides
): Promise<TransactionRequest> ): Promise<BigNumber>
/** /**
* Estimates gas required to deposit some ETH into the L2 chain. * Estimates gas required to deposit some ETH into the L2 chain.
...@@ -216,7 +221,7 @@ export interface ICrossChainMessenger { ...@@ -216,7 +221,7 @@ export interface ICrossChainMessenger {
depositETH( depositETH(
amount: NumberLike, amount: NumberLike,
overrides?: L1ToL2Overrides overrides?: L1ToL2Overrides
): Promise<TransactionRequest> ): Promise<BigNumber>
/** /**
* Estimates gas required to withdraw some ETH back to the L1 chain. * Estimates gas required to withdraw some ETH back to the L1 chain.
...@@ -225,9 +230,6 @@ export interface ICrossChainMessenger { ...@@ -225,9 +230,6 @@ export interface ICrossChainMessenger {
* @param overrides Optional transaction overrides. * @param overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens. * @returns Transaction that can be signed and executed to withdraw the tokens.
*/ */
withdrawETH( withdrawETH(amount: NumberLike, overrides?: Overrides): Promise<BigNumber>
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest>
} }
} }
import { BigNumber } from 'ethers' import { Event, BigNumber } from 'ethers'
import { Provider, BlockTag } from '@ethersproject/abstract-provider' import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import { import {
MessageLike, MessageLike,
MessageRequestLike,
TransactionLike, TransactionLike,
AddressLike, AddressLike,
NumberLike, NumberLike,
...@@ -13,6 +14,8 @@ import { ...@@ -13,6 +14,8 @@ import {
OEContracts, OEContracts,
MessageReceipt, MessageReceipt,
CustomBridges, CustomBridges,
StateRoot,
StateRootBatch,
} from './types' } from './types'
/** /**
...@@ -203,12 +206,14 @@ export interface ICrossChainProvider { ...@@ -203,12 +206,14 @@ export interface ICrossChainProvider {
* @param message Message get a gas estimate for. * @param message Message get a gas estimate for.
* @param opts Options object. * @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20. * @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
* @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit. * @returns Estimates L2 gas limit.
*/ */
estimateL2MessageGasLimit( estimateL2MessageGasLimit(
message: MessageLike, message: MessageRequestLike,
opts?: { opts?: {
bufferPercent?: number bufferPercent?: number
from?: string
} }
): Promise<BigNumber> ): Promise<BigNumber>
...@@ -224,13 +229,52 @@ export interface ICrossChainProvider { ...@@ -224,13 +229,52 @@ export interface ICrossChainProvider {
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number> estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/** /**
* Returns the estimated amount of time before the message can be executed (in L1 blocks). * Queries the current challenge period in seconds from the StateCommitmentChain.
* When this is a message being sent to L1, this will return the estimated time until the message
* will complete its challenge period. When this is a message being sent to L2, this will return
* the estimated amount of time until the message will be picked up and executed on L2.
* *
* @param message Message to estimate the time remaining for. * @returns Current challenge period in seconds.
* @returns Estimated amount of time remaining (in blocks) before the message can be executed. */
getChallengePeriodSeconds(): Promise<number>
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
* state root for the given message has not been published yet, this function returns null.
*
* @param message Message to find a state root for.
* @returns State root for the block in which the message was created.
*/
getMessageStateRoot(message: MessageLike): Promise<StateRoot | null>
/**
* Returns the StateBatchAppended event that was emitted when the batch with a given index was
* created. Returns null if no such event exists (the batch has not been submitted).
*
* @param batchIndex Index of the batch to find an event for.
* @returns StateBatchAppended event for the batch, or null if no such batch exists.
*/
getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<Event | null>
/**
* Returns the StateBatchAppended event for the batch that includes the transaction with the
* given index. Returns null if no such event exists.
*
* @param transactionIndex Index of the L2 transaction to find an event for.
* @returns StateBatchAppended event for the batch that includes the given transaction by index.
*/
getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<Event | null>
/**
* Returns information about the state root batch that included the state root for the given
* transaction by index. Returns null if no such state root has been published yet.
*
* @param transactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch for the given transaction index, or null if none exists yet.
*/ */
estimateMessageWaitTimeBlocks(message: MessageLike): Promise<number> getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null>
} }
...@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest { ...@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest {
direction: MessageDirection direction: MessageDirection
target: string target: string
message: string message: string
l2GasLimit: NumberLike
} }
/** /**
...@@ -212,9 +211,19 @@ export interface StateRootBatchHeader { ...@@ -212,9 +211,19 @@ export interface StateRootBatchHeader {
} }
/** /**
* State root batch, including header and actual state roots. * Information about a state root, including header, block number, and root iself.
*/
export interface StateRoot {
blockNumber: number
header: StateRootBatchHeader
stateRoot: string
}
/**
* Information about a batch of state roots.
*/ */
export interface StateRootBatch { export interface StateRootBatch {
blockNumber: number
header: StateRootBatchHeader header: StateRootBatchHeader
stateRoots: string[] stateRoots: string[]
} }
...@@ -225,7 +234,7 @@ export interface StateRootBatch { ...@@ -225,7 +234,7 @@ export interface StateRootBatch {
* limit field (gas used depends on the amount of gas provided). * limit field (gas used depends on the amount of gas provided).
*/ */
export type L1ToL2Overrides = Overrides & { export type L1ToL2Overrides = Overrides & {
l2GasLimit: NumberLike l2GasLimit?: NumberLike
} }
/** /**
...@@ -234,13 +243,22 @@ export type L1ToL2Overrides = Overrides & { ...@@ -234,13 +243,22 @@ export type L1ToL2Overrides = Overrides & {
export type TransactionLike = string | TransactionReceipt | TransactionResponse export type TransactionLike = string | TransactionReceipt | TransactionResponse
/** /**
* Stuff that can be coerced into a message. * Stuff that can be coerced into a CrossChainMessage.
*/ */
export type MessageLike = export type MessageLike =
| CrossChainMessage | CrossChainMessage
| TransactionLike | TransactionLike
| TokenBridgeMessage | TokenBridgeMessage
/**
* Stuff that can be coerced into a CrossChainMessageRequest.
*/
export type MessageRequestLike =
| CrossChainMessageRequest
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/** /**
* Stuff that can be coerced into a provider. * Stuff that can be coerced into a provider.
*/ */
......
...@@ -7,13 +7,22 @@ contract MockMessenger is ICrossDomainMessenger { ...@@ -7,13 +7,22 @@ contract MockMessenger is ICrossDomainMessenger {
return address(0); return address(0);
} }
uint256 public nonce;
// Empty function to satisfy the interface. // Empty function to satisfy the interface.
function sendMessage( function sendMessage(
address _target, address _target,
bytes calldata _message, bytes calldata _message,
uint32 _gasLimit uint32 _gasLimit
) public { ) public {
return; emit SentMessage(
_target,
msg.sender,
_message,
nonce,
_gasLimit
);
nonce++;
} }
struct SentMessageEventParams { struct SentMessageEventParams {
......
pragma solidity ^0.8.9;
contract MockSCC {
event StateBatchAppended(
uint256 indexed _batchIndex,
bytes32 _batchRoot,
uint256 _batchSize,
uint256 _prevTotalElements,
bytes _extraData
);
struct StateBatchAppendedArgs {
uint256 batchIndex;
bytes32 batchRoot;
uint256 batchSize;
uint256 prevTotalElements;
bytes extraData;
}
// Window in seconds, will resolve to 100 blocks.
uint256 public FRAUD_PROOF_WINDOW = 1500;
uint256 public batches = 0;
StateBatchAppendedArgs public sbaParams;
function getTotalBatches() public view returns (uint256) {
return batches;
}
function setSBAParams(
StateBatchAppendedArgs memory _args
) public {
sbaParams = _args;
}
function appendStateBatch(
bytes32[] memory _roots,
uint256 _shouldStartAtIndex
) public {
batches++;
emit StateBatchAppended(
sbaParams.batchIndex,
sbaParams.batchRoot,
sbaParams.batchSize,
sbaParams.prevTotalElements,
sbaParams.extraData
);
}
}
import './setup' import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from './setup'
import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
} from '../src'
describe('CrossChainMessenger', () => { describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
})
describe('sendMessage', () => { describe('sendMessage', () => {
describe('when no l2GasLimit is provided', () => { let l1Messenger: Contract
it('should send a message with an estimated l2GasLimit') let l2Messenger: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
describe('when the message is an L1 to L2 message', () => {
describe('when no l2GasLimit is provided', () => {
it('should send a message with an estimated l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
const estimate = await provider.estimateL2MessageGasLimit(message)
await expect(messenger.sendMessage(message))
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
estimate
)
})
})
describe('when an l2GasLimit is provided', () => {
it('should send a message with the provided l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(
messenger.sendMessage(message, {
l2GasLimit: 1234,
})
)
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
1234
)
})
})
}) })
describe('when an l2GasLimit is provided', () => { describe('when the message is an L2 to L1 message', () => {
it('should send a message with the provided l2GasLimit') it('should send a message', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(messenger.sendMessage(message))
.to.emit(l2Messenger, 'SentMessage')
.withArgs(
message.target,
await l2Signer.getAddress(),
message.message,
0,
0
)
})
}) })
}) })
......
export const DUMMY_MESSAGE = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
export * from './constants'
...@@ -11523,15 +11523,15 @@ node-fetch@*: ...@@ -11523,15 +11523,15 @@ node-fetch@*:
data-uri-to-buffer "^3.0.1" data-uri-to-buffer "^3.0.1"
fetch-blob "^3.1.2" fetch-blob "^3.1.2"
node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1: node-fetch@2.6.1:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@2.6.5: node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.5" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment