diff --git a/README.md b/README.md index f00ddcdeb..1df7db8b2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -PendulumPay is a gateway for cross-border payments. It is built on top of the Pendulum blockchain. +Vortex is a gateway for cross-border payments. It is built on top of the Pendulum blockchain. ## Run diff --git a/signer-service/src/api/routes/v1/index.js b/signer-service/src/api/routes/v1/index.js index 063b4afc9..db40eece3 100644 --- a/signer-service/src/api/routes/v1/index.js +++ b/signer-service/src/api/routes/v1/index.js @@ -82,4 +82,6 @@ router.use('/rating', ratingRoutes); */ router.use('/siwe', siweRoutes); +router.use('/ip', (request, response) => response.send(request.ip)); + module.exports = router; diff --git a/signer-service/src/config/express.js b/signer-service/src/config/express.js index 43ea97b13..de34b1bc2 100644 --- a/signer-service/src/config/express.js +++ b/signer-service/src/config/express.js @@ -33,7 +33,19 @@ app.use( // enable rate limiting // Set number of expected proxies -app.set('trust proxy', rateLimitNumberOfProxies); +app.set('trust proxy', true); + +app.use((req, res, next) => { + console.log({ + 'Raw Socket IP': req.socket.remoteAddress, + 'Express req.ip': req.ip, + 'X-Forwarded-For': req.headers['x-forwarded-for'], + 'X-Real-IP': req.headers['x-real-ip'], + 'Trust Proxy Setting': app.get('trust proxy'), + }); + next(); +}); + // Define rate limiter const limiter = rateLimit({ windowMs: rateLimitWindowMinutes * 60 * 1000, diff --git a/src/assets/account-balance-wallet-blue.svg b/src/assets/account-balance-wallet-blue.svg new file mode 100644 index 000000000..2f55a2d12 --- /dev/null +++ b/src/assets/account-balance-wallet-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/payments/sepa.svg b/src/assets/payments/sepa.svg index 06fca5066..919a48f3e 100644 --- a/src/assets/payments/sepa.svg +++ b/src/assets/payments/sepa.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/src/assets/wallet-bifold-outline.svg b/src/assets/wallet-bifold-outline.svg new file mode 100644 index 000000000..2197cdbf8 --- /dev/null +++ b/src/assets/wallet-bifold-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/wallet.svg b/src/assets/wallet.svg deleted file mode 100644 index a4be7376d..000000000 --- a/src/assets/wallet.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/components/FeeCollapse/index.tsx b/src/components/FeeCollapse/index.tsx index adcc7254f..e858b20a2 100644 --- a/src/components/FeeCollapse/index.tsx +++ b/src/components/FeeCollapse/index.tsx @@ -45,7 +45,7 @@ export const FeeCollapse: FC = ({ toAmount = Big(0), toToken, exc

Details

-
+

Your quote ({exchangeRate})

diff --git a/src/components/InputKeys/PoolListItem/index.tsx b/src/components/InputKeys/PoolListItem/index.tsx index 57e6fc010..eaab3a250 100644 --- a/src/components/InputKeys/PoolListItem/index.tsx +++ b/src/components/InputKeys/PoolListItem/index.tsx @@ -9,6 +9,7 @@ interface PoolListItemProps { isSelected?: boolean; onSelect: (tokenType: T) => void; assetIcon: AssetIconType; + name?: string; } export function PoolListItem({ @@ -17,6 +18,7 @@ export function PoolListItem({ isSelected, onSelect, assetIcon, + name, }: PoolListItemProps) { const tokenIcon = useGetAssetIcon(assetIcon); @@ -43,7 +45,7 @@ export function PoolListItem({ {tokenSymbol} - {tokenSymbol} + {name || tokenSymbol} ); diff --git a/src/components/InputKeys/SelectionModal.tsx b/src/components/InputKeys/SelectionModal.tsx index ac9c18635..d7996b8fc 100644 --- a/src/components/InputKeys/SelectionModal.tsx +++ b/src/components/InputKeys/SelectionModal.tsx @@ -13,7 +13,7 @@ interface PoolSelectorModalProps ext } interface PoolListProps { - definitions: { assetSymbol: string; type: T; assetIcon: AssetIconType }[]; + definitions: { assetSymbol: string; type: T; assetIcon: AssetIconType; name?: string }[]; onSelect: (tokenType: InputTokenType | OutputTokenType) => void; selected: InputTokenType | OutputTokenType; } @@ -46,7 +46,7 @@ function PoolList({ onSelect, defini placeholder="Find by name or address" />
- {definitions.map(({ assetIcon, assetSymbol, type }) => ( + {definitions.map(({ assetIcon, assetSymbol, type, name }) => ( ({ onSelect, defini tokenType={type} tokenSymbol={assetSymbol} assetIcon={assetIcon} + name={name} /> ))}
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 8e0b97e16..6a0b49892 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -82,12 +82,12 @@ const MobileMenuList: FC = ({ showMenu, closeMenu }) => ( const Links = () => (
    {links.map((link) => ( -
  • +
  • {link.title} @@ -103,10 +103,6 @@ export const Navbar = () => { return (
    - - Vortex Logo - 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) => ( ( +
    + {alt} + {comingSoon && ( +
    Coming soon
    + )} +
    +); + +const ImageList = ({ images }: { images: ImageProps[] }) => ( +
    + {images.map((img) => ( + + ))} +
    +); export function PoweredBy() { return ( -
    -

    Powered by

    - - Satoshipay - -
    +
    + +
    +

    Powered by

    + Satoshipay +
    +

    + + A Satoshipay Company + +

    +
    ); } 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 ( -
    -
    -
    -

    Action Required

    -
    - -
    -
    -
    - wallet account button -
    -
    -

    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

    +
    + +
    + +
    + wallet account button +
    +
    +

    Please sign the transaction in

    +

    your connected wallet to proceed

    +
    +
    + + +
    + +
    +
    +
    + + + +

    + Waiting for signature {signatureState.current}/{signatureState.max} +

    +
    -
    - -
    - -

    - Waiting for signature {getSignatureNumber(step)}/{maxSignatures} -

    -
    -
    -
    + + )} + ); }; 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) => (
    - {alt} + {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

    +
    + 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 ( -

    - <> -

    - Available - - {inputTokenBalance} {token.assetSymbol} - - {showMaxButton && ( - - )} -
    - -

    +
    + Available +

    + {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