feat(express_relay): Sample protocol and monitor (#1374)
This commit is contained in:
parent
7796fbe025
commit
0735cdb975
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
lib/*
|
||||
out
|
||||
cache
|
||||
tslib
|
||||
!lib/README.md
|
|
@ -0,0 +1,20 @@
|
|||
# EasyLend Protocol
|
||||
|
||||
EasyLend is a simplified lending protocol that uses Express Relay for avoiding value leakage on liquidations.
|
||||
It uses Pyth price feeds to calculate the asset values and the liquidation thresholds.
|
||||
|
||||
This project illustrates how to use the Express Relay SDK for contract integration and publishing opportunities.
|
||||
|
||||
## Contracts
|
||||
|
||||
The contracts are located in the `contracts` directory. The `EasyLend.sol` file contains the main contract logic.
|
||||
The protocol can allow creation of undercollateralized vaults that are liquidatable upon creation. This is solely
|
||||
for ease of testing and demonstration purposes.
|
||||
|
||||
## Monitoring script
|
||||
|
||||
The script in `src/monitor.ts` is used to monitor the vaults health and publish the liquidation opportunities:
|
||||
|
||||
- It subscribes to Pyth price feeds to get the latest prices for the assets used in the protocol.
|
||||
- It periodically checks for new vaults using the chain rpc.
|
||||
- Upon finding a vault that is below the liquidation threshold, it publishes a liquidation opportunity using the Express Relay SDK.
|
|
@ -0,0 +1,358 @@
|
|||
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "./EasyLendStructs.sol";
|
||||
import "./EasyLendErrors.sol";
|
||||
import "forge-std/StdMath.sol";
|
||||
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/Strings.sol";
|
||||
|
||||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
||||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
||||
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol";
|
||||
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol";
|
||||
|
||||
contract EasyLend is IExpressRelayFeeReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey);
|
||||
|
||||
uint256 _nVaults;
|
||||
address public immutable expressRelay;
|
||||
mapping(uint256 => Vault) _vaults;
|
||||
address _oracle;
|
||||
bool _allowUndercollateralized;
|
||||
|
||||
/**
|
||||
* @notice EasyLend constructor - Initializes a new token vault contract with given parameters
|
||||
*
|
||||
* @param expressRelayAddress: address of the express relay
|
||||
* @param oracleAddress: address of the oracle contract
|
||||
* @param allowUndercollateralized: boolean to allow undercollateralized vaults to be created and updated. Can be set to true for testing.
|
||||
*/
|
||||
constructor(
|
||||
address expressRelayAddress,
|
||||
address oracleAddress,
|
||||
bool allowUndercollateralized
|
||||
) {
|
||||
_nVaults = 0;
|
||||
expressRelay = expressRelayAddress;
|
||||
_oracle = oracleAddress;
|
||||
_allowUndercollateralized = allowUndercollateralized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice getLastVaultId function - getter function to get the id of the next vault to be created
|
||||
* Ids are sequential and start from 0
|
||||
*/
|
||||
function getLastVaultId() public view returns (uint256) {
|
||||
return _nVaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice convertToUint function - converts a Pyth price struct to a uint256 representing the price of an asset
|
||||
*
|
||||
* @param price: Pyth price struct to be converted
|
||||
* @param targetDecimals: target number of decimals for the output
|
||||
*/
|
||||
function convertToUint(
|
||||
PythStructs.Price memory price,
|
||||
uint8 targetDecimals
|
||||
) private pure returns (uint256) {
|
||||
if (price.price < 0 || price.expo > 0 || price.expo < -255) {
|
||||
revert InvalidPriceExponent();
|
||||
}
|
||||
|
||||
uint8 priceDecimals = uint8(uint32(-1 * price.expo));
|
||||
|
||||
if (targetDecimals >= priceDecimals) {
|
||||
return
|
||||
uint(uint64(price.price)) *
|
||||
10 ** uint32(targetDecimals - priceDecimals);
|
||||
} else {
|
||||
return
|
||||
uint(uint64(price.price)) /
|
||||
10 ** uint32(priceDecimals - targetDecimals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice getPrice function - retrieves price of a given token from the oracle
|
||||
*
|
||||
* @param id: price feed Id of the token
|
||||
*/
|
||||
function _getPrice(bytes32 id) internal view returns (uint256) {
|
||||
IPyth oracle = IPyth(payable(_oracle));
|
||||
return convertToUint(oracle.getPrice(id), 18);
|
||||
}
|
||||
|
||||
function getAllowUndercollateralized() public view returns (bool) {
|
||||
return _allowUndercollateralized;
|
||||
}
|
||||
|
||||
function getOracle() public view returns (address) {
|
||||
return _oracle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice getVaultHealth function - calculates vault collateral/debt ratio
|
||||
*
|
||||
* @param vaultId: Id of the vault for which to calculate health
|
||||
*/
|
||||
function getVaultHealth(uint256 vaultId) public view returns (uint256) {
|
||||
Vault memory vault = _vaults[vaultId];
|
||||
return _getVaultHealth(vault);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice _getVaultHealth function - calculates vault collateral/debt ratio using the on-chain price feeds.
|
||||
* In a real world scenario, caller should ensure that the price feeds are up to date before calling this function.
|
||||
*
|
||||
* @param vault: vault struct containing vault parameters
|
||||
*/
|
||||
function _getVaultHealth(
|
||||
Vault memory vault
|
||||
) internal view returns (uint256) {
|
||||
uint256 priceCollateral = _getPrice(vault.tokenIdCollateral);
|
||||
uint256 priceDebt = _getPrice(vault.tokenIdDebt);
|
||||
|
||||
if (priceCollateral < 0) {
|
||||
revert NegativePrice();
|
||||
}
|
||||
if (priceDebt < 0) {
|
||||
revert NegativePrice();
|
||||
}
|
||||
|
||||
uint256 valueCollateral = priceCollateral * vault.amountCollateral;
|
||||
uint256 valueDebt = priceDebt * vault.amountDebt;
|
||||
|
||||
return (valueCollateral * 1_000_000_000_000_000_000) / valueDebt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice createVault function - creates a vault
|
||||
*
|
||||
* @param tokenCollateral: address of the collateral token of the vault
|
||||
* @param tokenDebt: address of the debt token of the vault
|
||||
* @param amountCollateral: amount of collateral tokens in the vault
|
||||
* @param amountDebt: amount of debt tokens in the vault
|
||||
* @param minHealthRatio: minimum health ratio of the vault, 10**18 is 100%
|
||||
* @param minPermissionlessHealthRatio: minimum health ratio of the vault before permissionless liquidations are allowed. This should be less than minHealthRatio
|
||||
* @param tokenIdCollateral: price feed Id of the collateral token
|
||||
* @param tokenIdDebt: price feed Id of the debt token
|
||||
* @param updateData: data to update price feeds with
|
||||
*/
|
||||
function createVault(
|
||||
address tokenCollateral,
|
||||
address tokenDebt,
|
||||
uint256 amountCollateral,
|
||||
uint256 amountDebt,
|
||||
uint256 minHealthRatio,
|
||||
uint256 minPermissionlessHealthRatio,
|
||||
bytes32 tokenIdCollateral,
|
||||
bytes32 tokenIdDebt,
|
||||
bytes[] calldata updateData
|
||||
) public payable returns (uint256) {
|
||||
_updatePriceFeeds(updateData);
|
||||
Vault memory vault = Vault(
|
||||
tokenCollateral,
|
||||
tokenDebt,
|
||||
amountCollateral,
|
||||
amountDebt,
|
||||
minHealthRatio,
|
||||
minPermissionlessHealthRatio,
|
||||
tokenIdCollateral,
|
||||
tokenIdDebt
|
||||
);
|
||||
if (minPermissionlessHealthRatio > minHealthRatio) {
|
||||
revert InvalidHealthRatios();
|
||||
}
|
||||
if (
|
||||
!_allowUndercollateralized &&
|
||||
_getVaultHealth(vault) < vault.minHealthRatio
|
||||
) {
|
||||
revert UncollateralizedVaultCreation();
|
||||
}
|
||||
|
||||
IERC20(vault.tokenCollateral).safeTransferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
vault.amountCollateral
|
||||
);
|
||||
IERC20(vault.tokenDebt).safeTransfer(msg.sender, vault.amountDebt);
|
||||
|
||||
_vaults[_nVaults] = vault;
|
||||
_nVaults += 1;
|
||||
|
||||
return _nVaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice updateVault function - updates a vault's collateral and debt amounts
|
||||
*
|
||||
* @param vaultId: Id of the vault to be updated
|
||||
* @param deltaCollateral: delta change to collateral amount (+ means adding collateral tokens, - means removing collateral tokens)
|
||||
* @param deltaDebt: delta change to debt amount (+ means withdrawing debt tokens from protocol, - means resending debt tokens to protocol)
|
||||
*/
|
||||
function updateVault(
|
||||
uint256 vaultId,
|
||||
int256 deltaCollateral,
|
||||
int256 deltaDebt
|
||||
) public {
|
||||
Vault memory vault = _vaults[vaultId];
|
||||
|
||||
uint256 qCollateral = stdMath.abs(deltaCollateral);
|
||||
uint256 qDebt = stdMath.abs(deltaDebt);
|
||||
|
||||
bool withdrawExcessiveCollateral = (deltaCollateral < 0) &&
|
||||
(qCollateral > vault.amountCollateral);
|
||||
|
||||
if (withdrawExcessiveCollateral) {
|
||||
revert InvalidVaultUpdate();
|
||||
}
|
||||
|
||||
uint256 futureCollateral = (deltaCollateral >= 0)
|
||||
? (vault.amountCollateral + qCollateral)
|
||||
: (vault.amountCollateral - qCollateral);
|
||||
uint256 futureDebt = (deltaDebt >= 0)
|
||||
? (vault.amountDebt + qDebt)
|
||||
: (vault.amountDebt - qDebt);
|
||||
|
||||
vault.amountCollateral = futureCollateral;
|
||||
vault.amountDebt = futureDebt;
|
||||
|
||||
if (
|
||||
!_allowUndercollateralized &&
|
||||
_getVaultHealth(vault) < vault.minHealthRatio
|
||||
) {
|
||||
revert InvalidVaultUpdate();
|
||||
}
|
||||
|
||||
// update collateral position
|
||||
if (deltaCollateral >= 0) {
|
||||
// sender adds more collateral to their vault
|
||||
IERC20(vault.tokenCollateral).safeTransferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
qCollateral
|
||||
);
|
||||
_vaults[vaultId].amountCollateral += qCollateral;
|
||||
} else {
|
||||
// sender takes back collateral from their vault
|
||||
IERC20(vault.tokenCollateral).safeTransfer(msg.sender, qCollateral);
|
||||
_vaults[vaultId].amountCollateral -= qCollateral;
|
||||
}
|
||||
|
||||
// update debt position
|
||||
if (deltaDebt >= 0) {
|
||||
// sender takes out more debt position
|
||||
IERC20(vault.tokenDebt).safeTransfer(msg.sender, qDebt);
|
||||
_vaults[vaultId].amountDebt += qDebt;
|
||||
} else {
|
||||
// sender sends back debt tokens
|
||||
IERC20(vault.tokenDebt).safeTransferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
qDebt
|
||||
);
|
||||
_vaults[vaultId].amountDebt -= qDebt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice getVault function - getter function to get a vault's parameters
|
||||
*
|
||||
* @param vaultId: Id of the vault
|
||||
*/
|
||||
function getVault(uint256 vaultId) public view returns (Vault memory) {
|
||||
return _vaults[vaultId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice _updatePriceFeeds function - updates the specified price feeds with given data
|
||||
*
|
||||
* @param updateData: data to update price feeds with
|
||||
*/
|
||||
function _updatePriceFeeds(bytes[] calldata updateData) internal {
|
||||
if (updateData.length == 0) {
|
||||
return;
|
||||
}
|
||||
IPyth oracle = IPyth(payable(_oracle));
|
||||
oracle.updatePriceFeeds{value: msg.value}(updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice liquidate function - liquidates a vault
|
||||
* This function calculates the health of the vault and based on the vault parameters one of the following actions is taken:
|
||||
* 1. If health >= minHealthRatio, don't liquidate
|
||||
* 2. If minHealthRatio > health >= minPermissionlessHealthRatio, only liquidate if the vault is permissioned via express relay
|
||||
* 3. If minPermissionlessHealthRatio > health, liquidate no matter what
|
||||
*
|
||||
* @param vaultId: Id of the vault to be liquidated
|
||||
*/
|
||||
function liquidate(uint256 vaultId) public {
|
||||
Vault memory vault = _vaults[vaultId];
|
||||
uint256 vaultHealth = _getVaultHealth(vault);
|
||||
|
||||
// if vault health is above the minimum health ratio, don't liquidate
|
||||
if (vaultHealth >= vault.minHealthRatio) {
|
||||
revert InvalidLiquidation();
|
||||
}
|
||||
|
||||
if (vaultHealth >= vault.minPermissionlessHealthRatio) {
|
||||
// if vault health is below the minimum health ratio but above the minimum permissionless health ratio,
|
||||
// only liquidate if permissioned
|
||||
if (
|
||||
!IExpressRelay(expressRelay).isPermissioned(
|
||||
address(this), // protocol fee receiver
|
||||
abi.encode(vaultId) // vault id uniquely represents the opportunity and can be used as permission id
|
||||
)
|
||||
) {
|
||||
revert InvalidLiquidation();
|
||||
}
|
||||
}
|
||||
|
||||
IERC20(vault.tokenDebt).transferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
vault.amountDebt
|
||||
);
|
||||
IERC20(vault.tokenCollateral).transfer(
|
||||
msg.sender,
|
||||
vault.amountCollateral
|
||||
);
|
||||
|
||||
_vaults[vaultId].amountCollateral = 0;
|
||||
_vaults[vaultId].amountDebt = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice liquidateWithPriceUpdate function - liquidates a vault after updating the specified price feeds with given data
|
||||
*
|
||||
* @param vaultId: Id of the vault to be liquidated
|
||||
* @param updateData: data to update price feeds with
|
||||
*/
|
||||
function liquidateWithPriceUpdate(
|
||||
uint256 vaultId,
|
||||
bytes[] calldata updateData
|
||||
) external payable {
|
||||
_updatePriceFeeds(updateData);
|
||||
liquidate(vaultId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice receiveAuctionProceedings function - receives native token from the express relay
|
||||
* You can use permission key to distribute the received funds to users who got liquidated, LPs, etc...
|
||||
*
|
||||
* @param permissionKey: permission key that was used for the auction
|
||||
*/
|
||||
function receiveAuctionProceedings(
|
||||
bytes calldata permissionKey
|
||||
) external payable {
|
||||
emit VaultReceivedETH(msg.sender, msg.value, permissionKey);
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
// Signature: 0xe922edfd
|
||||
error UncollateralizedVaultCreation();
|
||||
|
||||
// Signature: 0xdcb430ee
|
||||
error InvalidVaultUpdate();
|
||||
|
||||
// Signature: 0x9cd7b1c6
|
||||
error InvalidPriceExponent();
|
||||
|
||||
// Signature: 0x85914873
|
||||
error InvalidLiquidation();
|
||||
|
||||
// Signature: 0x61ca76d2
|
||||
error NegativePrice();
|
||||
|
||||
// Signature: 0x4a7a3163
|
||||
error InvalidHealthRatios();
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
struct Vault {
|
||||
address tokenCollateral;
|
||||
address tokenDebt;
|
||||
uint256 amountCollateral;
|
||||
uint256 amountDebt;
|
||||
uint256 minHealthRatio; // 10**18 is 100%
|
||||
uint256 minPermissionlessHealthRatio;
|
||||
bytes32 tokenIdCollateral;
|
||||
bytes32 tokenIdDebt;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[profile.default]
|
||||
src = "contracts"
|
||||
out = "out"
|
||||
libs = [
|
||||
'lib',
|
||||
'../../../node_modules',
|
||||
'../../../target_chains/ethereum/sdk/solidity'
|
||||
]
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
|
@ -0,0 +1 @@
|
|||
Forge installs the dependencies in this folder. They are .gitignored
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "easylend",
|
||||
"version": "0.1.0",
|
||||
"description": "Example lending protocol with express relay integration",
|
||||
"private": true,
|
||||
"files": [
|
||||
"tslib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"monitor": "npm run build && node tslib/monitor.js",
|
||||
"install-forge-deps": "forge install foundry-rs/forge-std@v1.7.6 --no-git --no-commit"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.5.0",
|
||||
"@pythnetwork/express-relay-evm-js": "*",
|
||||
"@pythnetwork/express-relay-sdk-solidity": "*",
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"viem": "^2.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "^17.0.10",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.25.7",
|
||||
"typescript": "^5.1",
|
||||
"yargs": "^17.4.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
forge-std/=lib/forge-std/src/
|
||||
@openzeppelin/=../../../node_modules/@openzeppelin/
|
||||
@pythnetwork/=../../../node_modules/@pythnetwork/
|
|
@ -0,0 +1,163 @@
|
|||
// This is only a subset of the generated abi necessary for the monitor script
|
||||
export const abi = [
|
||||
{
|
||||
type: "function",
|
||||
name: "getLastVaultId",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
],
|
||||
stateMutability: "view",
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "getVault",
|
||||
inputs: [
|
||||
{
|
||||
name: "vaultId",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "tuple",
|
||||
internalType: "struct Vault",
|
||||
components: [
|
||||
{
|
||||
name: "tokenCollateral",
|
||||
type: "address",
|
||||
internalType: "address",
|
||||
},
|
||||
{
|
||||
name: "tokenDebt",
|
||||
type: "address",
|
||||
internalType: "address",
|
||||
},
|
||||
{
|
||||
name: "amountCollateral",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "amountDebt",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "minHealthRatio",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "minPermissionLessHealthRatio",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "tokenIdCollateral",
|
||||
type: "bytes32",
|
||||
internalType: "bytes32",
|
||||
},
|
||||
{
|
||||
name: "tokenIdDebt",
|
||||
type: "bytes32",
|
||||
internalType: "bytes32",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
stateMutability: "view",
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "liquidate",
|
||||
inputs: [
|
||||
{
|
||||
name: "vaultId",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable",
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "liquidateWithPriceUpdate",
|
||||
inputs: [
|
||||
{
|
||||
name: "vaultId",
|
||||
type: "uint256",
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "updateData",
|
||||
type: "bytes[]",
|
||||
internalType: "bytes[]",
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "payable",
|
||||
},
|
||||
{
|
||||
type: "event",
|
||||
name: "VaultReceivedETH",
|
||||
inputs: [
|
||||
{
|
||||
name: "sender",
|
||||
type: "address",
|
||||
indexed: false,
|
||||
internalType: "address",
|
||||
},
|
||||
{
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
indexed: false,
|
||||
internalType: "uint256",
|
||||
},
|
||||
{
|
||||
name: "permissionKey",
|
||||
type: "bytes",
|
||||
indexed: false,
|
||||
internalType: "bytes",
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidHealthRatios",
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidLiquidation",
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidPriceExponent",
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidVaultUpdate",
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "NegativePrice",
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "UncollateralizedVaultCreation",
|
||||
inputs: [],
|
||||
},
|
||||
] as const;
|
|
@ -0,0 +1,239 @@
|
|||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import {
|
||||
checkAddress,
|
||||
Client,
|
||||
OpportunityParams,
|
||||
} from "@pythnetwork/express-relay-evm-js";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import type { ContractFunctionReturnType } from "viem";
|
||||
import {
|
||||
Address,
|
||||
createPublicClient,
|
||||
encodeAbiParameters,
|
||||
encodeFunctionData,
|
||||
getContract,
|
||||
Hex,
|
||||
http,
|
||||
isHex,
|
||||
} from "viem";
|
||||
import { optimismSepolia } from "viem/chains";
|
||||
import { abi } from "./abi";
|
||||
import {
|
||||
PriceFeed,
|
||||
PriceServiceConnection,
|
||||
} from "@pythnetwork/price-service-client";
|
||||
|
||||
type VaultWithId = ContractFunctionReturnType<
|
||||
typeof abi,
|
||||
"view",
|
||||
"getVault"
|
||||
> & { id: bigint };
|
||||
class ProtocolMonitor {
|
||||
private client: Client;
|
||||
private subscribedIds: Set<string> = new Set();
|
||||
private prices: Record<Hex, PriceFeed> = {};
|
||||
private priceConnection: PriceServiceConnection;
|
||||
|
||||
constructor(
|
||||
expressRelayEndpoint: string,
|
||||
pythEndpoint: string,
|
||||
private chainId: string,
|
||||
private wethContract: Address,
|
||||
private vaultContract: Address,
|
||||
private onlyRecent: number | undefined
|
||||
) {
|
||||
this.client = new Client({ baseUrl: expressRelayEndpoint });
|
||||
this.priceConnection = new PriceServiceConnection(pythEndpoint, {
|
||||
priceFeedRequestConfig: { binary: true },
|
||||
});
|
||||
}
|
||||
|
||||
updatePrice(feed: PriceFeed) {
|
||||
this.prices[`0x${feed.id}`] = feed;
|
||||
}
|
||||
|
||||
async subscribeToPriceFeed(tokenId: string) {
|
||||
if (!this.subscribedIds.has(tokenId)) {
|
||||
await this.priceConnection.subscribePriceFeedUpdates(
|
||||
[tokenId],
|
||||
this.updatePrice.bind(this)
|
||||
);
|
||||
this.subscribedIds.add(tokenId);
|
||||
}
|
||||
}
|
||||
|
||||
async checkVaults() {
|
||||
const rpcClient = createPublicClient({
|
||||
chain: optimismSepolia,
|
||||
transport: http(),
|
||||
});
|
||||
const contract = getContract({
|
||||
address: this.vaultContract,
|
||||
abi,
|
||||
client: rpcClient,
|
||||
});
|
||||
const lastVaultId = await contract.read.getLastVaultId();
|
||||
const vaults: VaultWithId[] = [];
|
||||
let startVaultId = 0n;
|
||||
if (this.onlyRecent && lastVaultId > BigInt(this.onlyRecent)) {
|
||||
startVaultId = lastVaultId - BigInt(this.onlyRecent);
|
||||
}
|
||||
for (let vaultId = startVaultId; vaultId < lastVaultId; vaultId++) {
|
||||
const vault = await contract.read.getVault([vaultId]);
|
||||
// Already liquidated vault
|
||||
if (vault.amountCollateral == 0n && vault.amountDebt == 0n) {
|
||||
continue;
|
||||
}
|
||||
vaults.push({ id: vaultId, ...vault });
|
||||
await this.subscribeToPriceFeed(vault.tokenIdCollateral);
|
||||
await this.subscribeToPriceFeed(vault.tokenIdDebt);
|
||||
}
|
||||
|
||||
for (const vault of vaults) {
|
||||
if (this.isLiquidatable(vault)) {
|
||||
const opportunity = this.createOpportunity(vault);
|
||||
await this.client.submitOpportunity(opportunity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await this.checkVaults();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
}
|
||||
}
|
||||
|
||||
private createOpportunity(vault: VaultWithId) {
|
||||
const priceUpdates = [
|
||||
this.prices[vault.tokenIdCollateral].getVAA()!,
|
||||
this.prices[vault.tokenIdDebt].getVAA()!,
|
||||
];
|
||||
const vaas: Hex[] = priceUpdates.map(
|
||||
(vaa): Hex => `0x${Buffer.from(vaa, "base64").toString("hex")}`
|
||||
);
|
||||
const calldata = encodeFunctionData({
|
||||
abi,
|
||||
functionName: "liquidateWithPriceUpdate",
|
||||
args: [vault.id, vaas],
|
||||
});
|
||||
const permission = this.createPermission(vault.id);
|
||||
const targetCallValue = BigInt(priceUpdates.length);
|
||||
let sellTokens;
|
||||
if (targetCallValue > 0 && vault.tokenDebt == this.wethContract) {
|
||||
sellTokens = [
|
||||
{
|
||||
token: this.wethContract,
|
||||
amount: targetCallValue + vault.amountDebt,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
sellTokens = [
|
||||
{ token: vault.tokenDebt, amount: vault.amountDebt },
|
||||
{ token: this.wethContract, amount: targetCallValue },
|
||||
];
|
||||
}
|
||||
const opportunity: OpportunityParams = {
|
||||
chainId: this.chainId,
|
||||
targetContract: this.vaultContract,
|
||||
targetCalldata: calldata,
|
||||
permissionKey: permission,
|
||||
targetCallValue: targetCallValue,
|
||||
buyTokens: [
|
||||
{ token: vault.tokenCollateral, amount: vault.amountCollateral },
|
||||
],
|
||||
sellTokens: sellTokens,
|
||||
};
|
||||
return opportunity;
|
||||
}
|
||||
|
||||
private isLiquidatable(vault: VaultWithId): boolean {
|
||||
if (
|
||||
!this.prices[vault.tokenIdCollateral] ||
|
||||
!this.prices[vault.tokenIdDebt]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const priceCollateral = BigInt(
|
||||
this.prices[vault.tokenIdCollateral].getPriceUnchecked().price
|
||||
);
|
||||
const priceDebt = BigInt(
|
||||
this.prices[vault.tokenIdDebt].getPriceUnchecked().price
|
||||
);
|
||||
const valueCollateral = priceCollateral * vault.amountCollateral;
|
||||
const valueDebt = priceDebt * vault.amountDebt;
|
||||
if (valueDebt * vault.minHealthRatio > valueCollateral * 10n ** 18n) {
|
||||
const health = Number(valueCollateral) / Number(valueDebt);
|
||||
console.log(`Vault ${vault.id} is undercollateralized health: ${health}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private createPermission(vaultId: bigint) {
|
||||
const permissionPayload = encodeAbiParameters(
|
||||
[{ type: "uint256", name: "vaultId" }],
|
||||
[vaultId]
|
||||
);
|
||||
const permission = encodeAbiParameters(
|
||||
[
|
||||
{ type: "address", name: "contract" },
|
||||
{ type: "bytes", name: "vaultId" },
|
||||
],
|
||||
[this.vaultContract, permissionPayload]
|
||||
);
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option("express-relay-endpoint", {
|
||||
description:
|
||||
"Express relay endpoint. e.g: https://per-staging.dourolabs.app/",
|
||||
type: "string",
|
||||
default: "https://per-staging.dourolabs.app/",
|
||||
})
|
||||
.option("pyth-endpoint", {
|
||||
description: "Pyth endpoint to use for fetching prices",
|
||||
type: "string",
|
||||
default: "https://hermes.pyth.network",
|
||||
})
|
||||
.option("chain-id", {
|
||||
description: "Chain id to send opportunities for. e.g: sepolia",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("weth-contract", {
|
||||
description: "wrapped eth contract address",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("vault-contract", {
|
||||
description: "Dummy token vault contract address",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("only-recent", {
|
||||
description:
|
||||
"Instead of checking all vaults, only check recent ones. Specify the number of recent vaults to check",
|
||||
type: "number",
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h")
|
||||
.parseSync();
|
||||
|
||||
async function run() {
|
||||
const monitor = new ProtocolMonitor(
|
||||
argv.expressRelayEndpoint,
|
||||
argv.pythEndpoint,
|
||||
argv.chainId,
|
||||
checkAddress(argv.wethContract),
|
||||
checkAddress(argv.vaultContract),
|
||||
argv.onlyRecent
|
||||
);
|
||||
await monitor.start();
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"rootDir": "src/",
|
||||
"outDir": "./tslib",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
|
@ -45,6 +45,7 @@
|
|||
"@types/yargs": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"name": "root",
|
||||
"version": "0.0.1",
|
||||
"workspaces": [
|
||||
"express_relay/examples/easy_lend",
|
||||
"express_relay/sdk/js",
|
||||
"express_relay/sdk/solidity",
|
||||
"governance/xc_admin/packages/*",
|
||||
|
@ -650,6 +651,371 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend": {
|
||||
"name": "easylend",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.5.0",
|
||||
"@pythnetwork/express-relay-evm-js": "*",
|
||||
"@pythnetwork/express-relay-sdk-solidity": "*",
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"viem": "^2.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "^17.0.10",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.25.7",
|
||||
"typescript": "^5.1",
|
||||
"yargs": "^17.4.1"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@eslint/eslintrc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.6.0",
|
||||
"globals": "^13.19.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@eslint/js": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
|
||||
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^2.0.2",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
||||
"dev": true
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@scure/bip32": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz",
|
||||
"integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==",
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.2.0",
|
||||
"@noble/hashes": "~1.3.2",
|
||||
"@scure/base": "~1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/@scure/bip39": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.3.0",
|
||||
"@scure/base": "~1.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/abitype": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz",
|
||||
"integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4",
|
||||
"zod": "^3 >=3.22.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/eslint": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.57.0",
|
||||
"@humanwhocodes/config-array": "^0.11.14",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
"debug": "^4.3.2",
|
||||
"doctrine": "^3.0.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^7.2.2",
|
||||
"eslint-visitor-keys": "^3.4.3",
|
||||
"espree": "^9.6.1",
|
||||
"esquery": "^1.4.2",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"globals": "^13.19.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"levn": "^0.4.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"text-table": "^0.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/typescript": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/viem": {
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/viem/-/viem-2.8.13.tgz",
|
||||
"integrity": "sha512-jEbRUjsiBwmoDr3fnKL1Bh1GhK5ERhmZcPLeARtEaQoBTPB6bcO2siKhNPVOF8qrYRnGHGQrZHncBWMQhTjGYg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "1.10.0",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.2",
|
||||
"@scure/bip32": "1.3.2",
|
||||
"@scure/bip39": "1.2.1",
|
||||
"abitype": "1.0.0",
|
||||
"isows": "1.0.3",
|
||||
"ws": "8.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"express_relay/examples/easy_lend/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"express_relay/sdk/js": {
|
||||
"name": "@pythnetwork/express-relay-evm-js",
|
||||
"version": "0.2.0",
|
||||
|
@ -663,6 +1029,7 @@
|
|||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"@types/yargs": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
|
@ -27840,6 +28207,10 @@
|
|||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/easylend": {
|
||||
"resolved": "express_relay/examples/easy_lend",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
|
@ -67903,6 +68274,7 @@
|
|||
"@pythnetwork/express-relay-evm-js": {
|
||||
"version": "file:express_relay/sdk/js",
|
||||
"requires": {
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"@types/yargs": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
|
@ -83307,6 +83679,255 @@
|
|||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"easylend": {
|
||||
"version": "file:express_relay/examples/easy_lend",
|
||||
"requires": {
|
||||
"@openzeppelin/contracts": "^4.5.0",
|
||||
"@pythnetwork/express-relay-evm-js": "*",
|
||||
"@pythnetwork/express-relay-sdk-solidity": "*",
|
||||
"@pythnetwork/pyth-evm-js": "*",
|
||||
"@pythnetwork/pyth-sdk-solidity": "*",
|
||||
"@types/yargs": "^17.0.10",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.7",
|
||||
"typescript": "^5.1",
|
||||
"viem": "^2.7.6",
|
||||
"yargs": "^17.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.6.0",
|
||||
"globals": "^13.19.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
|
||||
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
|
||||
"dev": true
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@humanwhocodes/object-schema": "^2.0.2",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/object-schema": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
||||
"dev": true
|
||||
},
|
||||
"@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"requires": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
}
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
||||
},
|
||||
"@scure/bip32": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz",
|
||||
"integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==",
|
||||
"requires": {
|
||||
"@noble/curves": "~1.2.0",
|
||||
"@noble/hashes": "~1.3.2",
|
||||
"@scure/base": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"@scure/bip39": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||
"requires": {
|
||||
"@noble/hashes": "~1.3.0",
|
||||
"@scure/base": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"abitype": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz",
|
||||
"integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.57.0",
|
||||
"@humanwhocodes/config-array": "^0.11.14",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
"debug": "^4.3.2",
|
||||
"doctrine": "^3.0.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^7.2.2",
|
||||
"eslint-visitor-keys": "^3.4.3",
|
||||
"espree": "^9.6.1",
|
||||
"esquery": "^1.4.2",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"globals": "^13.19.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"levn": "^0.4.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"text-table": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"viem": {
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/viem/-/viem-2.8.13.tgz",
|
||||
"integrity": "sha512-jEbRUjsiBwmoDr3fnKL1Bh1GhK5ERhmZcPLeARtEaQoBTPB6bcO2siKhNPVOF8qrYRnGHGQrZHncBWMQhTjGYg==",
|
||||
"requires": {
|
||||
"@adraffy/ens-normalize": "1.10.0",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.2",
|
||||
"@scure/bip32": "1.3.2",
|
||||
"@scure/bip39": "1.2.1",
|
||||
"abitype": "1.0.0",
|
||||
"isows": "1.0.3",
|
||||
"ws": "8.13.0"
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "root",
|
||||
"workspaces": [
|
||||
"express_relay/examples/easy_lend",
|
||||
"express_relay/sdk/js",
|
||||
"express_relay/sdk/solidity",
|
||||
"governance/xc_admin/packages/*",
|
||||
|
|
Loading…
Reference in New Issue