-
-
- Alpha
-
diff --git a/src/components/NetworkSelector/index.tsx b/src/components/NetworkSelector/index.tsx
index c8c4e9dac..e909d5d33 100644
--- a/src/components/NetworkSelector/index.tsx
+++ b/src/components/NetworkSelector/index.tsx
@@ -15,7 +15,7 @@ interface NetworkButtonProps {
const NetworkButton = ({ selectedNetwork, isOpen, onClick, disabled }: NetworkButtonProps) => (
(
+
+

+ {comingSoon && (
+
Coming soon
+ )}
+
+);
+
+const ImageList = ({ images }: { images: ImageProps[] }) => (
+
+ {images.map((img) => (
+
+ ))}
+
+);
export function PoweredBy() {
return (
-
-
Powered by
-
-
-
-
+
);
}
diff --git a/src/components/SignIn/index.tsx b/src/components/SignIn/index.tsx
deleted file mode 100644
index de567715d..000000000
--- a/src/components/SignIn/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { FC } from 'preact/compat';
-import { Modal } from 'react-daisyui';
-
-interface SignInModalProps {
- signingPending: boolean;
- closeModal: () => void;
- handleSignIn: () => void;
-}
-
-export const SignInModal: FC = ({ signingPending, closeModal, handleSignIn }) => {
- if (!signingPending) {
- return null;
- }
-
- return (
-
-
- Sign In
-
-
-
- Please sign the message to log-in
-
-
-
-
-
-
- );
-};
diff --git a/src/components/SigningBox/index.tsx b/src/components/SigningBox/index.tsx
index f7f1e27a2..e5e9e6ff6 100644
--- a/src/components/SigningBox/index.tsx
+++ b/src/components/SigningBox/index.tsx
@@ -1,105 +1,125 @@
-import { Progress } from 'react-daisyui';
-import { FC } from 'preact/compat';
-import accountBalanceWalletIcon from '../../assets/account-balance-wallet.svg';
+import { FC, useState, useEffect } from 'preact/compat';
+import { motion, AnimatePresence } from 'framer-motion';
-import { SigningPhase } from '../../hooks/offramp/useMainProcess';
-import { isNetworkEVM, Networks } from '../../helpers/networks';
+import accountBalanceWalletIcon from '../../assets/account-balance-wallet-blue.svg';
+import { OfframpSigningPhase } from '../../types/offramp';
+import { isNetworkEVM } from '../../helpers/networks';
import { useNetwork } from '../../contexts/network';
import { Spinner } from '../Spinner';
-type ProgressStep = {
- started: string;
- signed: string;
- finished: string;
- approved: string;
+type ProgressConfig = {
+ [key in OfframpSigningPhase]: number;
};
-type SignatureConfig = {
- maxSignatures: number;
- getSignatureNumber: (step: SigningPhase) => string;
+const PROGRESS_CONFIGS: Record<'EVM' | 'NON_EVM', ProgressConfig> = {
+ EVM: {
+ started: 25,
+ approved: 50,
+ signed: 75,
+ finished: 100,
+ login: 15,
+ },
+ NON_EVM: {
+ started: 33,
+ finished: 100,
+ signed: 0,
+ approved: 0,
+ login: 15,
+ },
};
-const EVM_PROGRESS_CONFIG: ProgressStep = {
- started: '25',
- approved: '50',
- signed: '75',
- finished: '100',
-};
-
-const NON_EVM_PROGRESS_CONFIG: ProgressStep = {
- started: '33',
- finished: '100',
- signed: '0',
- approved: '0',
-};
-
-const EVM_SIGNATURE_CONFIG: SignatureConfig = {
- maxSignatures: 2,
- getSignatureNumber: (step: SigningPhase) => (step === 'started' ? '1' : '2'),
-};
-
-const NON_EVM_SIGNATURE_CONFIG: SignatureConfig = {
- maxSignatures: 1,
- getSignatureNumber: () => '1',
-};
-
-const getProgressConfig = (network: Networks): ProgressStep => {
- return isNetworkEVM(network) ? EVM_PROGRESS_CONFIG : NON_EVM_PROGRESS_CONFIG;
-};
-
-const getSignatureConfig = (network: Networks): SignatureConfig => {
- return isNetworkEVM(network) ? EVM_SIGNATURE_CONFIG : NON_EVM_SIGNATURE_CONFIG;
+const getSignatureDetails = (step: OfframpSigningPhase, isEVM: boolean) => {
+ if (!isEVM) return { max: 1, current: 1 };
+ if (step === 'login') return { max: 1, current: 1 };
+ if (step === 'started') return { max: 2, current: 1 };
+ return { max: 2, current: 2 };
};
interface SigningBoxProps {
- step?: SigningPhase;
+ step?: OfframpSigningPhase;
}
-const isValidStep = (step: SigningPhase | undefined, network: Networks): step is SigningPhase => {
+const isValidStep = (step: OfframpSigningPhase | undefined, isEVM: boolean): step is OfframpSigningPhase => {
if (!step) return false;
- if (!['started', 'approved', 'signed'].includes(step)) return false;
- if (!isNetworkEVM(network) && (step === 'approved' || step === 'signed')) return false;
+ if (step === 'finished' || step === 'login') return true;
+ if (!isEVM && (step === 'approved' || step === 'signed')) return false;
return true;
};
export const SigningBox: FC = ({ step }) => {
const { selectedNetwork } = useNetwork();
+ const isEVM = isNetworkEVM(selectedNetwork);
+ const progressConfig = isEVM ? PROGRESS_CONFIGS.EVM : PROGRESS_CONFIGS.NON_EVM;
- if (!isValidStep(step, selectedNetwork)) return null;
+ const [progress, setProgress] = useState(0);
+ const [signatureState, setSignatureState] = useState({ max: 0, current: 0 });
+ const [shouldExit, setShouldExit] = useState(false);
- const progressValue = getProgressConfig(selectedNetwork)[step];
- const { maxSignatures, getSignatureNumber } = getSignatureConfig(selectedNetwork);
+ useEffect(() => {
+ if (!isValidStep(step, isEVM)) return;
- return (
-
-
-
-
-
-
-
-

-
-
-
Please sign the transaction in
-
your connected wallet to proceed
-
-
+ if (step !== 'finished' && shouldExit) {
+ setShouldExit(false);
+ }
-
-
+ if (step === 'finished') {
+ setProgress(100);
+ setTimeout(() => setShouldExit(true), 2500);
+ return;
+ }
+
+ setProgress(progressConfig[step]);
+ setSignatureState(getSignatureDetails(step, isEVM));
+ }, [step, isEVM, progressConfig, shouldExit]);
+
+ return (
+
+ {!isValidStep(step, isEVM) || shouldExit ? null : (
+
+
+
+ Action Required
+
+
+
+
+
+

+
+
+
Please sign the transaction in
+
your connected wallet to proceed
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Waiting for signature {signatureState.current}/{signatureState.max}
+
+
-
-
-
-
-
+
+ )}
+
);
};
diff --git a/src/components/TrustedBy/index.tsx b/src/components/TrustedBy/index.tsx
index 1e22ed979..0e829c36f 100644
--- a/src/components/TrustedBy/index.tsx
+++ b/src/components/TrustedBy/index.tsx
@@ -3,10 +3,7 @@ import POLKADOT from '../../assets/trustedby/polkadot.svg';
import STELLAR from '../../assets/trustedby/stellar.svg';
import NABLA from '../../assets/trustedby/nabla.svg';
import WEB3 from '../../assets/trustedby/web3.svg';
-
-import MASTERCARD from '../../assets/payments/mastercard.svg';
-import SEPA from '../../assets/payments/sepa.svg';
-import VISA from '../../assets/payments/visa.svg';
+import { motion } from 'framer-motion';
interface ImageProps {
src: string;
@@ -16,15 +13,32 @@ interface ImageProps {
const Image = ({ src, alt, comingSoon }: ImageProps) => (
-

+
{comingSoon &&
Coming soon
}
);
const ImageList = ({ images }: { images: ImageProps[] }) => (
- {images.map((img) => (
-
+ {images.map((img, index) => (
+
+
+
))}
);
@@ -38,18 +52,11 @@ export const TrustedBy = () => {
{ src: WEB3, alt: 'Web3 Foundation logo' },
];
- const paymentImages = [
- { src: SEPA, alt: 'SEPA logo' },
- { src: MASTERCARD, alt: 'Mastercard logo', comingSoon: true },
- { src: VISA, alt: 'Visa logo', comingSoon: true },
- ];
-
return (
-
- Trusted by
+
);
diff --git a/src/components/UserBalance/index.tsx b/src/components/UserBalance/index.tsx
index f9fe9248c..9d293d6d0 100644
--- a/src/components/UserBalance/index.tsx
+++ b/src/components/UserBalance/index.tsx
@@ -1,7 +1,7 @@
import { InputTokenDetails } from '../../constants/tokenConfig';
import { useInputTokenBalance } from '../../hooks/useInputTokenBalance';
import { useVortexAccount } from '../../hooks/useVortexAccount';
-import wallet from '../../assets/wallet.svg';
+import wallet from '../../assets/wallet-bifold-outline.svg';
interface UserBalanceProps {
token: InputTokenDetails;
@@ -13,30 +13,26 @@ export const UserBalance = ({ token, onClick }: UserBalanceProps) => {
const inputTokenBalance = useInputTokenBalance({ fromToken: token });
if (isDisconnected || inputTokenBalance === undefined) {
- return <>>;
+ return ;
}
const showMaxButton = Number(inputTokenBalance) !== 0;
return (
-
- <>
-
-

-
- {inputTokenBalance} {token.assetSymbol}
-
- {showMaxButton && (
-
- )}
-
- >
-
+
+

+
+ {inputTokenBalance} {token.assetSymbol}
+
+ {showMaxButton && (
+
+ )}
+
);
};
diff --git a/src/components/WhyVortex/index.tsx b/src/components/WhyVortex/index.tsx
index 1a4fda8e8..132860222 100644
--- a/src/components/WhyVortex/index.tsx
+++ b/src/components/WhyVortex/index.tsx
@@ -13,7 +13,7 @@ const features: Feature[] = [
{
icon: DOLLAR,
title: 'Lower Fees',
- description: 'Offramping fees at just 0.5%, well below market average.',
+ description: 'Offramping fees as low as 0.25%.',
subtext: 'Keep more of what you earn.',
},
{
diff --git a/src/components/buttons/AssetButton/index.tsx b/src/components/buttons/AssetButton/index.tsx
index e33f38de2..36666fcfd 100644
--- a/src/components/buttons/AssetButton/index.tsx
+++ b/src/components/buttons/AssetButton/index.tsx
@@ -12,12 +12,12 @@ export function AssetButton({ assetIcon, tokenSymbol, onClick }: AssetButtonProp
return (
{(initializeFailedMessage || apiInitializeFailed) && (
- {initializeFailedMessage}
+
+
{initializeFailedMessage}
+
)}
@@ -450,7 +459,7 @@ export const SwapPage = () => {
-
+
{showCompareFees && fromToken && fromAmount && toToken && (
void;
+ setOfframpSigningPhase: (n: OfframpSigningPhase) => void;
trackEvent: (event: TrackableEvent) => void;
pendulumNode: { ss58Format: number; api: ApiPromise; decimals: number };
assetHubNode: { api: ApiPromise };
diff --git a/src/services/phases/polkadot/__tests__/eventsListener.test.tsx b/src/services/phases/polkadot/__tests__/eventsListener.test.tsx
new file mode 100644
index 000000000..8836d986e
--- /dev/null
+++ b/src/services/phases/polkadot/__tests__/eventsListener.test.tsx
@@ -0,0 +1,106 @@
+import { describe, expect, it } from 'vitest';
+
+import { ApiPromise, WsProvider } from '@polkadot/api';
+import { EventListener } from '../eventListener';
+import { ASSETHUB_WSS, PENDULUM_WSS } from '../../../../constants/constants';
+import { hexToString, stellarHexToPublic } from '../convert';
+
+export class TestableEventListener extends EventListener {
+ constructor(api: ApiPromise) {
+ super(api);
+ // We DO NOT WANT to actually subscribe, for testing.
+ this.unsubscribe();
+ }
+
+ // Analogous to what we would do in the callback of the subscription
+ processEventsForTest(events: any[]) {
+ events.forEach((event) => {
+ this.processEvents(event, this.pendingIssueEvents);
+ this.processEvents(event, this.pendingRedeemEvents);
+ this.processEvents(event, this.pendingXcmSentEvents);
+ });
+ }
+}
+
+async function getEventsFromBlock(api: ApiPromise, blockHash: string) {
+ const at = await api.at(blockHash);
+ const events = await at.query.system.events();
+ return events;
+}
+// Tests for EventListener's filters and parseEvent functions, specifically: Redeem.ExecuteRedeem and PolkadotXcm.Sent events.
+// Request redeem event parser is tested in spacewalk.test.tsx.
+describe('EventListener Tests', () => {
+ it('should detect successful polkadotXcm.Sent event', async () => {
+ const XCM_SENT_EVENT_BLOCK_HASH = '0xbac62e758e09f7e51fae2c74a8766c7e5e57a224d4a9ca8828e782ed9754340e';
+ const ORIGIN_XCM_ACCOUNT = '5DqTNJsGp6UayR5iHAZvH4zquY6ni6j35ZXLtJA6bXwsfixg';
+
+ const provider = new WsProvider(ASSETHUB_WSS);
+ const api = await ApiPromise.create({ provider });
+
+ const events = await getEventsFromBlock(api, XCM_SENT_EVENT_BLOCK_HASH);
+
+ const listener = new TestableEventListener(api);
+
+ const promise = listener.waitForXcmSentEvent(ORIGIN_XCM_ACCOUNT, 50000000); // We're not testing for timeout, so we set a high value.
+
+ // Bypass subscription and directly process the events
+ listener.processEventsForTest(events);
+
+ await expect(promise).resolves.toMatchObject({
+ originAddress: ORIGIN_XCM_ACCOUNT,
+ });
+ });
+
+ it('should detect successful ExecuteRedeem Event', async () => {
+ const EXECUTE_REDEEM_EVENT_BLOCK_HASH = '0x8c8dc97201be2fdc3aa050218a866e809aa0f2770a5e6dc413e41966c37d493a';
+ const REDEEM_ID = '0xa6c042f8816aaddd148fb2d24176312ca9a65bb331617fdfd33f8573a20e921e';
+ const REDEEMER = '6g7GLX4eBUCswt8ZaU3qkwntcu1NxkALZbyB4t1oU2WeKDFk';
+ const VAULT_ID = {
+ accountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs',
+ currencies: {
+ collateral: {
+ XCM: 10,
+ },
+ wrapped: {
+ Stellar: {
+ AlphaNum4: {
+ code: hexToString('0x41525300'),
+ issuer: stellarHexToPublic('0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1'),
+ },
+ },
+ },
+ },
+ };
+ const AMOUNT = 538780000000000;
+ const ASSET = {
+ Stellar: {
+ AlphaNum4: {
+ code: hexToString('0x41525300'),
+ issuer: stellarHexToPublic('0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1'),
+ },
+ },
+ };
+ const FEE = 0;
+ const TRANSFER_FEE = 0;
+
+ const provider = new WsProvider(PENDULUM_WSS);
+ const api = await ApiPromise.create({ provider });
+
+ const events = await getEventsFromBlock(api, EXECUTE_REDEEM_EVENT_BLOCK_HASH);
+
+ const listener = new TestableEventListener(api);
+
+ const promise = listener.waitForRedeemExecuteEvent(REDEEM_ID, 50000000);
+
+ listener.processEventsForTest(events);
+ await expect(promise).resolves.toMatchObject({
+ redeemId: REDEEM_ID,
+ redeemer: REDEEMER,
+ vaultId: VAULT_ID,
+ amount: AMOUNT,
+ asset: ASSET,
+ fee: FEE,
+ transferFee: TRANSFER_FEE,
+ });
+ });
+});
diff --git a/src/services/phases/polkadot/assethub.ts b/src/services/phases/polkadot/assethub.ts
index 8232160c4..7b9a78b11 100644
--- a/src/services/phases/polkadot/assethub.ts
+++ b/src/services/phases/polkadot/assethub.ts
@@ -7,6 +7,7 @@ import Big from 'big.js';
import { ExecutionContext, OfframpingState } from '../../offrampingFlow';
import { waitUntilTrue } from '../../../helpers/function';
import { getRawInputBalance } from './ephemeral';
+import { EventListener } from './eventListener';
export function createAssethubAssetTransfer(assethubApi: ApiPromise, receiverAddress: string, rawAmount: string) {
const receiverId = u8aToHex(decodeAddress(receiverAddress));
@@ -33,6 +34,10 @@ export async function executeAssetHubXCM(state: OfframpingState, context: Execut
const { assetHubNode, walletAccount, setOfframpSigningPhase } = context;
const { pendulumEphemeralAddress } = state;
+ // We wait for up to 1 minute. XCM event should appear on the same block.
+ const maxWaitingTimeMinutes = 1;
+ const maxWaitingTimeMs = maxWaitingTimeMinutes * 60 * 1000;
+
if (!walletAccount) {
throw new Error('Wallet account not available');
}
@@ -40,8 +45,6 @@ export async function executeAssetHubXCM(state: OfframpingState, context: Execut
throw new Error('AssetHub node not available');
}
- setOfframpSigningPhase?.('started');
-
const didInputTokenArrivedOnPendulum = async () => {
const inputBalanceRaw = await getRawInputBalance(state, context);
return inputBalanceRaw.gt(Big(0));
@@ -53,8 +56,16 @@ export async function executeAssetHubXCM(state: OfframpingState, context: Execut
if (assetHubXcmTransactionHash === undefined) {
const tx = createAssethubAssetTransfer(assetHubNode.api, pendulumEphemeralAddress, inputAmount.raw);
context.setOfframpSigningPhase('started');
+
+ const eventListener = EventListener.getEventListener(assetHubNode.api);
+ const xcmSentEventPromise = eventListener.waitForXcmSentEvent(walletAccount.address, maxWaitingTimeMs);
+
const { hash } = await tx.signAndSend(walletAccount.address, { signer: walletAccount.signer as Signer });
setOfframpSigningPhase?.('finished');
+
+ await xcmSentEventPromise;
+ eventListener.unsubscribe();
+
return { ...state, assetHubXcmTransactionHash: hash.toString() };
}
diff --git a/src/services/phases/polkadot/eventListener.tsx b/src/services/phases/polkadot/eventListener.tsx
index 5c7066eee..e876f5473 100644
--- a/src/services/phases/polkadot/eventListener.tsx
+++ b/src/services/phases/polkadot/eventListener.tsx
@@ -3,7 +3,7 @@
import { ApiPromise } from '@polkadot/api';
-import { parseEventRedeemExecution } from './eventParsers';
+import { parseEventRedeemExecution, parseEventXcmSent } from './eventParsers';
interface IPendingEvent {
filter: any;
@@ -13,8 +13,11 @@ interface IPendingEvent {
export class EventListener {
static eventListeners = new Map();
+ private unsubscribeHandle: (() => void) | null = null;
+
pendingIssueEvents: IPendingEvent[] = [];
pendingRedeemEvents: IPendingEvent[] = [];
+ pendingXcmSentEvents: IPendingEvent[] = [];
api: ApiPromise | undefined = undefined;
@@ -33,10 +36,11 @@ export class EventListener {
}
async initEventSubscriber() {
- this.api!.query.system.events((events) => {
+ this.unsubscribeHandle = await this.api!.query.system.events((events) => {
events.forEach((event) => {
this.processEvents(event, this.pendingIssueEvents);
this.processEvents(event, this.pendingRedeemEvents);
+ this.processEvents(event, this.pendingXcmSentEvents);
});
});
}
@@ -67,6 +71,32 @@ export class EventListener {
});
}
+ waitForXcmSentEvent(originAddress: string, maxWaitingTimeMs: number) {
+ const filter = (event: any) => {
+ if (event.event.section === 'polkadotXcm' && event.event.method === 'Sent') {
+ const eventParsed = parseEventXcmSent(event);
+ if (eventParsed.originAddress == originAddress) {
+ return eventParsed;
+ }
+ }
+ return null;
+ };
+
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(new Error(`Max waiting time exceeded for XCM Sent event from origin: ${originAddress}`));
+ }, maxWaitingTimeMs);
+
+ this.pendingXcmSentEvents.push({
+ filter,
+ resolve: (event) => {
+ clearTimeout(timeout);
+ resolve(event);
+ },
+ });
+ });
+ }
+
processEvents(event: any, pendingEvents: IPendingEvent[]) {
pendingEvents.forEach((pendingEvent, index) => {
const matchedEvent = pendingEvent.filter(event);
@@ -77,4 +107,19 @@ export class EventListener {
}
});
}
+
+ unsubscribe() {
+ if (this.unsubscribeHandle) {
+ this.unsubscribeHandle();
+ this.unsubscribeHandle = null;
+ }
+
+ this.pendingIssueEvents = [];
+ this.pendingRedeemEvents = [];
+ this.pendingXcmSentEvents = [];
+
+ EventListener.eventListeners.delete(this.api!);
+
+ this.api = undefined;
+ }
}
diff --git a/src/services/phases/polkadot/eventParsers.tsx b/src/services/phases/polkadot/eventParsers.tsx
index 861ed22d4..f69b9e496 100644
--- a/src/services/phases/polkadot/eventParsers.tsx
+++ b/src/services/phases/polkadot/eventParsers.tsx
@@ -1,6 +1,7 @@
// @todo: remove no-explicit-any
/* eslint-disable @typescript-eslint/no-explicit-any */
import Big from 'big.js';
+import { encodeAddress } from '@polkadot/util-crypto';
import { stellarHexToPublic, hexToString } from './convert';
@@ -54,6 +55,14 @@ export function parseEventRedeemExecution(event: any) {
return mappedData;
}
+export function parseEventXcmSent(event: any) {
+ const rawEventData = JSON.parse(event.event.data.toString());
+ const mappedData = {
+ originAddress: encodeAddress(rawEventData[0].interior.x1[0].accountId32.id.toString()),
+ };
+ return mappedData;
+}
+
function extractStellarAssetInfo(data: any) {
if ('stellarNative' in data.stellar) {
return {
diff --git a/src/types/offramp.ts b/src/types/offramp.ts
index 305299ffd..4dcbeecc8 100644
--- a/src/types/offramp.ts
+++ b/src/types/offramp.ts
@@ -5,7 +5,7 @@ import { OfframpingState } from '../services/offrampingFlow';
import { InputTokenType, OutputTokenType } from '../constants/tokenConfig';
import { ISep24Intermediate, IAnchorSessionParams } from './sep';
-export type OfframpSigningPhase = 'started' | 'approved' | 'signed' | 'finished';
+export type OfframpSigningPhase = 'login' | 'started' | 'approved' | 'signed' | 'finished';
export interface OfframpExecutionInput {
inputTokenType: InputTokenType;
diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts
index c9f0e358d..ec5eeb284 100644
--- a/src/wagmiConfig.ts
+++ b/src/wagmiConfig.ts
@@ -5,13 +5,22 @@ import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { createAppKit } from '@reown/appkit/react';
// If we have an Alchemy API key, we can use it to fetch data from Polygon, otherwise use the default endpoint
-// TODO we need to add better RPCs because metamask warns about unkown ones (defaults). Avalanche, base, etc.
const transports = config.alchemyApiKey
? {
[polygon.id]: http(`https://polygon-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
+ [mainnet.id]: http(`https://eth-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
+ [bsc.id]: http(`https://bnb-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
+ [arbitrum.id]: http(`https://arb-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
+ [base.id]: http(`https://base-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
+ [avalanche.id]: http(`https://avax-mainnet.g.alchemy.com/v2/${config.alchemyApiKey}`),
}
: {
[polygon.id]: http(''),
+ [mainnet.id]: http(''),
+ [bsc.id]: http(''),
+ [arbitrum.id]: http(''),
+ [base.id]: http(''),
+ [avalanche.id]: http(''),
};
// 2. Create a metadata object - optional