diff --git a/components/instructions/programs/symmetryV2.tsx b/components/instructions/programs/symmetryV2.tsx
new file mode 100644
index 0000000000..3a0bd424f4
--- /dev/null
+++ b/components/instructions/programs/symmetryV2.tsx
@@ -0,0 +1,242 @@
+import { Connection } from "@solana/web3.js"
+import { BasketsSDK } from "@symmetry-hq/baskets-sdk"
+import BufferLayout from 'buffer-layout'
+
+const targetCompositionLayout = BufferLayout.seq(
+ BufferLayout.u8(),
+ 15,
+ 'targetComposition'
+);
+const targetWeightsLayout = BufferLayout.seq(
+ BufferLayout.u32(),
+ 15,
+ 'targetWeights'
+);
+const rebalanceAndLpLayout = BufferLayout.seq(
+ BufferLayout.u8(),
+ 2,
+ 'rebalanceAndLp'
+);
+
+export const SYMMETRY_V2_INSTRUCTIONS = {
+ "2KehYt3KsEQR53jYcxjbQp2d2kCp4AkuQW68atufRwSr": {
+ 78: {
+ name: 'Symmetry: Withdraw from Basket',
+ accounts: [
+ { name: 'Withdrawer' },
+ { name: 'Basket Address' },
+ { name: 'Symmetry PDA' },
+ { name: 'Withdraw Temporary State Account' },
+ { name: 'Withdrawer Basket Token Account' },
+ { name: 'Basket Token Mint' },
+ { name: 'System Program' },
+ { name: 'Token Program' },
+ { name: 'Rent Program' },
+ { name: 'Account' },
+ ],
+ getDataUI: async (connection: Connection, data: Uint8Array) => {
+
+ //@ts-ignore
+ const { amount, rebalance } = BufferLayout.struct([
+ BufferLayout.nu64('amount'),
+ BufferLayout.nu64('rebalance')
+ ]).decode(Buffer.from(data), 8)
+
+ return (
+ <>
+
Withdraw Amount: {amount / 10**6}
+ Rebalance to USDC: {rebalance === 2 ? 'Yes' : 'No - Withdraw Assets Directly'}
+ >
+ )
+ },
+ },
+ 251: {
+ name: 'Symmetry: Deposit into Basket',
+ accounts: [
+ { name: 'Depositor' },
+ { name: 'Basket Address' },
+ { name: 'Basket Token Mint' },
+ { name: 'Symmetry Token List' },
+ { name: 'Symmetry PDA' },
+ { name: 'USDC PDA Account' },
+ { name: 'Depositor USDC Account' },
+ { name: 'Manager USDC Account' },
+ { name: 'Symmetry Fee Account' },
+ { name: 'Host Platform USDC Account' },
+ { name: 'Depositor Basket Token Account' },
+ { name: 'Temporary State Account for Deposit' },
+ { name: 'System Program' },
+ { name: 'Token Program' },
+ { name: 'Rent' },
+ { name: 'Associated Token Program' },
+ { name: 'Account' },
+ ],
+ getDataUI: async (connection: Connection, data: Uint8Array) => {
+
+ //@ts-ignore
+ const { amount, rebalance } = BufferLayout.struct([
+ BufferLayout.nu64('amount')
+ ]).decode(Buffer.from(data), 8)
+
+ return (
+ <>
+ USDC Deposit Amount: {amount / 10**6}
+ >
+ )
+ },
+ },
+ 38: {
+ name: 'Symmetry: Edit Basket',
+ accounts: [
+ { name: 'Basket Manager' },
+ { name: 'Basket Address' },
+ { name: 'Symmetry Token List' },
+ { name: 'Manager Fee Receiver Address' },
+ ],
+ getDataUI: async (connection: Connection, data: Uint8Array) => {
+
+ //@ts-ignore
+ const { managerFee, rebalanceInterval, rebalanceThreshold, rebalanceSlippage, lpOffsetThreshold, rebalanceAndLp, numOfTokens, targetComposition, targetWeights } = BufferLayout.struct([
+ BufferLayout.u16('managerFee'),
+ BufferLayout.nu64('rebalanceInterval'),
+ BufferLayout.u16('rebalanceThreshold'),
+ BufferLayout.u16('rebalanceSlippage'),
+ BufferLayout.u16('lpOffsetThreshold'),
+ rebalanceAndLpLayout,
+ BufferLayout.u8('numOfTokens'),
+ targetCompositionLayout,
+ targetWeightsLayout,
+ ]).decode(Buffer.from(data), 8)
+
+ const basketsSdk = await BasketsSDK.init(connection);
+ const tokenData = basketsSdk.getTokenListData();
+ let usdcIncluded = false;
+ let totalWeight = 0; targetWeights.map(w => totalWeight += w);
+
+ let composition = targetComposition.map((tokenId, i) => {
+ let token = tokenData.filter(x => x.id == tokenId)[0]
+ if(token.id === 0) {
+ if(!usdcIncluded) {
+ usdcIncluded = true;
+ return {
+ ...token,
+ weight: targetWeights[i] / totalWeight * 100
+ }
+ }
+ } else {
+ return {
+ ...token,
+ weight: targetWeights[i] / totalWeight * 100
+ }
+ }
+ }).filter(x => x != null)
+
+ return (
+ <>
+ Manager Fee: {managerFee / 100}%
+ Rebalance Check Interval: {rebalanceInterval / 60} minutes
+ Rebalance Trigger Threshold: {rebalanceThreshold / 100}%
+ Maximum Slippage Allowed During Rebalancing: {rebalanceSlippage / 100}%
+ Liquidity Provision Threshold: {lpOffsetThreshold / 100}%
+ Rebalancing Enabled: {rebalanceAndLp[0] === 0 ? "Yes" : "No"}
+ Liquidity Provision Enabled: {rebalanceAndLp[1] === 0 ? "No" : "Yes"}
+ Basket Composition Size: {numOfTokens} Tokens
+
+ Basket Composition:
+ {
+ composition.map((compItem, i) => {
+ return
+ })
+ }
+
+ >
+ )
+ },
+ },
+ 47: {
+ name: 'Symmetry: Create Basket',
+ accounts: [
+ { name: 'Manager' },
+ { name: 'Token List' },
+ { name: 'Basket Address' },
+ { name: 'Symmetry PDA' },
+ { name: 'Basket Token Mint' },
+ { name: 'Symmetry Fee Collector' },
+ { name: 'Metadata Account' },
+ { name: 'Metadata Program' },
+ { name: 'System Program' },
+ { name: 'Token Program' },
+ { name: 'Rent' },
+ { name: 'Host Platform' },
+ { name: 'Fee Collector Address' },
+ { name: 'Account' },
+ ],
+ getDataUI: async (connection: Connection, data: Uint8Array) => {
+ //@ts-ignore
+ const { managerFee, hostFee, basketType,rebalanceInterval, rebalanceThreshold, rebalanceSlippage, lpOffsetThreshold, rebalanceAndLp, numOfTokens, targetComposition, targetWeights, } = BufferLayout.struct([
+ BufferLayout.u16('managerFee'),
+ BufferLayout.u16('hostFee'),
+ BufferLayout.u8('basketType'),
+ BufferLayout.nu64('rebalanceInterval'),
+ BufferLayout.u16('rebalanceThreshold'),
+ BufferLayout.u16('rebalanceSlippage'),
+ BufferLayout.u16('lpOffsetThreshold'),
+ rebalanceAndLpLayout,
+ BufferLayout.u8('numOfTokens'),
+ targetCompositionLayout,
+ targetWeightsLayout,
+ ]).decode(Buffer.from(data), 8)
+
+
+ let basketsSdk = await BasketsSDK.init(connection);
+ let tokenData = basketsSdk.getTokenListData();
+ let usdcIncluded = false;
+ let totalWeight = 0; targetWeights.map(w => totalWeight += w);
+
+ let composition = targetComposition.map((tokenId, i) => {
+ let token = tokenData.filter(x => x.id == tokenId)[0]
+ if(token.id === 0) {
+ if(!usdcIncluded) {
+ usdcIncluded = true;
+ return {
+ ...token,
+ weight: targetWeights[i] / totalWeight * 100
+ }
+ }
+ } else
+ return {
+ ...token,
+ weight: targetWeights[i] / totalWeight * 100
+ }
+ }).filter(x => x != null)
+
+ return (
+ <>
+ Manager Fee: {managerFee / 100}%
+ Host Platform Fee: {hostFee / 100}%
+ Basket Type: {basketType === 0 ? "Bundle" : basketType === 1 ? "Portfolio" : "Private"}
+ Rebalance Check Interval: {rebalanceInterval / 60} minutes
+ Rebalance Trigger Threshold: {rebalanceThreshold / 100}%
+ Maximum Slippage Allowed During Rebalancing: {rebalanceSlippage / 100}%
+ Liquidity Provision Threshold: {lpOffsetThreshold / 100}%
+ Rebalancing Enabled: {rebalanceAndLp[0] === 0 ? "Yes" : "No"}
+ Liquidity Provision Enabled: {rebalanceAndLp[1] === 0 ? "No" : "Yes"}
+ Basket Composition Size: {numOfTokens} Tokens
+
+ Basket Composition:
+ {
+ composition.map((compItem, i) => {
+ return
+ })
+ }
+
+ >
+ )
+ },
+ },
+ },
+}
\ No newline at end of file
diff --git a/components/instructions/tools.tsx b/components/instructions/tools.tsx
index b598d91986..ac37a15726 100644
--- a/components/instructions/tools.tsx
+++ b/components/instructions/tools.tsx
@@ -36,6 +36,7 @@ import { STAKE_INSTRUCTIONS } from './programs/stake'
import dayjs from 'dayjs'
import { JUPITER_REF } from './programs/jupiterRef'
import { STAKE_SANCTUM_INSTRUCTIONS } from './programs/stakeSanctum'
+import { SYMMETRY_V2_INSTRUCTIONS } from './programs/symmetryV2'
/**
* Default governance program id instance
@@ -516,6 +517,7 @@ export const INSTRUCTION_DESCRIPTORS = {
...STAKE_INSTRUCTIONS,
...STAKE_SANCTUM_INSTRUCTIONS,
...JUPITER_REF,
+ ...SYMMETRY_V2_INSTRUCTIONS,
}
export async function getInstructionDescriptor(
diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts
index 2c19ce718d..279c368b5d 100644
--- a/hooks/useGovernanceAssets.ts
+++ b/hooks/useGovernanceAssets.ts
@@ -188,6 +188,10 @@ export default function useGovernanceAssets() {
name: 'Solend',
image: '/img/solend.png',
},
+ [PackageEnum.Symmetry]: {
+ name: 'Symmetry',
+ image: '/img/symmetry.png',
+ },
[PackageEnum.Squads]: {
name: 'Squads',
image: '/img/squads.png',
@@ -713,6 +717,30 @@ export default function useGovernanceAssets() {
name: 'Withdraw Funds',
packageId: PackageEnum.Solend,
},
+
+ /*
+ ███████ ██ ██ ███ ███ ███ ███ ███████ ████████ ██████ ██ ██
+ ██ ██ ██ ████ ████ ████ ████ ██ ██ ██ ██ ██ ██
+ ███████ ████ ██ ████ ██ ██ ████ ██ █████ ██ ██████ ████
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
+ ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██
+ */
+ [Instructions.SymmetryCreateBasket]: {
+ name: 'Create Basket',
+ packageId: PackageEnum.Symmetry,
+ },
+ [Instructions.SymmetryEditBasket]: {
+ name: 'Edit Basket',
+ packageId: PackageEnum.Symmetry,
+ },
+ [Instructions.SymmetryDeposit]: {
+ name: 'Deposit into Basket',
+ packageId: PackageEnum.Symmetry,
+ },
+ [Instructions.SymmetryWithdraw]: {
+ name: 'Withdraw from Basket',
+ packageId: PackageEnum.Symmetry,
+ },
/*
███████ ██████ ██ ██ █████ ██████ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██
diff --git a/package.json b/package.json
index 9bc2c427ba..c2a767b86a 100644
--- a/package.json
+++ b/package.json
@@ -111,6 +111,7 @@
"@sqds/mesh": "1.0.6",
"@switchboard-xyz/sbv2-lite": "0.2.4",
"@switchboard-xyz/solana.js": "3.2.5",
+ "@symmetry-hq/baskets-sdk": "0.0.45",
"@tailwindcss/forms": "0.5.3",
"@tailwindcss/line-clamp": "0.4.2",
"@tanstack/react-query": "4.14.3",
diff --git a/pages/dao/[symbol]/proposal/components/instructions/Symmetry/AddTokenToBasketModal.tsx b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/AddTokenToBasketModal.tsx
new file mode 100644
index 0000000000..3460856022
--- /dev/null
+++ b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/AddTokenToBasketModal.tsx
@@ -0,0 +1,73 @@
+import Modal from "@components/Modal"
+import Input from "@components/inputs/Input"
+import { useEffect, useState } from "react"
+
+
+const AddTokenToBasketModal = ({
+ open,
+ onClose,
+ supportedTokens,
+ onSelect
+}:{
+ open: boolean,
+ onClose: any,
+ supportedTokens: any,
+ onSelect: any
+}) => {
+ const [allTokens, setAllTokens] = useState(supportedTokens);
+ const [searchValue, setSearchValue] = useState('');
+
+ useEffect(() => {
+ if(searchValue.length > 0){
+ const filteredTokens = supportedTokens.filter((token: any) => {
+ return token.name.toLowerCase().includes(searchValue.toLowerCase()) || token.symbol.toLowerCase().includes(searchValue.toLowerCase())
+ })
+ setAllTokens(filteredTokens)
+ } else {
+ setAllTokens(supportedTokens)
+ }
+
+ }, [searchValue, supportedTokens])
+ return <>
+ {
+ open &&
+ onClose()}
+ >
+ Select a Token
+ setSearchValue(e.target.value)} type="text" />
+
+ {
+ allTokens.map((token, i) => {
+ return (
+
+ )
+ })
+ }
+
+
+ }
+ >
+
+}
+
+export default AddTokenToBasketModal;
\ No newline at end of file
diff --git a/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryCreateBasket.tsx b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryCreateBasket.tsx
new file mode 100644
index 0000000000..92a3d481be
--- /dev/null
+++ b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryCreateBasket.tsx
@@ -0,0 +1,322 @@
+import { ProgramAccount, Governance, serializeInstructionToBase64 } from '@solana/spl-governance'
+import { SymmetryCreateBasketForm, UiInstruction } from '@utils/uiTypes/proposalCreationTypes';
+import { useContext, useEffect, useState } from 'react';
+import Tooltip from '@components/Tooltip'
+import Input from '@components/inputs/Input'
+import { NewProposalContext } from '../../../new';
+import Switch from '@components/Switch';
+import { BasketsSDK } from "@symmetry-hq/baskets-sdk";
+import { createBasketIx } from "@symmetry-hq/baskets-sdk/dist/basketInstructions";
+import { useConnection } from '@solana/wallet-adapter-react';
+import Button from '@components/Button';
+import AddTokenToBasketModal from './AddTokenToBasketModal';
+import { TrashCan } from '@carbon/icons-react';
+import { PublicKey } from '@solana/web3.js';
+import { LinkIcon } from '@heroicons/react/solid';
+import useGovernanceAssets from '@hooks/useGovernanceAssets';
+import GovernedAccountSelect from '../../GovernedAccountSelect';
+
+const SymmetryCreateBasket = ({
+ index,
+ governance,
+}: {
+ index: number
+ governance: ProgramAccount
+}) => {
+ const {connection} = useConnection();
+ const { assetAccounts } = useGovernanceAssets()
+ const [form, setForm] = useState({
+ basketName: "",
+ basketSymbol: "",
+ basketMetadataUrl: "",
+ basketType: 2,
+ basketComposition: [],
+ rebalanceThreshold: 1000,
+ rebalanceSlippageTolerance: 50,
+ depositFee: 10,
+ feeCollectorAddress: "",
+ liquidityProvision: false,
+ liquidityProvisionRange: 0,
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const { handleSetInstructions } = useContext(NewProposalContext)
+ const shouldBeGoverned = !!(index !== 0 && governance)
+ const [supportedTokens, setSupportedTokens] = useState(null);
+ const [addTokenModal, setAddTokenModal] = useState(false);
+
+
+
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({ ...form, [propertyName]: value })
+ }
+
+ useEffect(() => {
+ BasketsSDK.init(connection).then((sdk) => {
+ setSupportedTokens(sdk.getTokenListData());
+ });
+ }, []);
+
+ useEffect(() => {
+ handleSetInstructions(
+ { governedAccount: form.governedAccount?.governance, getInstruction },
+ index
+ )
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
+ }, [form])
+
+ async function getInstruction(): Promise {
+
+ const basketParams = {
+ name: form.basketName,
+ symbol: form.basketSymbol,
+ uri: form.basketMetadataUrl,
+ hostPlatform: new PublicKey('4Vry5hGDmbwGwhjgegHmoitjMHniSotpjBFkRuDYHcDG'),
+ hostPlatformFee: 10,
+ //@ts-ignore
+ manager: form.governedAccount?.extensions.transferAddress,
+ managerFee: form.depositFee,
+ activelyManaged: 1,
+ rebalanceInterval: 3600,
+ rebalanceThreshold: form.rebalanceThreshold,
+ rebalanceSlippage: form.rebalanceSlippageTolerance,
+ lpOffsetThreshold: 0,
+ disableRebalance: false,
+ disableLp: !form.liquidityProvision,
+ composition: form.basketComposition.map((token) => {
+ return {
+ token: token.token,
+ weight: token.weight,
+ }
+ }),
+ feeDelegate: new PublicKey(form.feeCollectorAddress)
+ }
+ //@ts-ignore
+ const ix = await createBasketIx(connection, basketParams)
+ return {
+ serializedInstruction: serializeInstructionToBase64(ix),
+ isValid: true,
+ governance: form.governedAccount?.governance
+ };
+ }
+ return (
+ <>
+
+ Allow anyone to Deposit
+ handleSetForm({ value: x ? 2 : 1, propertyName: 'basketType' })}/>
+
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'basketName',
+ })
+ }
+ error={formErrors['basketName']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'basketSymbol',
+ })
+ }
+ error={formErrors['basketSymbol']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'basketMetadataUrl',
+ })
+ }
+ error={formErrors['basketMetadataUrl']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'depositFee',
+ })
+ }
+ error={formErrors['depositFee']}
+ />
+ x.isSol)}
+ onChange={(value) => {
+ handleSetForm({ value, propertyName: 'governedAccount' })
+ }}
+ value={form.governedAccount}
+ error={formErrors['governedAccount']}
+ shouldBeGoverned={shouldBeGoverned}
+ governance={governance}
+ type='wallet'
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'feeCollectorAddress',
+ })
+ }
+ error={formErrors['feeCollectorAddress']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'rebalanceSlippageTolerance',
+ })
+ }
+ error={formErrors['rebalanceSlippageTolerance']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'rebalanceThreshold',
+ })
+ }
+ error={formErrors['rebalanceThreshold']}
+ />
+
+
Enable Passive Rebalancing
+
Plugs into DEX Aggregators to passively allow for favorable swaps that keep the composition rebalanced.
+ Earns Extra Yield.
+
+
handleSetForm({ value: x, propertyName: 'liquidityProvision' })}/>
+
+
+
Basket Composition
+
+
+
Token
+
Mint Address
+
Token Weight
+
Actions
+
+ { form.basketComposition.length > 0 ?
+ form.basketComposition.map((token, i) => {
+ return (
+
+
+
+ {
+ token.name
+ }
+
+
+ {
+ token.symbol
+ }
+
+
+
+
+ {
+ token.token.toBase58().slice(0,6) + '...' + token.token.toBase58().slice(-6)
+ }
+
+
+
+
+
+
+ {
+ const newComposition = form.basketComposition;
+ newComposition[i].weight = parseFloat(evt.target.value);
+ setForm({
+ ...form,
+ basketComposition: newComposition
+ });
+ }
+ }
+ />
+
+
+ {
+ const newComposition = form.basketComposition;
+ newComposition.splice(i, 1);
+ setForm({
+ ...form,
+ basketComposition: newComposition
+ });
+ }}
+ />
+
+
+ )
+ })
+ :
+
Composition Empty. Add tokens below
+ }
+
+ {
+ form.basketComposition.length < 15 && supportedTokens && (
+
+ )
+ }
+ {
+ addTokenModal &&
+
setAddTokenModal(false)}
+ supportedTokens={supportedTokens}
+ onSelect={(token) => {
+ if(form.basketComposition.find((t) => t.token.toBase58() === token.tokenMint)
+ || form.basketComposition.length >= 15) {
+ return;
+ }
+
+ setForm({
+ ...form,
+ basketComposition: [
+ ...form.basketComposition,
+ {
+ name: token.name,
+ symbol: token.symbol,
+ token: new PublicKey(token.tokenMint),
+ weight: 0,
+ }
+ ]
+ });
+ setAddTokenModal(false);
+ }}
+ />
+ }
+
+
+ >
+ )
+}
+
+export default SymmetryCreateBasket;
\ No newline at end of file
diff --git a/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryDeposit.tsx b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryDeposit.tsx
new file mode 100644
index 0000000000..ea8cb31184
--- /dev/null
+++ b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryDeposit.tsx
@@ -0,0 +1,160 @@
+import { ProgramAccount, Governance, serializeInstructionToBase64 } from '@solana/spl-governance'
+import { SymmetryDepositForm, UiInstruction } from '@utils/uiTypes/proposalCreationTypes';
+import { useContext, useEffect, useState } from 'react';
+import Input from '@components/inputs/Input'
+import { NewProposalContext } from '../../../new';
+import { BasketsSDK, FilterOption } from "@symmetry-hq/baskets-sdk";
+import { buyBasketIx } from "@symmetry-hq/baskets-sdk/dist/basketInstructions";
+
+import { useConnection } from '@solana/wallet-adapter-react';
+import { PublicKey } from '@solana/web3.js';
+import useGovernanceAssets from '@hooks/useGovernanceAssets';
+import Select from '@components/inputs/Select';
+import GovernedAccountSelect from '../../GovernedAccountSelect';
+
+const SymmetryDeposit = ({
+ index,
+ governance,
+}: {
+ index: number
+ governance: ProgramAccount
+}) => {
+ const {connection} = useConnection();
+ const { assetAccounts } = useGovernanceAssets();
+ const [basketsSdk, setBasketSdk] = useState(undefined);
+ const [form, setForm] = useState({
+ governedAccount: undefined,
+ basketAddress: undefined,
+ depositToken: undefined,
+ depositAmount: 0,
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const { handleSetInstructions } = useContext(NewProposalContext);
+ const [managedBaskets, setManagedBaskets] = useState(undefined);
+ const shouldBeGoverned = !!(index !== 0 && governance)
+ const [assetAccountsLoaded, setAssetAccountsLoaded] = useState(false);
+
+
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({ ...form, [propertyName]: value })
+ }
+
+ useEffect(() => {
+ if(assetAccounts && assetAccounts.length > 0 && !assetAccountsLoaded)
+ setAssetAccountsLoaded(true);
+ }, [assetAccounts]);
+
+ useEffect(() => {
+ if(assetAccountsLoaded) {
+ const basketsOwnerAccounts: FilterOption[] = assetAccounts.filter(x => x.isSol).map((token) => {
+ return {
+ filterType: 'manager',
+ filterPubkey: token.pubkey
+ }
+ })
+ BasketsSDK.init(connection).then((sdk) => {
+ setBasketSdk(sdk);
+ sdk.findBaskets(basketsOwnerAccounts).then((baskets) => {
+ sdk.getCurrentCompositions(baskets).then((compositions) => {
+ const basketAccounts:any[] = [];
+ baskets.map((basket, i) => {
+
+ basketAccounts.push({
+ governedAccount: assetAccounts.filter(x => x.pubkey.toBase58() === basket.data.manager.toBase58())[0],
+ basket: basket,
+ composition: compositions[i]
+ });
+ });
+ setManagedBaskets(basketAccounts);
+ });
+ });
+ });
+ }
+ }, [assetAccountsLoaded]);
+
+ useEffect(() => {
+ handleSetInstructions(
+ { governedAccount: form.governedAccount?.governance, getInstruction },
+ index
+ )
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
+ }, [form])
+
+ async function getInstruction(): Promise {
+ const ix = await buyBasketIx(
+ connection,
+ //@ts-ignore
+ form.governedAccount?.governance.nativeTreasuryAddress,
+ form.basketAddress,
+ Number(form.depositAmount)
+ )
+
+ return {
+ serializedInstruction: serializeInstructionToBase64(ix),
+ isValid: true,
+ governance: form.governedAccount?.governance
+ };
+ }
+
+ return <>
+ {
+ managedBaskets ?
+
+ :
+ Loading Baskets Managed by the DAO
+ }
+ {
+ form.basketAddress &&
+
+ }
+ x.extensions.mint?.publicKey.toBase58() === 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')} /* Only allow USDC deposits for now */
+ onChange={(value) => {
+ handleSetForm({ value, propertyName: 'governedAccount' })
+ }}
+ value={form.governedAccount}
+ error={formErrors['governedAccount']}
+ shouldBeGoverned={shouldBeGoverned}
+ governance={governance}
+ type='token'
+ />
+
+ handleSetForm({ propertyName: 'depositAmount', value: e.target.value })}
+ error={formErrors['depositAmount']}
+ />
+ >
+}
+
+export default SymmetryDeposit;
\ No newline at end of file
diff --git a/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryEditBasket.tsx b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryEditBasket.tsx
new file mode 100644
index 0000000000..49c84f47e8
--- /dev/null
+++ b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryEditBasket.tsx
@@ -0,0 +1,382 @@
+import { ProgramAccount, Governance, serializeInstructionToBase64 } from '@solana/spl-governance'
+import { SymmetryEditBasketForm, UiInstruction } from '@utils/uiTypes/proposalCreationTypes';
+import { useContext, useEffect, useState } from 'react';
+import Tooltip from '@components/Tooltip'
+import Input from '@components/inputs/Input'
+import { NewProposalContext } from '../../../new';
+import Switch from '@components/Switch';
+import { BasketsSDK, FilterOption } from "@symmetry-hq/baskets-sdk";
+import { editBasketIx } from "@symmetry-hq/baskets-sdk/dist/basketInstructions";
+import { useConnection } from '@solana/wallet-adapter-react';
+import Button from '@components/Button';
+import AddTokenToBasketModal from './AddTokenToBasketModal';
+import { TrashCan } from '@carbon/icons-react';
+import { PublicKey } from '@solana/web3.js';
+import { LinkIcon } from '@heroicons/react/solid';
+import useGovernanceAssets from '@hooks/useGovernanceAssets';
+import Select from '@components/inputs/Select';
+
+const SymmetryEditBasket = ({
+ index,
+ governance,
+}: {
+ index: number
+ governance: ProgramAccount
+}) => {
+ const {connection} = useConnection();
+ const { assetAccounts } = useGovernanceAssets()
+ const [form, setForm] = useState({
+
+ basketName: "",
+ basketSymbol: "",
+ basketMetadataUrl: "",
+ basketType: 2,
+ basketComposition: [],
+ rebalanceThreshold: 1000,
+ rebalanceSlippageTolerance: 50,
+ depositFee: 10,
+ feeCollectorAddress: "",
+ liquidityProvision: false,
+ liquidityProvisionRange: 0,
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const { handleSetInstructions } = useContext(NewProposalContext);
+ const [managedBaskets, setManagedBaskets] = useState(undefined);
+ const shouldBeGoverned = !!(index !== 0 && governance)
+ const [addTokenModal, setAddTokenModal] = useState(false);
+ const [supportedTokens, setSupportedTokens] = useState(null);
+ const [assetAccountsLoaded, setAssetAccountsLoaded] = useState(false);
+
+
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({ ...form, [propertyName]: value })
+ }
+
+ const handleSelectBasket = (address: string) => {
+ const foundBasket = managedBaskets.filter(x => x.basket.ownAddress.toBase58() === address)[0]
+ if(!foundBasket) return;
+ const formData = {
+ governedAccount: foundBasket.governedAccount,
+ basketAddress: new PublicKey(address),
+ basketName: String.fromCharCode.apply(null, foundBasket.basket.data.name),
+ basketSymbol: String.fromCharCode.apply(null, foundBasket.basket.data.symbol),
+ basketMetadataUrl: String.fromCharCode.apply(null, foundBasket.basket.data.uri),
+ basketType: foundBasket.basket.data.activelyManaged.toNumber(),
+ basketComposition: foundBasket.composition.currentComposition.map((comp) => {
+ return {
+ name: comp.name,
+ symbol: comp.symbol,
+ token: new PublicKey(comp.mintAddress),
+ weight: comp.targetWeight
+ }
+ }),
+ rebalanceThreshold: foundBasket.basket.data.rebalanceThreshold.toNumber(),
+ rebalanceSlippageTolerance: foundBasket.basket.data.rebalanceSlippage.toNumber(),
+ depositFee: foundBasket.basket.data.managerFee.toNumber(),
+ feeCollectorAddress: foundBasket.basket.data.feeDelegate.toBase58(),
+ liquidityProvision: foundBasket.basket.data.disableLp.toNumber === 0,
+ liquidityProvisionRange: foundBasket.basket.data.lpOffsetThreshold.toNumber()
+ }
+ setForm(formData);
+ }
+
+ useEffect(() => {
+ if(assetAccounts && assetAccounts.length > 0 && !assetAccountsLoaded)
+ setAssetAccountsLoaded(true);
+ }, [assetAccounts]);
+
+ useEffect(() => {
+ if(assetAccountsLoaded) {
+ const basketsOwnerAccounts: FilterOption[] = assetAccounts.filter(x => x.isSol).map((token) => {
+ return {
+ filterType: 'manager',
+ filterPubkey: token.pubkey
+ }
+ })
+ if(basketsOwnerAccounts.length > 0) {
+ BasketsSDK.init(connection).then((sdk) => {
+ setSupportedTokens(sdk.getTokenListData());
+ sdk.findBaskets(basketsOwnerAccounts).then((baskets) => {
+ sdk.getCurrentCompositions(baskets).then((compositions) => {
+ const basketAccounts:any[] = [];
+ baskets.map((basket, i) => {
+
+ basketAccounts.push({
+ governedAccount: assetAccounts.filter(x => x.pubkey.toBase58() === basket.data.manager.toBase58())[0],
+ basket: basket,
+ composition: compositions[i]
+ });
+ });
+ setManagedBaskets(basketAccounts);
+ });
+ });
+ });
+ }
+ }
+ }, [assetAccountsLoaded]);
+
+ useEffect(() => {
+ handleSetInstructions(
+ { governedAccount: form.governedAccount?.governance, getInstruction },
+ index
+ )
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
+ }, [form])
+
+ async function getInstruction(): Promise {
+ const basketParams = {
+ //@ts-ignore
+ managerFee: form.depositFee,
+ activelyManaged: form.basketType,
+ rebalanceInterval: 3600,
+ rebalanceThreshold: form.rebalanceThreshold,
+ rebalanceSlippage: form.rebalanceSlippageTolerance,
+ lpOffsetThreshold: 0,
+ disableRebalance: false,
+ disableLp: !form.liquidityProvision,
+ composition: form.basketComposition.map((token) => {
+ return {
+ token: token.token,
+ weight: token.weight,
+ }
+ }),
+ feeDelegate: new PublicKey(form.feeCollectorAddress)
+ }
+ //@ts-ignore
+ const ix = await editBasketIx(connection, form.basketAddress, basketParams)
+
+ return {
+ serializedInstruction: serializeInstructionToBase64(ix),
+ isValid: true,
+ governance: form.governedAccount?.governance
+ };
+ }
+
+ return <>
+ {
+ managedBaskets ?
+
+ :
+ Loading Baskets Managed by the DAO
+ }
+
+ {
+ form.basketAddress &&
+ <>
+
+
+ View Basket on Symmetry
+
+
+ Allow anyone to Deposit
+ handleSetForm({ value: x ? 1 : 2, propertyName: 'basketType' })}/>
+
+ null}
+ error={formErrors['basketName']}
+ />
+ null }
+ error={formErrors['basketSymbol']}
+ />
+ null}
+ error={formErrors['basketMetadataUrl']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'depositFee',
+ })
+ }
+ error={formErrors['depositFee']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'feeCollectorAddress',
+ })
+ }
+ error={formErrors['feeCollectorAddress']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'rebalanceSlippageTolerance',
+ })
+ }
+ error={formErrors['rebalanceSlippageTolerance']}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'rebalanceThreshold',
+ })
+ }
+ error={formErrors['rebalanceThreshold']}
+ />
+
+
Enable Passive Rebalancing
+
Plugs into DEX Aggregators to passively allow for favorable swaps that keep the composition rebalanced.
+ Earns Extra Yield.
+
+
handleSetForm({ value: x, propertyName: 'liquidityProvision' })}/>
+
+
+
Basket Composition
+
+
+
Token
+
Mint Address
+
Token Weight
+
Actions
+
+ { form.basketComposition.length > 0 ?
+ form.basketComposition.map((token, i) => {
+ return (
+
+
+
+ {
+ token.name
+ }
+
+
+ {
+ token.symbol
+ }
+
+
+
+
+ {
+ token.token.toBase58().slice(0,6) + '...' + token.token.toBase58().slice(-6)
+ }
+
+
+
+
+
+
+ {
+ const newComposition = form.basketComposition;
+ newComposition[i].weight = parseFloat(evt.target.value);
+ setForm({
+ ...form,
+ basketComposition: newComposition
+ });
+ }
+ }
+ />
+
+
+ {
+ const newComposition = form.basketComposition;
+ newComposition.splice(i, 1);
+ setForm({
+ ...form,
+ basketComposition: newComposition
+ });
+ }}
+ />
+
+
+ )
+ })
+ :
+
Composition Empty. Add tokens below
+ }
+
+ {
+ form.basketComposition.length < 15 && supportedTokens && (
+
+ )
+ }
+ {
+ addTokenModal &&
+
setAddTokenModal(false)}
+ supportedTokens={supportedTokens}
+ onSelect={(token) => {
+ if(form.basketComposition.find((t) => t.token.toBase58() === token.tokenMint)
+ || form.basketComposition.length >= 15) {
+ return;
+ }
+
+ setForm({
+ ...form,
+ basketComposition: [
+ ...form.basketComposition,
+ {
+ name: token.name,
+ symbol: token.symbol,
+ token: new PublicKey(token.tokenMint),
+ weight: 0,
+ }
+ ]
+ });
+ setAddTokenModal(false);
+ }}
+ />
+ }
+
+
+ >
+ }
+ >
+}
+
+export default SymmetryEditBasket;
\ No newline at end of file
diff --git a/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryWithdraw.tsx b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryWithdraw.tsx
new file mode 100644
index 0000000000..ee562d3d9b
--- /dev/null
+++ b/pages/dao/[symbol]/proposal/components/instructions/Symmetry/SymmetryWithdraw.tsx
@@ -0,0 +1,187 @@
+import { ProgramAccount, Governance, serializeInstructionToBase64 } from '@solana/spl-governance'
+import { SymmetryWithdrawForm, UiInstruction } from '@utils/uiTypes/proposalCreationTypes';
+import { useContext, useEffect, useState } from 'react';
+import Input from '@components/inputs/Input'
+import { NewProposalContext } from '../../../new';
+import { BasketsSDK, FilterOption } from "@symmetry-hq/baskets-sdk";
+import { sellBasketIx } from "@symmetry-hq/baskets-sdk/dist/basketInstructions";
+import { useConnection } from '@solana/wallet-adapter-react';
+import useGovernanceAssets from '@hooks/useGovernanceAssets';
+import GovernedAccountSelect from '../../GovernedAccountSelect';
+import Select from '@components/inputs/Select';
+import { PublicKey } from '@solana/web3.js';
+
+const SymmetryWithdraw = ({
+ index,
+ governance,
+}: {
+ index: number
+ governance: ProgramAccount
+}) => {
+ const {connection} = useConnection();
+ const { assetAccounts } = useGovernanceAssets();
+ const [basketsSdk, setBasketSdk] = useState(undefined);
+ const [form, setForm] = useState({
+ governedAccount: undefined,
+ basketAddress: undefined,
+ withdrawAmount: 0,
+ withdrawType: 3
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const { handleSetInstructions } = useContext(NewProposalContext);
+ const [managedBaskets, setManagedBaskets] = useState(undefined);
+ const shouldBeGoverned = !!(index !== 0 && governance)
+ const [assetAccountsLoaded, setAssetAccountsLoaded] = useState(false);
+ const [selectedBasket, setSelectedBasket] = useState(undefined);
+
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({ ...form, [propertyName]: value })
+ }
+
+ const handleSelectBasket = (basket: any) => {
+ handleSetForm({ propertyName: 'basketAddress', value: basket.basket.ownAddress })
+ }
+
+ useEffect(() => {
+ if(assetAccounts && assetAccounts.length > 0 && !assetAccountsLoaded)
+ setAssetAccountsLoaded(true);
+ }, [assetAccounts]);
+
+ useEffect(() => {
+ if(assetAccountsLoaded) {
+ const basketsOwnerAccounts: FilterOption[] = assetAccounts.filter(x => x.isSol).map((token) => {
+ return {
+ filterType: 'manager',
+ filterPubkey: token.pubkey
+ }
+ })
+ BasketsSDK.init(connection).then((sdk) => {
+ setBasketSdk(sdk);
+ sdk.findBaskets(basketsOwnerAccounts).then((baskets) => {
+ sdk.getCurrentCompositions(baskets).then((compositions) => {
+ const basketAccounts:any[] = [];
+ baskets.map((basket, i) => {
+
+ basketAccounts.push({
+ governedAccount: assetAccounts.filter(x => x.pubkey.toBase58() === basket.data.manager.toBase58())[0],
+ basket: basket,
+ composition: compositions[i]
+ });
+ });
+ setManagedBaskets(basketAccounts);
+ });
+ });
+ });
+ }
+ }, [assetAccountsLoaded]);
+
+ useEffect(() => {
+ handleSetInstructions(
+ { governedAccount: form.governedAccount?.governance, getInstruction },
+ index
+ )
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
+ }, [form])
+
+ async function getInstruction(): Promise {
+ const ix = await sellBasketIx(
+ connection,
+ //@ts-ignore
+ form.governedAccount?.governance.nativeTreasuryAddress,
+ form.basketAddress,
+ Number(form.withdrawAmount),
+ form.withdrawType
+ )
+
+ return {
+ serializedInstruction: serializeInstructionToBase64(ix),
+ isValid: true,
+ governance: form.governedAccount?.governance
+ };
+ }
+
+ return <>
+ {
+ managedBaskets ?
+
+ :
+ Loading Baskets Managed by the DAO
+ }
+ {
+ form.basketAddress &&
+
+ }
+ {
+ form.basketAddress &&
+ x.isToken).filter(x => x.extensions.mint?.publicKey.toBase58() === selectedBasket?.composition?.basketTokenMint)}
+ onChange={(value) => {
+ handleSetForm({ value, propertyName: 'governedAccount' })
+ }}
+ value={form.governedAccount}
+ error={formErrors['governedAccount']}
+ shouldBeGoverned={shouldBeGoverned}
+ governance={governance}
+ type='token'
+ />
+ }
+
+ handleSetForm({ propertyName: 'withdrawAmount', value: e.target.value })}
+ error={formErrors['withdrawAmount']}
+ />
+
+ >
+}
+
+export default SymmetryWithdraw;
\ No newline at end of file
diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx
index b543278a20..3fdd06afbb 100644
--- a/pages/dao/[symbol]/proposal/new.tsx
+++ b/pages/dao/[symbol]/proposal/new.tsx
@@ -141,6 +141,10 @@ import PythRecoverAccount from './components/instructions/Pyth/PythRecoverAccoun
import { useVoteByCouncilToggle } from '@hooks/useVoteByCouncilToggle'
import BurnTokens from './components/instructions/BurnTokens'
import RemoveLockup from './components/instructions/Validators/removeLockup'
+import SymmetryCreateBasket from './components/instructions/Symmetry/SymmetryCreateBasket'
+import SymmetryEditBasket from './components/instructions/Symmetry/SymmetryEditBasket'
+import SymmetryDeposit from './components/instructions/Symmetry/SymmetryDeposit'
+import SymmetryWithdraw from './components/instructions/Symmetry/SymmetryWithdraw'
const TITLE_LENGTH_LIMIT = 130
// the true length limit is either at the tx size level, and maybe also the total account size level (I can't remember)
@@ -598,6 +602,10 @@ const New = () => {
[Instructions.RemoveServiceFromDID]: RemoveServiceFromDID,
[Instructions.RevokeGoverningTokens]: RevokeGoverningTokens,
[Instructions.SetMintAuthority]: SetMintAuthority,
+ [Instructions.SymmetryCreateBasket]: SymmetryCreateBasket,
+ [Instructions.SymmetryEditBasket]: SymmetryEditBasket,
+ [Instructions.SymmetryDeposit]: SymmetryDeposit,
+ [Instructions.SymmetryWithdraw]: SymmetryWithdraw,
}),
[governance?.pubkey?.toBase58()]
)
diff --git a/public/img/symmetry.png b/public/img/symmetry.png
new file mode 100644
index 0000000000..27ac77afe4
Binary files /dev/null and b/public/img/symmetry.png differ
diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts
index 57f3b0adef..172f480a25 100644
--- a/utils/uiTypes/proposalCreationTypes.ts
+++ b/utils/uiTypes/proposalCreationTypes.ts
@@ -24,6 +24,7 @@ export enum PackageEnum {
Pyth,
Serum,
Solend,
+ Symmetry,
Squads,
Switchboard,
VsrPlugin,
@@ -392,6 +393,10 @@ export enum Instructions {
SetMintAuthority,
SanctumDepositStake,
SanctumWithdrawStake,
+ SymmetryCreateBasket,
+ SymmetryEditBasket,
+ SymmetryDeposit,
+ SymmetryWithdraw
}
export interface ComponentInstructionData {
@@ -548,3 +553,59 @@ export interface DualFinanceVoteDepositForm {
realm: string | undefined
delegateToken: AssetAccount | undefined
}
+
+export interface SymmetryCreateBasketForm {
+ governedAccount?: AssetAccount,
+ basketType: number,
+ basketName: string,
+ basketSymbol: string,
+ basketMetadataUrl: string,
+ basketComposition: {
+ name: string,
+ symbol: string,
+ token: PublicKey;
+ weight: number;
+ }[],
+ rebalanceThreshold: number,
+ rebalanceSlippageTolerance: number,
+ depositFee: number,
+ feeCollectorAddress:string,
+ liquidityProvision: boolean,
+ liquidityProvisionRange: number,
+}
+
+
+export interface SymmetryEditBasketForm {
+ governedAccount?: AssetAccount,
+ basketAddress?: PublicKey,
+ basketType: number,
+ basketName: string,
+ basketSymbol: string,
+ basketMetadataUrl: string,
+ basketComposition: {
+ name: string,
+ symbol: string,
+ token: PublicKey;
+ weight: number;
+ }[],
+ rebalanceThreshold: number,
+ rebalanceSlippageTolerance: number,
+ depositFee: number,
+ feeCollectorAddress:string,
+ liquidityProvision: boolean,
+ liquidityProvisionRange: number,
+}
+
+export interface SymmetryDepositForm {
+ governedAccount?: AssetAccount,
+ basketAddress?: PublicKey,
+ depositToken?: PublicKey,
+ depositAmount: number,
+}
+
+export interface SymmetryWithdrawForm {
+ governedAccount?: AssetAccount,
+ basketAddress?: PublicKey,
+ withdrawAmount: number,
+ withdrawType: number
+}
diff --git a/yarn.lock b/yarn.lock
index 571ba93b48..1f83dbbc8d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5521,6 +5521,19 @@
dotenv "^16.3.1"
lodash "^4.17.21"
+"@symmetry-hq/baskets-sdk@0.0.45":
+ version "0.0.45"
+ resolved "https://registry.yarnpkg.com/@symmetry-hq/baskets-sdk/-/baskets-sdk-0.0.45.tgz#19d1c96bb0f659eccd7969aad3513bc4a7006a6b"
+ integrity sha512-w5w3yZl+qTwEAL8b21XGesuLX3HPpaQYyYkkRbwQBheqCJ8Geyg7/NyuZRF+8/a0XdubiQfW70QzBHZch6J0nA==
+ dependencies:
+ "@coral-xyz/anchor" "0.29.0"
+ "@metaplex-foundation/js" "0.19.4"
+ "@pythnetwork/client" "2.17.0"
+ "@solana/web3.js" "1.78.8"
+ "@types/crypto-js" "4.1.1"
+ axios "0.26.1"
+ crypto-js "4.1.1"
+
"@tailwindcss/forms@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.3.tgz#e4d7989686cbcaf416c53f1523df5225332a86e7"
@@ -5798,6 +5811,11 @@
dependencies:
"@types/node" "*"
+"@types/crypto-js@4.1.1":
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
+ integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
+
"@types/d3-array@*", "@types/d3-array@^3.0.3":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2"
@@ -8461,7 +8479,7 @@ crypto-hash@^1.3.0:
resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
-crypto-js@>=4.1.1, crypto-js@^3.1.9-1, crypto-js@^4.1.1, crypto-js@^4.2.0:
+crypto-js@4.1.1, crypto-js@>=4.1.1, crypto-js@^3.1.9-1, crypto-js@^4.1.1, crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==