Wormhole wrapped token convert

This commit is contained in:
armaniferrante 2021-03-18 17:02:39 -07:00
parent 66fc4e2735
commit 2d77694c19
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
7 changed files with 486 additions and 45 deletions

View File

@ -1,5 +1,5 @@
language: node_js
node_js: 10
node_js: 14
dist: bionic
cache: yarn
script: yarn build

View File

@ -7,6 +7,7 @@
"@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@project-serum/serum": "^0.13.33",
"@project-serum/swap": "^0.0.11",
"@solana/spl-token-registry": "^0.2.1",
"@solana/web3.js": "^0.87.2",
"@testing-library/jest-dom": "^5.11.6",

View File

@ -507,7 +507,7 @@ function BalanceListItemDetails({
const [sendDialogOpen, setSendDialogOpen] = useState(false);
const [depositDialogOpen, setDepositDialogOpen] = useState(false);
const [tokenInfoDialogOpen, setTokenInfoDialogOpen] = useState(false);
const [exportAccDialogOpen, setExportAccDialogOpen] = useState(false);
const [exportAccDialogOpen, setExportAccDialogOpen] = useState(false);
const [
closeTokenAccountDialogOpen,
setCloseTokenAccountDialogOpen,
@ -665,7 +665,7 @@ function BalanceListItemDetails({
onClick={() => setSendDialogOpen(true)}
>
Send
</Button>
</Button>
{mint && amount === 0 ? (
<Button
variant="outlined"
@ -705,7 +705,7 @@ function BalanceListItemDetails({
onClose={() => setCloseTokenAccountDialogOpen(false)}
balanceInfo={balanceInfo}
publicKey={publicKey}
/>
/>
</>
);
}

View File

@ -36,6 +36,7 @@ import {
import { parseTokenAccountData } from '../utils/tokens/data';
import { Switch, Tooltip } from '@material-ui/core';
import { EthFeeEstimate } from './EthFeeEstimate';
import SwapWormholeDialog from './SwapWormholeDialog';
const WUSDC_MINT = new PublicKey(
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
@ -43,12 +44,10 @@ const WUSDC_MINT = new PublicKey(
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const WUSDT_MINT = new PublicKey(
'BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4'
'BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4',
);
const USDT_MINT = new PublicKey(
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
);
const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB');
export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
const isProdNetwork = useIsProdNetwork();
@ -65,43 +64,37 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
const { mint, tokenName, tokenSymbol } = balanceInfo;
const getTabs = (mint) => {
const wormholeTab = (
<Tab label={'SPL Wormhole'} key="wormhole" value="wormhole" />
);
if (mint?.equals(WUSDC_MINT)) {
return [
<Tab label="SPL WUSDC" key="spl" value="spl" />,
<Tab
label="SPL USDC"
key="wusdcToSplUsdc"
value="wusdcToSplUsdc"
/>,
wormholeTab,
<Tab label="SPL USDC" key="wusdcToSplUsdc" value="wusdcToSplUsdc" />,
<Tab label="ERC20 USDC" key="swap" value="swap" />,
]
];
} else if (mint?.equals(WUSDT_MINT)) {
return [
<Tab label="SPL WUSDT" key="spl" value="spl" />,
<Tab
label="SPL USDT"
key="wusdtToSplUsdt"
value="wusdtToSplUsdt"
/>,
wormholeTab,
<Tab label="SPL USDT" key="wusdtToSplUsdt" value="wusdtToSplUsdt" />,
<Tab label="ERC20 USDT" key="swap" value="swap" />,
]
];
} else {
return [
<Tab label={`SPL ${swapCoinInfo.ticker}`} key="spl" value="spl" />,
wormholeTab,
<Tab
label={`SPL ${swapCoinInfo.ticker}`}
key="spl"
value="spl"
/>,
<Tab
label={`${
swapCoinInfo.erc20Contract ? 'ERC20' : 'Native'
} ${swapCoinInfo.ticker}`}
label={`${swapCoinInfo.erc20Contract ? 'ERC20' : 'Native'} ${
swapCoinInfo.ticker
}`}
key="swap"
value="swap"
/>,
]
];
}
}
};
return (
<>
@ -109,6 +102,9 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
open={open}
onClose={onClose}
onSubmit={() => onSubmitRef.current()}
maxWidth={
mint?.equals(WUSDC_MINT) || mint?.equals(WUSDT_MINT) ? 'md' : 'sm'
}
fullWidth
>
<DialogTitle>
@ -160,6 +156,14 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
onSubmitRef={onSubmitRef}
wusdtToSplUsdt
/>
) : tab === 'wormhole' ? (
<SwapWormholeDialog
publicKey={publicKey}
onClose={onClose}
balanceInfo={balanceInfo}
swapCoinInfo={swapCoinInfo}
onSubmitRef={onSubmitRef}
/>
) : (
<SendSwapDialog
key={tab}
@ -323,11 +327,12 @@ function SendSwapDialog({
} = useForm(balanceInfo);
const { tokenName, decimals, mint } = balanceInfo;
const blockchain = wusdcToSplUsdc || wusdtToSplUsdt
? 'sol'
: swapCoinInfo.blockchain === 'sol'
? 'eth'
: swapCoinInfo.blockchain;
const blockchain =
wusdcToSplUsdc || wusdtToSplUsdt
? 'sol'
: swapCoinInfo.blockchain === 'sol'
? 'eth'
: swapCoinInfo.blockchain;
const needMetamask = blockchain === 'eth';
const [ethBalance] = useAsyncData(
@ -367,7 +372,13 @@ function SendSwapDialog({
} else if (wusdtToSplUsdt && splUsdtWalletAddress) {
setDestinationAddress(splUsdtWalletAddress);
}
}, [setDestinationAddress, wusdcToSplUsdc, splUsdcWalletAddress, wusdtToSplUsdt, splUsdtWalletAddress]);
}, [
setDestinationAddress,
wusdcToSplUsdc,
splUsdcWalletAddress,
wusdtToSplUsdt,
splUsdtWalletAddress,
]);
async function makeTransaction() {
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
@ -644,7 +655,7 @@ function useForm(
};
}
function balanceAmountToUserAmount(balanceAmount, decimals) {
export function balanceAmountToUserAmount(balanceAmount, decimals) {
return (balanceAmount / Math.pow(10, decimals)).toFixed(decimals);
}

View File

@ -0,0 +1,324 @@
import React, { useState, useEffect } from 'react';
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import TextField from '@material-ui/core/TextField';
import InputAdornment from '@material-ui/core/InputAdornment';
import CircularProgress from '@material-ui/core/CircularProgress';
import { PublicKey } from '@solana/web3.js';
import { Pool } from '@project-serum/swap';
import { balanceAmountToUserAmount } from './SendDialog';
import { useWallet, useWalletAddressForMint } from '../utils/wallet';
import { swapApiRequest } from '../utils/swap/api';
import { getErc20Decimals } from '../utils/swap/eth.js';
import { useSendTransaction } from '../utils/notifications';
import { signAndSendTransaction } from '../utils/tokens';
const SWAP_PROGRAM_ID = new PublicKey(
'SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8',
);
const POOL_BASE = new PublicKey(
'CAXLccDUeS6egtNNEBLrxAqxSvuL6SwspqYX14JdKaiK',
);
export default function SwapWormholeDialog({
publicKey,
onClose,
balanceInfo,
swapCoinInfo,
onSubmitRef,
}) {
// Possible values:
//
// * undefined => loading.
// * pool.accountInfo === null => no pool exists.
// * pool.accountInfo !== null => pool exists.
const [pool, setPool] = useState(undefined);
const [wormholeMintAddr, setWormholeMintAddr] = useState(null);
const [transferAmountString, setTransferAmountString] = useState('');
const [isLoading, setIsLoading] = useState(false);
const wallet = useWallet();
const [sendTransaction] = useSendTransaction();
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
const parsedAmount = parseFloat(transferAmountString);
const validAmount = parsedAmount > 0 && parsedAmount <= balanceAmount;
const ethChainId = 2;
const wormholeTokenAddr = useWalletAddressForMint(wormholeMintAddr);
// Note: there are three "useEffect" closures to be run in order.
// Each one triggers the next.
// 1. Calculate wormhole wrapped token mint address *and*
// url to initialize the AMM pool.
useEffect(() => {
if (wormholeMintAddr === null) {
const fetch = async () => {
let erc20Contract;
let decimals;
let _wormholeMintAddr;
if (swapCoinInfo.ticker === 'ETH') {
erc20Contract = 'eth';
decimals = -1;
_wormholeMintAddr = new PublicKey(
'FeGn77dhg1KXRRFeSwwMiykZnZPw5JXW6naf2aQgZDQf',
);
} else if (swapCoinInfo.ticker === 'BTC') {
erc20Contract = 'btc';
decimals = -1;
_wormholeMintAddr = new PublicKey(
'qfnqNqs3nCAHjnyCgLRDbBtq4p2MtHZxw8YjSyYhPoL',
);
} else {
erc20Contract = swapCoinInfo.erc20Contract;
decimals = await getErc20Decimals(erc20Contract);
_wormholeMintAddr = await wormholeMintAddress(
ethChainId,
Math.min(decimals, 9),
Buffer.from(erc20Contract.slice(2), 'hex'),
);
}
setWormholeMintAddr(_wormholeMintAddr);
};
fetch();
}
}, [
ethChainId,
swapCoinInfo.erc20Contract,
swapCoinInfo.ticker,
wormholeMintAddr,
]);
// 2. Fetch the wormhole pool, if it exists.
useEffect(() => {
if (wormholeMintAddr !== null) {
const fetch = async () => {
const seed =
balanceInfo.mint.toString().slice(0, 16) +
wormholeMintAddr.toString().slice(0, 16);
const publicKey = await PublicKey.createWithSeed(
POOL_BASE,
seed,
SWAP_PROGRAM_ID,
);
const accountInfo = await wallet.connection.getAccountInfo(publicKey);
setPool({
publicKey,
accountInfo,
});
};
fetch();
}
}, [wormholeMintAddr, balanceInfo.mint, wallet.connection]);
// 3. Tell the bridge to create the AMM pool, if no
// sollet <-> wormhole pool exists.
useEffect(() => {
if (
pool &&
pool.accountInfo === null &&
pool.publicKey &&
wormholeMintAddr &&
balanceAmount > 0
) {
const url = `wormhole/pool/${
swapCoinInfo.ticker
}/${pool.publicKey.toString()}/${balanceInfo.mint.toString()}/${wormholeMintAddr.toString()}`;
swapApiRequest('POST', url).catch(console.error);
}
}, [
pool,
wormholeMintAddr,
balanceAmount,
balanceInfo.mint,
swapCoinInfo.ticker,
]);
// Converts the sollet wrapped token into the wormhole wrapped token
// by trading on the constant price pool.
async function convert() {
// User does not have a wormhole account. Make it.
let _wormholeTokenAddr = wormholeTokenAddr;
if (!_wormholeTokenAddr) {
const createWormholeAccount = async () => {
const [addr, txSig] = await wallet.createAssociatedTokenAccount(
wormholeMintAddr,
);
_wormholeTokenAddr = addr;
return txSig;
};
let err = await new Promise((resolve) => {
sendTransaction(createWormholeAccount(), {
onSuccess: () => resolve(false),
onError: () => resolve(true),
});
});
if (err) {
return;
}
}
// Swapping from: sollet.
const tokenIn = {
mint: balanceInfo.mint,
tokenAccount: publicKey,
amount: parsedAmount,
};
// Swapping to: wormhole.
const tokenOut = {
mint: wormholeMintAddr,
tokenAccount: _wormholeTokenAddr,
amount: parsedAmount,
};
// Misc swap params.
const owner = wallet.publicKey;
const slippage = 0.5;
const hostFeeAccount = undefined;
const skipPreflight = true;
// Constant price AMM client.
const poolClient = await Pool.load(
wallet.connection,
pool.publicKey,
SWAP_PROGRAM_ID,
);
// Execute swap.
const { transaction, signers } = await poolClient.makeSwapTransaction(
wallet.connection,
owner,
tokenIn,
tokenOut,
slippage,
hostFeeAccount,
);
const txSig = await signAndSendTransaction(
wallet.connection,
transaction,
wallet,
signers,
skipPreflight,
);
console.log('Transaction: ', txSig);
return txSig;
}
async function onSubmit() {
setIsLoading(true);
await new Promise((resolve) => {
sendTransaction(convert(), {
onSuccess: () => resolve(),
onError: () => resolve(),
});
});
setIsLoading(false);
}
onSubmitRef.current = onSubmit;
return (
<>
<DialogContent style={{ paddingTop: 16 }}>
{pool === undefined || isLoading ? (
<CircularProgress
style={{
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
) : pool.accountInfo === null ? (
<DialogContentText>
{`Wormhole conversion is not yet setup for this token. Please try later.`}
</DialogContentText>
) : (
<>
<DialogContentText>
{`Convert your tokens into wormhole-wrapped tokens.`}
</DialogContentText>
<TextField
label="Amount"
fullWidth
variant="outlined"
margin="normal"
type="number"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Button
onClick={() =>
setTransferAmountString(
balanceAmountToUserAmount(balanceAmount, decimals),
)
}
>
MAX
</Button>
{tokenSymbol ? tokenSymbol : null}
</InputAdornment>
),
inputProps: {
step: Math.pow(10, -decimals),
},
}}
value={transferAmountString}
onChange={(e) => setTransferAmountString(e.target.value.trim())}
helperText={
<span
onClick={() =>
setTransferAmountString(
balanceAmountToUserAmount(balanceAmount, decimals),
)
}
>
Max: {balanceAmountToUserAmount(balanceAmount, decimals)}
</span>
}
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button disabled={!validAmount} type="submit" color="primary">
Convert
</Button>
</DialogActions>
</>
);
}
// Currently, only used for calculating the Solana wrapped token mint address.
// I.e. assetChain is always === 2 and assetAddress always is the ethereum
// contract address.
async function wormholeMintAddress(
assetChain: number,
assetDecimals: number,
assetAddress: Buffer,
): PublicKey {
const bridgeId = new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC');
const bridgeAuthority = await getBridgeAuthority(bridgeId);
const seeds = [
Buffer.from('wrapped'),
bridgeAuthority.toBuffer(),
Buffer.of(assetChain),
Buffer.of(assetDecimals),
padBuffer(assetAddress, 32),
];
const [mint] = await PublicKey.findProgramAddress(seeds, bridgeId);
return mint;
}
async function getBridgeAuthority(bridgeId: PublicKey): PublicKey {
const [ba] = await PublicKey.findProgramAddress(
[Buffer.from('bridge')],
bridgeId,
);
return ba;
}
export function padBuffer(b: Buffer, len: number): Buffer {
const zeroPad = Buffer.alloc(len);
b.copy(zeroPad, len - b.length);
return zeroPad;
}

View File

@ -27,6 +27,12 @@ export function useEthAccount() {
return account;
}
export async function getErc20Decimals(erc20Address) {
const erc20 = new web3.eth.Contract(ERC20_ABI, erc20Address);
const decimals = await erc20.methods.decimals().call();
return decimals;
}
export async function getErc20Balance(account, erc20Address) {
if (!erc20Address) {
return parseInt(await web3.eth.getBalance(account)) / 1e18;

115
yarn.lock
View File

@ -1254,6 +1254,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
@ -1862,7 +1869,7 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
"@project-serum/serum@^0.13.33":
"@project-serum/serum@^0.13.21", "@project-serum/serum@^0.13.33":
version "0.13.33"
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.33.tgz#03ce8219c1bb458f56c09dc8aa621d76538e70f5"
integrity sha512-g2ztZwhQAvhGE9u4/Md6uEFBpaOMV2Xa/H/FGhgTx3iBv2sikW5fheHRJ8Vy7yEA9ZhZCuzbCkw8Wz1fq82VAg==
@ -1871,6 +1878,19 @@
bn.js "^5.1.2"
buffer-layout "^1.2.0"
"@project-serum/swap@^0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.0.11.tgz#f606b73fbdcd152d7835068f168cb8ad172d8414"
integrity sha512-DxvNL81LnBcE62oENr0LOqIAen9/LNb7gEcDbNlIHr02PckUCZTDIngIsbDY09ld1+V7STy+pLPnstt6ZZElsg==
dependencies:
"@project-serum/serum" "^0.13.21"
"@solana/spl-token" "^0.0.13"
"@solana/spl-token-swap" "0.1.0"
bn.js "^5.1.3"
bs58 "^4.0.1"
buffer-layout "^1.2.0"
dotenv "^8.2.0"
"@rollup/plugin-node-resolve@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca"
@ -1925,6 +1945,54 @@
dependencies:
cross-fetch "^3.0.6"
"@solana/spl-token-swap@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@solana/spl-token-swap/-/spl-token-swap-0.1.0.tgz#a1bc2b0c96edae8b31bb2cc000ebacdc36e131c8"
integrity sha512-h0ntp6xwRZBEKDd6oNhJTbPISjIfeGm0eqQqAccTkeluo1zHve4dnUChTKF4MQ+JqXjfqd6z6DJjfa0+dGA37w==
dependencies:
"@babel/runtime" "^7.11.2"
"@solana/web3.js" "^0.90.0"
bn.js "^5.1.3"
buffer-layout "^1.2.0"
dotenv "8.2.0"
json-to-pretty-yaml "^1.2.2"
mkdirp "1.0.4"
"@solana/spl-token@^0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.0.13.tgz#5e0b235b1f8b34643280401dbfddeb34d13d1acd"
integrity sha512-WT8M9V/hxURR5jLbhr3zgwVsgcY6m8UhHtK045w7o+jx8FJ9MKARkj387WBFU7mKiFq0k8jw/8YL7XmnIUuH8Q==
dependencies:
"@babel/runtime" "^7.10.5"
"@solana/web3.js" "^0.86.1"
bn.js "^5.0.0"
buffer-layout "^1.2.0"
dotenv "8.2.0"
mkdirp "1.0.4"
"@solana/web3.js@^0.86.1":
version "0.86.4"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.86.4.tgz#69216d3928ca4727c25a1ea96c405e897156ac3b"
integrity sha512-FpabDmdyxBN5aHIVUWc9Q6pXJFWiLRm/xeyxFg9O9ICHjiUkd38omds7G0CAmykIccG7zaMziwtkXp+0KvQOhA==
dependencies:
"@babel/runtime" "^7.3.1"
bn.js "^5.0.0"
bs58 "^4.0.1"
buffer "^5.4.3"
buffer-layout "^1.2.0"
crypto-hash "^1.2.2"
esdoc-inject-style-plugin "^1.0.0"
jayson "^3.0.1"
keccak "^3.0.1"
mz "^2.7.0"
node-fetch "^2.2.0"
npm-run-all "^4.1.5"
rpc-websockets "^7.4.2"
secp256k1 "^4.0.2"
superstruct "^0.8.3"
tweetnacl "^1.0.0"
ws "^7.0.0"
"@solana/web3.js@^0.87.2":
version "0.87.2"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.87.2.tgz#92c8d344695c6113d4e0eb3339117fbc6b22d0d2"
@ -3477,6 +3545,11 @@ bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
bn.js@^5.1.3:
version "5.2.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
body-parser@1.19.0, body-parser@^1.16.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@ -3708,6 +3781,14 @@ buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.4.3:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.1:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
@ -5146,7 +5227,7 @@ dotenv-expand@5.1.0:
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv@8.2.0:
dotenv@8.2.0, dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
@ -6985,16 +7066,16 @@ idna-uts46-hx@^2.3.1:
dependencies:
punycode "2.1.0"
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
iferr@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
@ -8140,6 +8221,14 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
json-to-pretty-yaml@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b"
integrity sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs=
dependencies:
remedial "^1.0.7"
remove-trailing-spaces "^1.0.6"
json3@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
@ -8916,7 +9005,7 @@ mkdirp-promise@^5.0.1:
dependencies:
mkdirp "*"
mkdirp@*, mkdirp@^1.0.3, mkdirp@^1.0.4:
mkdirp@*, mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@ -11196,11 +11285,21 @@ relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
remedial@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
remove-trailing-spaces@^1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7"
integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==
renderkid@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"