From ac0faf8cdcce27e5761dae72d865e973266c1c6a Mon Sep 17 00:00:00 2001
From: onmax
Date: Wed, 26 Feb 2025 13:27:47 +0100
Subject: [PATCH 01/31] feat(demo): integrate demo mode
---
src/components/modals/BuyOptionsModal.vue | 4 +
src/components/modals/demos/DemoModalBuy.vue | 146 ++
.../modals/demos/DemoModalFallback.vue | 35 +
src/hub.ts | 3 +-
src/main.ts | 50 +-
src/network.ts | 9 +-
src/stores/Demo.ts | 1242 +++++++++++++++++
7 files changed, 1469 insertions(+), 20 deletions(-)
create mode 100644 src/components/modals/demos/DemoModalBuy.vue
create mode 100644 src/components/modals/demos/DemoModalFallback.vue
create mode 100644 src/stores/Demo.ts
diff --git a/src/components/modals/BuyOptionsModal.vue b/src/components/modals/BuyOptionsModal.vue
index a9a0604bf..e3070b727 100644
--- a/src/components/modals/BuyOptionsModal.vue
+++ b/src/components/modals/BuyOptionsModal.vue
@@ -202,6 +202,7 @@
+
+
diff --git a/src/components/modals/demos/DemoModalFallback.vue b/src/components/modals/demos/DemoModalFallback.vue
new file mode 100644
index 000000000..b9fdfa9de
--- /dev/null
+++ b/src/components/modals/demos/DemoModalFallback.vue
@@ -0,0 +1,35 @@
+
+
+
+ {{ $t('Nimiq Demo') }}
+
+
+
+ {{ $t('This is not a real Nimiq Wallet. It is just a demo so it is limited in functionality.') }}
+
+
+ {{ $t('You can open a free NIM account in less than a minute.') }}
+
+
+
+
+ {{ $t('Open Nimiq Wallet') }}
+
+
+
+
+
+
diff --git a/src/hub.ts b/src/hub.ts
index 4fc72bce7..6a7fe89ff 100644
--- a/src/hub.ts
+++ b/src/hub.ts
@@ -38,6 +38,7 @@ import { WELCOME_MODAL_LOCALSTORAGE_KEY, WELCOME_STAKING_MODAL_LOCALSTORAGE_KEY
import { usePwaInstallPrompt } from './composables/usePwaInstallPrompt';
import type { SetupSwapWithKycResult, SWAP_KYC_HANDLER_STORAGE_KEY } from './swap-kyc-handler'; // avoid bundling
import type { RelayServerInfo } from './lib/usdc/OpenGSN';
+import { DemoHubApi, checkIfDemoIsActive } from './stores/Demo';
export function shouldUseRedirects(ignoreSettings = false): boolean {
if (!ignoreSettings) {
@@ -114,7 +115,7 @@ function getBehavior({
// We can't use the reactive config via useConfig() here because that one can only be used after the composition-api
// plugin has been registered in Vue 2.
-const hubApi = new HubApi(Config.hubEndpoint);
+const hubApi = checkIfDemoIsActive() ? DemoHubApi.create() : new HubApi(Config.hubEndpoint);
hubApi.on(HubApi.RequestType.ONBOARD, async (accounts) => {
const { config } = useConfig();
diff --git a/src/main.ts b/src/main.ts
index 79f93398f..9d684e273 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -17,6 +17,7 @@ import { launchElectrum } from './electrum';
import { launchPolygon } from './ethers';
import { useAccountStore } from './stores/Account';
import { useFiatStore } from './stores/Fiat';
+import { useDemoStore } from './stores/Demo';
import { useSettingsStore } from './stores/Settings';
import router from './router';
import { i18n, loadLanguage } from './i18n/i18n-setup';
@@ -48,14 +49,21 @@ Vue.use(VuePortal, { name: 'Portal' });
async function start() {
initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
- await initStorage(); // Must be awaited before starting Vue
- initTrials(); // Must be called after storage was initialized, can affect Config
- // Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
- // background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code anyways.
- await initHubApi();
- syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.
-
- serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
+ const { isDemoEnabled } = useDemoStore();
+
+ if (!isDemoEnabled.value) {
+ await initStorage(); // Must be awaited before starting Vue
+ initTrials(); // Must be called after storage was initialized, can affect Config
+ // Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
+ // background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code
+ // anyways.
+ await initHubApi();
+ syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.
+
+ serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
+ } else {
+ useDemoStore().initialize(router);
+ }
// Update exchange rates every 2 minutes or every 10 minutes, depending on whether the Wallet is currently actively
// used. If an update takes longer than that time due to a provider's rate limit, wait until the update succeeds
@@ -94,11 +102,15 @@ async function start() {
const { language } = useSettingsStore();
loadLanguage(language.value);
- startSentry();
+ if (!isDemoEnabled.value) {
+ startSentry();
+ }
const { config } = useConfig();
- if (config.environment !== ENV_MAIN) {
+ if (isDemoEnabled.value) {
+ document.title = 'Nimiq Wallet Demo';
+ } else if (config.environment !== ENV_MAIN) {
document.title = 'Nimiq Testnet Wallet';
}
@@ -107,15 +119,17 @@ async function start() {
initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
});
- watch(() => {
- if (!config.oasis.apiEndpoint) return;
- initOasisApi(config.oasis.apiEndpoint);
- });
+ if (!isDemoEnabled.value) {
+ watch(() => {
+ if (!config.oasis.apiEndpoint) return;
+ initOasisApi(config.oasis.apiEndpoint);
+ });
- watch(() => {
- if (!config.ten31Pass.enabled) return;
- initKycConnection();
- });
+ watch(() => {
+ if (!config.ten31Pass.enabled) return;
+ initKycConnection();
+ });
+ }
// Make reactive config accessible in components
Vue.prototype.$config = config;
diff --git a/src/network.ts b/src/network.ts
index 4d8002bc8..bb150a40d 100644
--- a/src/network.ts
+++ b/src/network.ts
@@ -12,6 +12,7 @@ import { AddStakeEvent, ApiValidator, RawValidator, useStakingStore } from './st
import { ENV_MAIN, STAKING_CONTRACT_ADDRESS } from './lib/Constants';
import { reportToSentry } from './lib/Sentry';
import { useAccountStore } from './stores/Account';
+import { useDemoStore } from './stores/Demo';
let isLaunched = false;
let clientPromise: Promise;
@@ -102,6 +103,7 @@ export async function launchNetwork() {
const transactionsStore = useTransactionsStore();
const addressStore = useAddressStore();
const stakingStore = useStakingStore();
+ const demoStore = useDemoStore();
const subscribedAddresses = new Set();
@@ -118,7 +120,7 @@ export async function launchNetwork() {
network$.fetchingTxHistory--;
async function updateBalances(addresses: string[] = [...balances.keys()]) {
- if (!addresses.length) return;
+ if (!addresses.length || demoStore.isDemoEnabled) return;
await client.waitForConsensusEstablished();
const accounts = await retry(() => client.getAccounts(addresses)).catch(reportFor('getAccounts'));
if (!accounts) return;
@@ -303,6 +305,7 @@ export async function launchNetwork() {
})();
function transactionListener(plain: PlainTransactionDetails) {
+ if (demoStore.isDemoEnabled) return;
if (plain.recipient === STAKING_CONTRACT_ADDRESS) {
if (plain.data.type === 'add-stake') {
if (!balances.has(plain.sender) && 'staker' in plain.data) {
@@ -340,6 +343,7 @@ export async function launchNetwork() {
}
function subscribe(addresses: string[]) {
+ if (demoStore.isDemoEnabled) return false;
client.addTransactionListener(transactionListener, addresses);
updateBalances(addresses);
updateStakes(addresses);
@@ -349,6 +353,7 @@ export async function launchNetwork() {
// Subscribe to new addresses (for balance updates and transactions)
// Also remove logged out addresses from fetched (so that they get fetched on next login)
watch(addressStore.addressInfos, () => {
+ if (demoStore.isDemoEnabled) return;
const newAddresses: string[] = [];
const removedAddresses = new Set(subscribedAddresses);
@@ -380,6 +385,7 @@ export async function launchNetwork() {
// Fetch transactions for active address
watch([addressStore.activeAddress, txFetchTrigger], ([activeAddress, trigger]) => {
+ if (demoStore.isDemoEnabled) return;
const address = activeAddress as string | null;
if (!address || fetchedAddresses.value.includes(address)) return;
addFetchedAddress(address);
@@ -428,6 +434,7 @@ export async function launchNetwork() {
// Fetch transactions for proxies
const proxyStore = useProxyStore();
watch(proxyStore.networkTrigger, () => {
+ if (demoStore.isDemoEnabled) return;
const newProxies: string[] = [];
const addressesToSubscribe: string[] = [];
for (const proxyAddress of proxyStore.allProxies.value) {
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
new file mode 100644
index 000000000..c9c609184
--- /dev/null
+++ b/src/stores/Demo.ts
@@ -0,0 +1,1242 @@
+/* eslint-disable max-len */
+import { createStore } from 'pinia';
+import VueRouter from 'vue-router';
+import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
+import HubApi from '@nimiq/hub-api';
+import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
+import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
+import { STAKING_CONTRACT_ADDRESS } from '@/lib/Constants';
+import { AccountType, useAccountStore } from '@/stores/Account';
+import { AddressType, useAddressStore } from '@/stores/Address';
+import { toSecs, useTransactionsStore } from '@/stores/Transactions';
+import { useBtcTransactionsStore } from '@/stores/BtcTransactions';
+import { useUsdtTransactionsStore, TransactionState as UsdtTransactionState } from '@/stores/UsdtTransactions';
+import { useUsdcTransactionsStore, TransactionState as UsdcTransactionState } from '@/stores/UsdcTransactions';
+import { useStakingStore } from '@/stores/Staking';
+import { useAccountSettingsStore } from '@/stores/AccountSettings';
+import { usePolygonAddressStore } from '@/stores/PolygonAddress';
+import Config from 'config';
+import { SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
+import { SwapState, useSwapsStore } from '@/stores/Swaps';
+import { useBtcAddressStore } from './BtcAddress';
+import { useContactsStore } from './Contacts';
+import { useBtcLabelsStore } from './BtcLabels';
+import { useUsdcContactsStore } from './UsdcContacts';
+import { useUsdtContactsStore } from './UsdtContacts';
+
+export type DemoState = {
+ active: boolean,
+};
+
+// The query param that activates the demo. e.g. https://wallet.nimiq.com/?demo=
+const DEMO_PARAM = 'demo';
+
+const DemoFallbackModal = () =>
+ import(
+ /* webpackChunkName: 'demo-hub-fallback-modal' */
+ '@/components/modals/demos/DemoModalFallback.vue'
+ );
+
+const DemoPurchaseModal = () =>
+ import(
+ /* webpackChunkName: 'account-menu-modal' */
+ '@/components/modals/demos/DemoModalBuy.vue'
+ );
+
+// Replacing the enum with a simple object to avoid backticks
+const DemoModal = {
+ Fallback: 'demo-fallback',
+ Buy: 'demo-buy',
+};
+
+// Addresses for demo:
+const demoNimAddress = 'NQ57 2814 7L5B NBBD 0EU7 EL71 HXP8 M7H8 MHKD';
+const demoBtcAddress = '1XYZDemoAddress';
+const demoPolygonAddress = '0xabc123DemoPolygonAddress';
+const buyFromAddress = 'NQ04 JG63 HYXL H3QF PPNA 7ED7 426M 3FQE FHE5';
+
+// We keep this as our global/final balance, which should result from the transactions
+const nimInitialBalance = 14041800000; // 14,041,800,000 - 14 april, 2018
+const usdtInitialBalance = 5000000000; // 5000 USDT (6 decimals)
+const usdcInitialBalance = 3000000000; // 3000 USDC (6 decimals)
+
+// We keep a reference to the router here.
+let demoRouter: VueRouter;
+
+/**
+ * Main store for the demo environment.
+ */
+export const useDemoStore = createStore({
+ id: 'demo-store',
+ state: (): DemoState => ({
+ active: checkIfDemoIsActive(),
+ }),
+ getters: {
+ isDemoEnabled: (state) => state.active,
+ },
+ actions: {
+ /**
+ * Initializes the demo environment and sets up various routes, data, and watchers.
+ */
+ async initialize(router: VueRouter) {
+ // eslint-disable-next-line no-console
+ console.warn('[Demo] Initializing demo environment...');
+
+ demoRouter = router;
+
+ insertCustomDemoStyles();
+ rewriteDemoRoutes();
+ setupVisualCues();
+ addDemoModalRoutes();
+ interceptFetchRequest();
+
+ setupDemoAddresses();
+ setupDemoAccount();
+
+ generateFakeNimTransactions();
+
+ const { addTransactions: addBtcTransactions } = useBtcTransactionsStore();
+ addBtcTransactions(generateFakeBtcTransactions());
+
+ attachIframeListeners();
+ replaceStakingFlow();
+ replaceBuyNimFlow();
+ },
+
+ /**
+ * Adds a pretend buy transaction to show a deposit coming in.
+ */
+ async buyDemoNim(amount: number) {
+ const { addTransactions } = useTransactionsStore();
+ addTransactions([
+ createFakeTransaction({
+ value: amount,
+ recipient: demoNimAddress,
+ sender: buyFromAddress,
+ data: {
+ type: 'raw',
+ raw: encodeTextToHex('Online Purchase'),
+ },
+ }),
+ ]);
+ },
+ },
+});
+
+/**
+ * Checks if the 'demo' query param is present in the URL.
+ */
+export function checkIfDemoIsActive() {
+ return window.location.search.includes(DEMO_PARAM);
+}
+
+/**
+ * Creates a style tag to add demo-specific CSS.
+ */
+function insertCustomDemoStyles() {
+ const styleElement = document.createElement('style');
+ styleElement.innerHTML = demoCSS;
+ document.head.appendChild(styleElement);
+}
+
+/**
+ * Sets up a router guard to handle redirects for the demo environment.
+ */
+function rewriteDemoRoutes() {
+ demoRouter.beforeEach((to, _from, next) => {
+ // Redirect certain known paths to the Buy demo modal
+ if (to.path === '/simplex' || to.path === '/moonpay') {
+ return next({
+ path: `/${DemoModal.Buy}`,
+ query: { ...to.query, [DEMO_PARAM]: '' },
+ });
+ }
+
+ // Ensure the ?demo param is in place
+ if (to.query.demo === '') return next();
+ return next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' } });
+ });
+}
+
+/**
+ * Observes the home view to attach a highlight to some buttons for demonstration purposes.
+ */
+function setupVisualCues() {
+ const highlightTargets = [
+ ['.sidebar .trade-actions button', { top: '-18px', right: '-4px' }],
+ ['.sidebar .swap-tooltip button', { top: '-18px', right: '-8px' }],
+ ['.actions .staking-button', { top: '-2px', right: '-2px' }],
+ ] as const;
+
+ const mutationObserver = new MutationObserver(() => {
+ if (window.location.pathname !== '/') return;
+
+ highlightTargets.forEach(([selector, position]) => {
+ const target = document.querySelector(selector);
+ if (!target || target.querySelector('.demo-highlight-badge')) return;
+
+ const wrapper = document.createElement('div');
+ wrapper.classList.add('demo-highlight-badge');
+ wrapper.style.top = position.top;
+ wrapper.style.right = position.right;
+ const circle = document.createElement('div');
+
+ wrapper.appendChild(circle);
+ target.appendChild(wrapper);
+ });
+ });
+
+ mutationObserver.observe(document.body, { childList: true, subtree: true });
+}
+
+/**
+ * Adds routes pointing to our demo modals.
+ */
+function addDemoModalRoutes() {
+ demoRouter.addRoute('root', {
+ name: DemoModal.Fallback,
+ path: `/${DemoModal.Fallback}`,
+ components: { modal: DemoFallbackModal },
+ props: { modal: true },
+ });
+ demoRouter.addRoute('root', {
+ name: DemoModal.Buy,
+ path: `/${DemoModal.Buy}`,
+ components: { modal: DemoPurchaseModal },
+ props: { modal: true },
+ });
+}
+
+/**
+ * Setup and initialize the demo data for all currencies.
+ */
+function setupDemoAddresses() {
+ const { setAddressInfos } = useAddressStore();
+ setAddressInfos([
+ {
+ label: 'Demo Account',
+ type: AddressType.BASIC,
+ address: demoNimAddress,
+ balance: nimInitialBalance,
+ },
+ ]);
+
+ // Setup Polygon addresses and balances
+ const { setAddressInfos: setPolygonAddressInfos } = usePolygonAddressStore();
+ setPolygonAddressInfos([{
+ address: demoPolygonAddress,
+ balanceUsdc: usdcInitialBalance,
+ balanceUsdcBridged: 0,
+ balanceUsdtBridged: usdtInitialBalance,
+ pol: 1,
+ }]);
+
+ // Setup polygon token transactions
+ generateFakePolygonTransactions();
+}
+
+/**
+ * Creates a fake main account referencing our demo addresses.
+ */
+function setupDemoAccount() {
+ const { addAccountInfo, setActiveCurrency } = useAccountStore();
+ const { setStablecoin, setKnowsAboutUsdt } = useAccountSettingsStore();
+
+ // Setup account info with both USDC and USDT addresses
+ addAccountInfo({
+ id: 'demo-account-1',
+ type: AccountType.BIP39,
+ label: 'Demo Main Account',
+ fileExported: true,
+ wordsExported: true,
+ addresses: [demoNimAddress],
+ btcAddresses: { internal: [demoBtcAddress], external: [demoBtcAddress] },
+ polygonAddresses: [demoPolygonAddress, demoPolygonAddress],
+ uid: 'demo-uid-1',
+ });
+
+ // Pre-select USDC as the default stablecoin and mark USDT as known
+ setStablecoin(CryptoCurrency.USDC);
+ setKnowsAboutUsdt(true);
+
+ setActiveCurrency(CryptoCurrency.NIM);
+}
+
+/**
+ * Generates fake NIM transactions spanning the last ~4 years.
+ * They net to the global nimInitialBalance.
+ * We use a helper function to calculate each transaction value so it doesn't end in 0.
+ */
+function generateFakeNimTransactions() {
+ // We'll define total fractions so the net sum is 1.
+ const txDefinitions = [
+ { fraction: -0.14, daysAgo: 1442, description: 'New Designer Backpack for Hiking Trip' },
+ { fraction: -0.05, daysAgo: 1390, description: 'Streaming Subscription - Watch Anything Anytime', recipientLabel: 'Stream & Chill Co.' },
+ { fraction: 0.1, daysAgo: 1370, description: 'Sold Vintage Camera to Photography Enthusiast', recipientLabel: 'Retro Photo Guy' },
+ { fraction: -0.2, daysAgo: 1240, description: 'Groceries at Farmers Market' },
+ { fraction: 0.2, daysAgo: 1185, description: 'Birthday Gift from Uncle Bob', recipientLabel: 'Uncle Bob 🎁' },
+ { fraction: 0.3, daysAgo: 1120, description: 'Website Design Freelance Project', recipientLabel: 'Digital Nomad Inc.' },
+ { fraction: 0.02, daysAgo: 980, description: 'Lunch Money Payback from Alex' },
+ { fraction: 0.07, daysAgo: 940, description: 'Community Raffle Prize' },
+ { fraction: -0.15, daysAgo: 875, description: 'Car Repair at Thunder Road Garage', recipientLabel: 'FixMyCar Workshop' },
+ { fraction: -0.3, daysAgo: 780, description: 'Quarterly Apartment Rent', recipientLabel: 'Skyview Properties' },
+ { fraction: -0.1, daysAgo: 720, description: 'Anniversary Dinner at Skyline Restaurant' },
+ { fraction: -0.02, daysAgo: 650, description: 'Digital Book: "Blockchain for Beginners"' },
+ { fraction: -0.03, daysAgo: 580, description: 'Music Festival Weekend Pass' },
+ { fraction: 0.05, daysAgo: 540, description: 'Refund for Cancelled Flight' },
+ { fraction: 0.5, daysAgo: 470, description: 'Software Development Project Payment', recipientLabel: 'Tech Solutions Ltd' },
+ { fraction: 0.02, daysAgo: 390, description: 'Coffee Shop Reward Program Refund' },
+ { fraction: -0.11, daysAgo: 320, description: 'Custom Tailored Suit Purchase' },
+ { fraction: 0.06, daysAgo: 270, description: 'Website Testing Gig Payment' },
+ { fraction: -0.08, daysAgo: 210, description: 'Electric Scooter Rental for Month' },
+ { fraction: -0.12, daysAgo: 180, description: 'Online Course: "Advanced Crypto Trading"' },
+ { fraction: 0.05, daysAgo: 120, description: 'Sold Digital Artwork', recipientLabel: 'NFT Collector' },
+ { fraction: -0.1, daysAgo: 90, description: 'Quarterly Utility Bills', recipientLabel: 'City Power & Water' },
+ { fraction: -0.1, daysAgo: 45, description: 'Winter Wardrobe Shopping' },
+ ];
+
+ // Calculate sum of existing transactions to ensure they add up to exactly 1
+ const existingSum = txDefinitions.reduce((sum, def) => sum + def.fraction, 0);
+ const remainingFraction = 1 - existingSum;
+
+ // Add the final balancing transaction with appropriate description
+ if (Math.abs(remainingFraction) > 0.001) { // Only add if there's a meaningful amount to balance
+ txDefinitions.push({
+ fraction: remainingFraction,
+ daysAgo: 14,
+ description: remainingFraction > 0
+ ? 'Blockchain Hackathon Prize!'
+ : 'Annual Software Subscription Renewal',
+ recipientLabel: remainingFraction > 0 ? 'Crypto Innovation Fund' : undefined,
+ });
+ }
+
+ const { addTransactions } = useTransactionsStore();
+ const { setContact } = useContactsStore();
+
+ for (const def of txDefinitions) {
+ let txValue = Math.floor(nimInitialBalance * def.fraction);
+ // Adjust so it doesn't end in a 0 digit
+ while (txValue > 0 && txValue % 10 === 0) {
+ txValue -= 1;
+ }
+
+ const hex = encodeTextToHex(def.description);
+ const to32Bytes = (h: string) => h.padStart(64, '0').slice(-64);
+ const address = KeyPair.derive(PrivateKey.fromHex(to32Bytes(hex))).toAddress().toUserFriendlyAddress();
+ const recipient = def.fraction > 0 ? demoNimAddress : address;
+ const sender = def.fraction > 0 ? address : demoNimAddress;
+ const data = { type: 'raw', raw: hex } as const;
+ const value = Math.abs(txValue);
+ const timestamp = calculateDaysAgo(def.daysAgo);
+ const tx: Partial = { value, recipient, sender, timestamp, data };
+ addTransactions([createFakeTransaction(tx)]);
+
+ // Add contact if a recipientLabel is provided
+ if (def.recipientLabel && def.fraction < 0) {
+ setContact(address, def.recipientLabel);
+ }
+ }
+}
+
+/**
+ * Generates fake Bitcoin transactions spanning 5.5 years
+ */
+function generateFakeBtcTransactions() {
+ // Define transaction history with a similar structure to other currencies
+ const txDefinitions = [
+ {
+ daysAgo: 2000,
+ value: 20000000, // 0.2 BTC
+ incoming: true,
+ description: 'Initial BTC purchase from exchange',
+ address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
+ label: 'Satoshi Exchange',
+ },
+ {
+ daysAgo: 1600,
+ value: 15000000, // 0.15 BTC
+ incoming: true,
+ description: 'Mining pool payout',
+ address: '1Hz7vQrRjnu3z9k7gxDYhKjEmABqChDvJr',
+ label: 'Genesis Mining Pool',
+ },
+ {
+ daysAgo: 1200,
+ value: 19000000, // 0.19 BTC
+ incoming: false,
+ description: 'Purchase from online marketplace',
+ address: '1LxKe5kKdgGVwXukEgqFxh6DrCXF2Pturc',
+ label: 'Digital Bazaar Shop',
+ },
+ {
+ daysAgo: 800,
+ value: 30000000, // 0.3 BTC
+ incoming: true,
+ description: 'Company payment for consulting',
+ address: '1N7aecJuKGDXzYK8CgpnNRYxdhZvXPxp3B',
+ label: 'Corporate Treasury',
+ },
+ {
+ daysAgo: 365,
+ value: 15000000, // 0.15 BTC
+ incoming: false,
+ description: 'Auto-DCA investment program',
+ address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz',
+ },
+ {
+ daysAgo: 180,
+ value: 7500000, // 0.075 BTC
+ incoming: true,
+ description: 'P2P sale of digital goods',
+ address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS',
+ },
+ {
+ daysAgo: 60,
+ value: 5000000, // 0.05 BTC
+ incoming: true,
+ description: 'Recent purchase from exchange',
+ address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
+ },
+ ];
+
+ const { setSenderLabel } = useBtcLabelsStore();
+
+ // Convert to BTC transaction format with inputs/outputs
+ const transactions = [];
+ const knownUtxos = new Map();
+ let txCounter = 1;
+
+ for (const def of txDefinitions) {
+ // Create a transaction hash for this transaction
+ const txHash = `btc-tx-${txCounter++}`;
+
+ // Only add labels to select transactions to make the history look realistic
+ if (def.label) {
+ setSenderLabel(def.address, def.label);
+ }
+
+ const tx = {
+ isCoinbase: false,
+ inputs: [
+ {
+ address: def.incoming ? def.address : demoBtcAddress,
+ outputIndex: 0,
+ index: 0,
+ script: 'abcd',
+ sequence: 4294967295,
+ transactionHash: def.incoming ? txHash : getUTXOToSpend(knownUtxos)?.txHash || txHash,
+ witness: ['abcd'],
+ },
+ ],
+ outputs: def.incoming
+ ? [
+ {
+ value: def.value,
+ address: demoBtcAddress,
+ script: 'abcd',
+ index: 0,
+ },
+ ]
+ : [
+ {
+ value: def.value,
+ address: def.address,
+ script: 'abcd',
+ index: 0,
+ },
+ { // Change output
+ value: 900000,
+ address: demoBtcAddress,
+ script: 'abcd',
+ index: 1,
+ },
+ ],
+ transactionHash: txHash,
+ version: 1,
+ vsize: 200,
+ weight: 800,
+ locktime: 0,
+ confirmations: Math.max(1, Math.floor(10 - def.daysAgo / 200)),
+ replaceByFee: false,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ state: ElectrumTransactionState.CONFIRMED,
+ };
+
+ // Update UTXOs for this transaction
+ updateUTXOs(knownUtxos, tx);
+
+ transactions.push(tx);
+ }
+
+ // Set up the address with current UTXOs
+ const { addAddressInfos } = useBtcAddressStore();
+ addAddressInfos([{
+ address: demoBtcAddress,
+ txoCount: transactions.length + 2, // Total number of outputs received
+ utxos: Array.from(knownUtxos.values()),
+ }]);
+
+ return transactions;
+}
+
+/**
+ * Tracks UTXO changes for BTC transactions
+ */
+function updateUTXOs(knownUtxos: Map, tx: any) {
+ // Remove spent inputs
+ for (const input of tx.inputs) {
+ if (input.address === demoBtcAddress) {
+ const utxoKey = `${input.transactionHash}:${input.outputIndex}`;
+ knownUtxos.delete(utxoKey);
+ }
+ }
+
+ // Add new outputs for our address
+ for (const output of tx.outputs) {
+ if (output.address === demoBtcAddress) {
+ const utxoKey = `${tx.transactionHash}:${output.index}`;
+ knownUtxos.set(utxoKey, {
+ transactionHash: tx.transactionHash,
+ index: output.index,
+ witness: {
+ script: output.script,
+ value: output.value,
+ },
+ });
+ }
+ }
+}
+
+/**
+ * Helper to get a UTXO to spend
+ */
+function getUTXOToSpend(knownUtxos: Map) {
+ if (knownUtxos.size === 0) return null;
+ const utxo = knownUtxos.values().next().value;
+ return {
+ txHash: utxo.transactionHash,
+ index: utxo.index,
+ value: utxo.witness.value,
+ };
+}
+
+/**
+ * Generates fake transactions for both USDC and USDT on Polygon
+ */
+function generateFakePolygonTransactions() {
+ // Generate USDC transactions
+ const usdcTxDefinitions = [
+ { fraction: 0.3, daysAgo: 910, incoming: true, label: 'Yield Farming Protocol' },
+ { fraction: -0.05, daysAgo: 840 },
+ { fraction: 0.2, daysAgo: 730, incoming: true, label: 'Virtual Worlds Inc.' },
+ { fraction: -0.1, daysAgo: 650 },
+ { fraction: 0.4, daysAgo: 480, incoming: true, label: 'Innovation DAO' },
+ { fraction: -0.15, daysAgo: 360, label: 'MetaGames Marketplace' },
+ { fraction: 0.15, daysAgo: 180, incoming: true },
+ { fraction: -0.08, daysAgo: 90 },
+ ];
+
+ // Calculate sum of existing transactions and add balancing transaction
+ const usdcExistingSum = usdcTxDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const usdcRemainingFraction = 1 - usdcExistingSum;
+
+ // Add balancing transaction
+ if (Math.abs(usdcRemainingFraction) > 0.001) {
+ usdcTxDefinitions.push({
+ fraction: usdcRemainingFraction,
+ daysAgo: 30,
+ incoming: usdcRemainingFraction > 0,
+ label: usdcRemainingFraction > 0 ? 'CryptoCreators Guild' : 'Annual Platform Subscription',
+ });
+ }
+
+ // Generate USDT transactions
+ const usdtTxDefinitions = [
+ { fraction: 0.4, daysAgo: 360, incoming: true, label: 'LaunchPad Exchange' },
+ { fraction: -0.1, daysAgo: 320 },
+ { fraction: 0.2, daysAgo: 280, incoming: true, label: 'Crypto Consultants LLC' },
+ { fraction: -0.15, daysAgo: 210 },
+ { fraction: 0.3, daysAgo: 150, incoming: true },
+ { fraction: -0.05, daysAgo: 90 },
+ { fraction: 0.1, daysAgo: 45, incoming: true, label: 'Chain Testers' },
+ { fraction: -0.12, daysAgo: 20 },
+ ];
+
+ // Calculate sum of existing transactions and add balancing transaction
+ const usdtExistingSum = usdtTxDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const usdtRemainingFraction = 1 - usdtExistingSum;
+
+ // Add balancing transaction
+ if (Math.abs(usdtRemainingFraction) > 0.001) {
+ usdtTxDefinitions.push({
+ fraction: usdtRemainingFraction,
+ daysAgo: 7,
+ incoming: usdtRemainingFraction > 0,
+ label: usdtRemainingFraction > 0 ? 'Protocol Testing Reward' : 'Annual Service Renewal',
+ });
+ }
+
+ // Generate and add USDC transactions
+ const { addTransactions: addUsdcTxs } = useUsdcTransactionsStore();
+ addUsdcTxs(generateTokenTransactions(usdcTxDefinitions, usdcInitialBalance, Config.polygon.usdc.tokenContract, UsdcTransactionState.CONFIRMED, useUsdcContactsStore));
+
+ // Generate and add USDT transactions
+ const { addTransactions: addUsdtTxs } = useUsdtTransactionsStore();
+ addUsdtTxs(generateTokenTransactions(usdtTxDefinitions, usdtInitialBalance, Config.polygon.usdt_bridged.tokenContract, UsdtTransactionState.CONFIRMED, useUsdtContactsStore));
+}
+
+/**
+ * Shared function to generate token transactions for Polygon tokens
+ */
+function generateTokenTransactions(txDefinitions: any, initialBalance:number, tokenContract:string, confirmState:any, contactsStore:any) {
+ const transactions = [];
+ const { setContact } = contactsStore();
+
+ for (const def of txDefinitions) {
+ const value = Math.floor(initialBalance * Math.abs(def.fraction));
+ const randomAddress = `0x${Math.random().toString(16).slice(2, 42)}`;
+ const sender = def.incoming ? randomAddress : demoPolygonAddress;
+ const recipient = def.incoming ? demoPolygonAddress : randomAddress;
+
+ // Add contacts for select transactions (only if label is provided)
+ if (def.label) {
+ const addressToLabel = def.incoming ? randomAddress : randomAddress;
+ setContact(addressToLabel, def.label);
+ }
+
+ transactions.push({
+ token: tokenContract,
+ transactionHash: `token-tx-${Math.random().toString(36).substr(2, 9)}`,
+ logIndex: transactions.length,
+ sender,
+ recipient,
+ value,
+ state: confirmState,
+ blockHeight: 1000000 + transactions.length,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ });
+ }
+
+ return transactions;
+}
+
+enum MessageEventName {
+ FlowChange = 'FlowChange'
+}
+
+/**
+ * Listens for messages from iframes (or parent frames) about changes in the user flow.
+ */
+function attachIframeListeners() {
+ window.addEventListener('message', (event) => {
+ if (!event.data || typeof event.data !== 'object') return;
+ const { kind, data } = event.data as DemoFlowMessage;
+ if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
+ useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
+ demoRouter.push(demoRoutes[data]);
+ }
+ });
+
+ demoRouter.afterEach((to) => {
+ const match = Object.entries(demoRoutes).find(([, route]) => route === to.path);
+ if (!match) return;
+ window.parent.postMessage({ kind: MessageEventName.FlowChange, data: match[0] as DemoFlowType }, '*');
+ });
+}
+
+/**
+ * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
+ */
+function replaceBuyNimFlow() {
+ const targetSelector = '.sidebar .trade-actions';
+ let observing = false;
+ const observer = new MutationObserver(() => {
+ if (observing) return;
+
+ const target = document.querySelector(targetSelector);
+ if (!target) return;
+ observing = true;
+
+ target.innerHTML = '';
+
+ const btn1 = document.createElement('button');
+ btn1.className = 'nq-button-s inverse';
+ btn1.style.flex = '1';
+ btn1.addEventListener('click', () => {
+ demoRouter.push('/buy');
+ });
+ btn1.innerHTML = 'Buy';
+ const btn2 = document.createElement('button');
+ btn2.className = 'nq-button-s inverse';
+ btn2.style.flex = '1';
+ btn2.disabled = true;
+ btn2.innerHTML = 'Sell';
+ target.appendChild(btn1);
+ target.appendChild(btn2);
+ });
+
+ demoRouter.afterEach((to) => {
+ if (to.path.startsWith('/')) {
+ observer.observe(document.body, { childList: true, subtree: true });
+ } else {
+ observing = false;
+ observer.disconnect();
+ }
+ });
+}
+
+/**
+ * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
+ */
+function replaceStakingFlow() {
+ const targetSelector = '.stake-graph-page .stake-button';
+ let observing = false;
+ const observer = new MutationObserver(() => {
+ if (observing) return;
+
+ const target = document.querySelector(targetSelector);
+ if (!target) return;
+
+ // remove previous listeners by cloning the element and replacing the original
+ const newElement = target.cloneNode(true) as HTMLButtonElement;
+ target.parentNode!.replaceChild(newElement, target);
+ newElement.removeAttribute('disabled');
+ observing = true;
+
+ newElement.addEventListener('click', async () => {
+ const { setStake } = useStakingStore();
+ const { activeValidator } = useStakingStore();
+ const amountInput = document.querySelector('.nq-input') as HTMLInputElement;
+ const amount = Number.parseFloat(amountInput.value.replaceAll(' ', '')) * 1e5;
+
+ const { address: validatorAddress } = activeValidator.value!;
+ demoRouter.push('/');
+ await new Promise((resolve) => { window.setTimeout(resolve, 100); });
+ setStake({
+ activeBalance: amount,
+ inactiveBalance: nimInitialBalance - amount,
+ address: demoNimAddress,
+ retiredBalance: 0,
+ validator: validatorAddress,
+ });
+ const { addTransactions } = useTransactionsStore();
+ addTransactions([
+ createFakeTransaction({
+ value: amount,
+ recipient: STAKING_CONTRACT_ADDRESS,
+ sender: demoNimAddress,
+ data: {
+ type: 'add-stake',
+ raw: '',
+ staker: demoNimAddress,
+ },
+ }),
+ ]);
+ });
+ });
+
+ demoRouter.afterEach((to) => {
+ if (to.path === '/staking') {
+ observer.observe(document.body, { childList: true, subtree: true });
+ } else {
+ observing = false;
+ observer.disconnect();
+ }
+ });
+}
+
+/**
+ * Creates a fake transaction. Each call increments a global counter for the hash and block heights.
+ */
+let txCounter = 0;
+let currentHead = 0;
+function createFakeTransaction(tx: Partial) {
+ return {
+ network: 'mainnet',
+ state: 'confirmed',
+ transactionHash: `0x${(txCounter++).toString(16)}`,
+ value: 50000000,
+ recipient: '',
+ fee: 0,
+ feePerByte: 0,
+ format: 'basic',
+ sender: '',
+ senderType: 'basic',
+ recipientType: 'basic',
+ validityStartHeight: currentHead++,
+ blockHeight: currentHead++,
+ flags: 0,
+ timestamp: Date.now(),
+ proof: { type: 'raw', raw: '' },
+ size: 0,
+ valid: true,
+ ...tx,
+ } as PlainTransactionDetails;
+}
+
+/**
+ * Returns the hex encoding of a UTF-8 string.
+ */
+function encodeTextToHex(text: string): string {
+ const utf8Array = Utf8Tools.stringToUtf8ByteArray(text);
+ return Array.from(utf8Array)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('');
+}
+
+// We pick a random but fixed time-of-day offset for each day.
+const baseDate = new Date();
+baseDate.setHours(0, 0, 0, 0);
+const baseDateMs = baseDate.getTime();
+const oneDayMs = 24 * 60 * 60 * 1000;
+
+/**
+ * Generates a past timestamp for a given number of days ago, adding a predictable random offset.
+ */
+function calculateDaysAgo(days: number): number {
+ const x = Math.sin(days) * 10000;
+ const fractionalPart = x - Math.floor(x);
+ const randomPart = Math.floor(fractionalPart * oneDayMs);
+ return baseDateMs - days * oneDayMs - randomPart;
+}
+
+/**
+ * Flow type for our demo environment, e.g. buy, swap, stake.
+ */
+type DemoFlowType = 'buy' | 'swap' | 'stake';
+
+/**
+ * The expected message structure for flow-change events between frames.
+ */
+type DemoFlowMessage = { kind: 'FlowChange', data: DemoFlowType };
+
+/**
+ * Maps each flow type to a specific route path in our app.
+ */
+const demoRoutes: Record = {
+ buy: '/buy',
+ swap: '/swap/NIM-BTC',
+ stake: '/staking',
+};
+
+/**
+ * CSS for the special demo elements, stored in a normal string (no backticks).
+ */
+const demoCSS = `
+.transaction-list .month-label > :where(.fetching, .failed-to-fetch) {
+ display: none;
+}
+
+#app > div > .wallet-status-button.nq-button-pill {
+ display: none;
+}
+
+.staking-button .tooltip.staking-feature-tip {
+ display: none;
+}
+
+.modal.transaction-modal .confirmed .tooltip.info-tooltip {
+ display: none;
+}
+
+.send-modal-footer .footer-notice {
+ display: none;
+}
+
+.demo-highlight-badge {
+ position: absolute;
+ width: 34px;
+ height: 34px;
+ z-index: 5;
+ pointer-events: none;
+}
+
+.demo-highlight-badge > div {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background: rgba(31, 35, 72, 0.1);
+ border: 1.5px solid rgba(255, 255, 255, 0.5);
+ border-radius: 50%;
+ backdrop-filter: blur(3px);
+}
+
+.demo-highlight-badge > div::before {
+ content: "";
+ position: absolute;
+ inset: 5px;
+ background: rgba(31, 35, 72, 0.3);
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(3px);
+ border-radius: 12px;
+}
+
+.demo-highlight-badge > div::after {
+ content: "";
+ position: absolute;
+ inset: 11.6px;
+ background: rgba(255, 255, 255);
+ border-radius: 50%;
+}
+`;
+
+/**
+ * Intercepts fetch request:
+ * - fastspot.apiEndpoint will return a mock limit response
+ * - estimate endpoint will return mock estimate response
+ * - the rest of requests will be passed through
+ */
+function interceptFetchRequest() {
+ const originalFetch = window.fetch;
+ window.fetch = async (...args: Parameters) => {
+ if (typeof args[0] !== 'string') return originalFetch(...args);
+ if (args[0].startsWith('/')) return originalFetch(...args);
+
+ const url = new URL(args[0] as string);
+ const isFastspotRequest = url.host === Config.fastspot.apiEndpoint;
+ const isLimitsRequest = url.pathname.includes('/limits');
+ const isEstimateRequest = url.pathname.includes('/estimate');
+ const isAssetsRequest = url.pathname.includes('/assets');
+
+ if (!isFastspotRequest) {
+ return originalFetch(...args);
+ }
+
+ if (isLimitsRequest) {
+ // Return mock limits with higher values and proper structure
+ const json = {
+ asset: 'USD',
+ daily: '50000',
+ dailyRemaining: '49000',
+ monthly: '100000',
+ monthlyRemaining: '98000',
+ swap: '10000',
+ current: '9800',
+ };
+ return new Response(JSON.stringify(json));
+ }
+
+ if (isAssetsRequest) {
+ // Return mock assets data with fees
+ const json = {
+ BTC: {
+ enabled: true,
+ fee: 0.0001, // BTC network fee
+ minAmount: 0.001,
+ maxAmount: 1,
+ },
+ NIM: {
+ enabled: true,
+ fee: 0.01, // NIM network fee
+ minAmount: 1,
+ maxAmount: 1000000,
+ },
+ USDC_MATIC: {
+ enabled: true,
+ fee: 0.1, // USDC fee
+ minAmount: 1,
+ maxAmount: 50000,
+ },
+ USDT_MATIC: {
+ enabled: true,
+ fee: 0.1, // USDT fee
+ minAmount: 1,
+ maxAmount: 50000,
+ },
+ };
+ return new Response(JSON.stringify(json));
+ }
+
+ if (isEstimateRequest) {
+ // Parse the estimate request parameters
+ const searchParams = new URLSearchParams(url.search);
+ const from = searchParams.get('from');
+ const to = searchParams.get('to');
+ const value = Number(searchParams.get('value'));
+
+ // Validate the request
+ if (!from || !to || !value || Number.isNaN(value)) {
+ return new Response(JSON.stringify({
+ error: 'Invalid request parameters',
+ status: 400,
+ }), { status: 400 });
+ }
+
+ // Calculate fees based on the value
+ const networkFee = Math.floor(value * 0.0005); // 0.05% network fee
+ const escrowFee = Math.floor(value * 0.001); // 0.1% escrow fee
+
+ // Calculate the destination amount using our mock rates
+ const estimatedAmount = calculateEstimatedAmount(from, to, value);
+
+ // Note: These fee calculations are simplified for demo purposes
+ const toNetworkFee = Math.floor(estimatedAmount * 0.0005);
+ const toEscrowFee = Math.floor(estimatedAmount * 0.001);
+
+ // Create mock estimate response with a valid structure
+ const estimate = {
+ from: {
+ asset: from,
+ amount: value,
+ fee: 0, // Network-specific fee (e.g., BTC mining fee)
+ serviceNetworkFee: networkFee,
+ serviceEscrowFee: escrowFee,
+ },
+ to: {
+ asset: to,
+ amount: estimatedAmount,
+ fee: 0, // Network-specific fee
+ serviceNetworkFee: toNetworkFee,
+ serviceEscrowFee: toEscrowFee,
+ },
+ serviceFeePercentage: 0.025, // 2.5% service fee
+ expires: Date.now() + 30000, // 30 seconds expiry
+ swapProfit: 0,
+ };
+
+ return new Response(JSON.stringify(estimate));
+ }
+
+ return originalFetch(...args);
+ };
+}
+
+/**
+ * Calculate mock estimated amount for swaps based on predefined rates
+ */
+function calculateEstimatedAmount(fromAsset: string, toAsset: string, value: number): number {
+ // Define mock exchange rates (realistic market rates)
+ const rates: Record = {
+ [`${SwapAsset.NIM}-${SwapAsset.BTC}`]: 0.00000004, // 1 NIM = 0.00000004 BTC
+ [`${SwapAsset.NIM}-${SwapAsset.USDC_MATIC}`]: 0.0012, // 1 NIM = 0.0012 USDC
+ [`${SwapAsset.NIM}-${SwapAsset.USDT_MATIC}`]: 0.0012, // 1 NIM = 0.0012 USDT
+ [`${SwapAsset.BTC}-${SwapAsset.NIM}`]: 25000000, // 1 BTC = 25,000,000 NIM
+ [`${SwapAsset.BTC}-${SwapAsset.USDC_MATIC}`]: 30000, // 1 BTC = 30,000 USDC
+ [`${SwapAsset.BTC}-${SwapAsset.USDT_MATIC}`]: 30000, // 1 BTC = 30,000 USDT
+ [`${SwapAsset.USDC_MATIC}-${SwapAsset.NIM}`]: 833.33, // 1 USDC = 833.33 NIM
+ [`${SwapAsset.USDC_MATIC}-${SwapAsset.BTC}`]: 0.000033, // 1 USDC = 0.000033 BTC
+ [`${SwapAsset.USDC_MATIC}-${SwapAsset.USDT_MATIC}`]: 1, // 1 USDC = 1 USDT
+ [`${SwapAsset.USDT_MATIC}-${SwapAsset.NIM}`]: 833.33, // 1 USDT = 833.33 NIM
+ [`${SwapAsset.USDT_MATIC}-${SwapAsset.BTC}`]: 0.000033, // 1 USDT = 0.000033 BTC
+ [`${SwapAsset.USDT_MATIC}-${SwapAsset.USDC_MATIC}`]: 1, // 1 USDT = 1 USDC
+ };
+
+ const rate = rates[`${fromAsset}-${toAsset}`];
+ if (!rate) {
+ // If no direct rate, check reverse rate
+ const reverseRate = rates[`${toAsset}-${fromAsset}`];
+ if (reverseRate) {
+ return Math.floor(value * (1 / reverseRate));
+ }
+ return value; // 1:1 if no rate defined
+ }
+
+ return Math.floor(value * rate);
+}
+
+let swapCounter = 0;
+
+/**
+ * Replacement of the Hub API class to capture and redirect calls to our demo modals instead.
+ */
+export class DemoHubApi extends HubApi {
+ static create(): DemoHubApi {
+ const instance = new DemoHubApi();
+ return new Proxy(instance, {
+ get(target, prop: keyof HubApi) {
+ if (typeof target[prop] === 'function') {
+ return async (...args: Parameters) => {
+ const requestName = String(prop);
+ const [firstArg] = args;
+ // eslint-disable-next-line no-console
+ console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
+
+ if (requestName === 'on') {
+ return;
+ }
+ if (requestName === 'setupSwap') {
+ const leftColumn = document.querySelector('.swap-amounts .left-column');
+ const leftAmountElement = leftColumn!.querySelector('.width-value');
+ const leftValue = Number((leftAmountElement as HTMLDivElement).innerText);
+ const leftAsset = leftColumn!.querySelector('.ticker')!.innerHTML.toUpperCase() as SwapAsset;
+
+ const rightColumn = document.querySelector('.swap-amounts .right-column');
+ const rightAmountElement = rightColumn!.querySelector('.width-value');
+ const rightValue = Number((rightAmountElement as HTMLDivElement).innerText);
+ const rightAsset = rightColumn!.querySelector('.ticker')!.innerHTML.toUpperCase() as SwapAsset;
+
+ const swapHash = `0x${(++swapCounter).toString(16)}`;
+ const { setActiveSwap, setSwap } = useSwapsStore();
+
+ // Create initial swap state
+ const swap = {
+ id: `${swapCounter}`,
+ contracts: {},
+ from: {
+ asset: leftAsset,
+ amount: leftValue,
+ fee: 0,
+ serviceEscrowFee: Math.floor(leftValue * 0.001), // 0.1% fee
+ serviceNetworkFee: Math.floor(leftValue * 0.0005), // 0.05% network fee
+ },
+ to: {
+ asset: rightAsset,
+ amount: rightValue,
+ fee: 0,
+ serviceNetworkFee: Math.floor(rightValue * 0.0005),
+ serviceEscrowFee: Math.floor(rightValue * 0.001),
+ },
+ serviceFeePercentage: 0.025,
+ direction: 'forward',
+ state: SwapState.AWAIT_INCOMING,
+ stateEnteredAt: Date.now(),
+ expires: Date.now() + 1000 * 60 * 60 * 24, // 24h expiry
+ status: SwapStatus.WAITING_FOR_TRANSACTIONS,
+ hash: swapHash,
+ watchtowerNotified: true,
+ };
+
+ setSwap(swapHash, {
+ id: swap.id,
+ fees: {
+ totalFee: swap.from.serviceEscrowFee + swap.from.serviceNetworkFee,
+ asset: leftAsset,
+ },
+ });
+ setActiveSwap(swap);
+
+ // Simulate swap progression
+ setTimeout(() => {
+ // Update to processing state after 2s
+ swap.state = SwapState.SETTLE_SWAP;
+ swap.stateEnteredAt = Date.now();
+ setActiveSwap(swap);
+
+ // Create transactions for both sides
+ setTimeout(() => {
+ const { addTransactions: addNimTx } = useTransactionsStore();
+ const { addTransactions: addBtcTx } = useBtcTransactionsStore();
+ const { addTransactions: addUsdcTx } = useUsdcTransactionsStore();
+ const { addTransactions: addUsdtTx } = useUsdtTransactionsStore();
+
+ // Create appropriate transactions based on assets
+ if (leftAsset === SwapAsset.NIM) {
+ addNimTx([createFakeTransaction({
+ value: leftValue,
+ sender: demoNimAddress,
+ recipient: `0x${Math.random().toString(16).slice(2)}`,
+ data: {
+ type: 'raw',
+ raw: encodeTextToHex(`Swap: NIM to ${rightAsset}`),
+ },
+ })]);
+ }
+ if (rightAsset === SwapAsset.NIM) {
+ addNimTx([createFakeTransaction({
+ value: rightValue,
+ recipient: demoNimAddress,
+ sender: `0x${Math.random().toString(16).slice(2)}`,
+ data: {
+ type: 'raw',
+ raw: encodeTextToHex(`Swap: ${leftAsset} to NIM`),
+ },
+ })]);
+ }
+
+ // Handle BTC transactions
+ if (leftAsset === SwapAsset.BTC || rightAsset === SwapAsset.BTC) {
+ const isSending = leftAsset === SwapAsset.BTC;
+ const btcValue = isSending ? leftValue : rightValue;
+ addBtcTx([{
+ isCoinbase: false,
+ transactionHash: `btc-swap-${swapCounter}`,
+ inputs: [{
+ address: isSending ? demoBtcAddress : `1${Math.random().toString(36).slice(2)}`,
+ outputIndex: 0,
+ index: 0,
+ script: 'swap',
+ sequence: 4294967295,
+ transactionHash: `prev-${swapCounter}`,
+ witness: ['swap'],
+ }],
+ outputs: [{
+ value: btcValue,
+ address: isSending ? `1${Math.random().toString(36).slice(2)}` : demoBtcAddress,
+ script: 'swap',
+ index: 0,
+ }],
+ version: 1,
+ vsize: 200,
+ weight: 800,
+ locktime: 0,
+ confirmations: 1,
+ replaceByFee: false,
+ timestamp: Math.floor(Date.now() / 1000),
+ state: ElectrumTransactionState.CONFIRMED,
+ }]);
+ }
+
+ // Handle USDC transactions
+ if (leftAsset === SwapAsset.USDC_MATIC || rightAsset === SwapAsset.USDC_MATIC) {
+ const isSending = leftAsset === SwapAsset.USDC_MATIC;
+ const value = isSending ? leftValue : rightValue;
+ addUsdcTx([{
+ token: Config.polygon.usdc.tokenContract,
+ transactionHash: `usdc-swap-${swapCounter}`,
+ logIndex: 0,
+ sender: isSending ? demoPolygonAddress : `0x${Math.random().toString(16).slice(2)}`,
+ recipient: isSending ? `0x${Math.random().toString(16).slice(2)}` : demoPolygonAddress,
+ value,
+ state: UsdcTransactionState.CONFIRMED,
+ blockHeight: currentHead++,
+ timestamp: Math.floor(Date.now() / 1000),
+ }]);
+ }
+
+ // Handle USDT transactions
+ if (leftAsset === SwapAsset.USDT_MATIC || rightAsset === SwapAsset.USDT_MATIC) {
+ const isSending = leftAsset === SwapAsset.USDT_MATIC;
+ const value = isSending ? leftValue : rightValue;
+ addUsdtTx([{
+ token: Config.polygon.usdt_bridged.tokenContract,
+ transactionHash: `usdt-swap-${swapCounter}`,
+ logIndex: 0,
+ sender: isSending ? demoPolygonAddress : `0x${Math.random().toString(16).slice(2)}`,
+ recipient: isSending ? `0x${Math.random().toString(16).slice(2)}` : demoPolygonAddress,
+ value,
+ state: UsdtTransactionState.CONFIRMED,
+ blockHeight: currentHead++,
+ timestamp: Math.floor(Date.now() / 1000),
+ }]);
+ }
+
+ // Complete the swap
+ swap.state = SwapState.COMPLETED;
+ swap.stateEnteredAt = Date.now();
+ swap.status = SwapStatus.SUCCESS;
+ setActiveSwap(swap);
+ }, 3000); // Complete after 3 more seconds
+ }, 2000); // Start processing after 2s
+
+ return;
+ }
+
+ // Wait for router readiness
+ await new Promise((resolve) => {
+ demoRouter.onReady(resolve);
+ });
+
+ // eslint-disable-next-line no-console
+ console.log('[Demo] Redirecting to fallback modal');
+ demoRouter.push(`/${DemoModal.Fallback}`);
+ };
+ }
+ return target[prop];
+ },
+ });
+ }
+}
From 4abf23881e151bacb5b28055b3eecde217dc3955 Mon Sep 17 00:00:00 2001
From: onmax
Date: Wed, 5 Mar 2025 07:27:58 +0100
Subject: [PATCH 02/31] docs(demo): update README to clarify demo mode usage
and Nimiq Hub requirements
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index fe22f89dd..8a4297388 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,9 @@ yarn install
yarn serve
```
-> You will also need to run the [Nimiq Hub](https://github.com/nimiq/hub#contribute) and [Nimiq Keyguard](https://github.com/nimiq/keyguard/#development) in development, too.
+If you need to interact with a real account and data, then you will also need to run the [Nimiq Hub](https://github.com/nimiq/hub#contribute) and [Nimiq Keyguard](https://github.com/nimiq/keyguard/#development) in development, too.
+
+Otherwise, you can use activate the `demo` mode by appending `?demo` in the URL: `http://localhost:8081/?demo`.
### Compiles and minifies for production
```
From a757134e05a7f9e6ecca04e06fc20b91667bc9b3 Mon Sep 17 00:00:00 2001
From: onmax
Date: Wed, 5 Mar 2025 12:34:11 +0100
Subject: [PATCH 03/31] chore(demo): intercept and return swap api results
---
src/stores/Demo.ts | 996 ++++++++++++++++++++++++++++++++++-----------
1 file changed, 748 insertions(+), 248 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index c9c609184..21994c3dd 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -2,7 +2,6 @@
import { createStore } from 'pinia';
import VueRouter from 'vue-router';
import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
-import HubApi from '@nimiq/hub-api';
import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
import { STAKING_CONTRACT_ADDRESS } from '@/lib/Constants';
@@ -16,13 +15,14 @@ import { useStakingStore } from '@/stores/Staking';
import { useAccountSettingsStore } from '@/stores/AccountSettings';
import { usePolygonAddressStore } from '@/stores/PolygonAddress';
import Config from 'config';
-import { SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
-import { SwapState, useSwapsStore } from '@/stores/Swaps';
+import { AssetList, FastspotAsset, FastspotEstimate, FastspotFee, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset } from '@nimiq/fastspot-api';
import { useBtcAddressStore } from './BtcAddress';
import { useContactsStore } from './Contacts';
import { useBtcLabelsStore } from './BtcLabels';
import { useUsdcContactsStore } from './UsdcContacts';
import { useUsdtContactsStore } from './UsdtContacts';
+import { useFiatStore } from './Fiat';
+import HubApi from '@nimiq/hub-api';
export type DemoState = {
active: boolean,
@@ -101,6 +101,7 @@ export const useDemoStore = createStore({
attachIframeListeners();
replaceStakingFlow();
replaceBuyNimFlow();
+ enableSendModalInDemoMode();
},
/**
@@ -591,7 +592,7 @@ function generateFakePolygonTransactions() {
/**
* Shared function to generate token transactions for Polygon tokens
*/
-function generateTokenTransactions(txDefinitions: any, initialBalance:number, tokenContract:string, confirmState:any, contactsStore:any) {
+function generateTokenTransactions(txDefinitions: any, initialBalance: number, tokenContract: string, confirmState: any, contactsStore: any) {
const transactions = [];
const { setContact } = contactsStore();
@@ -882,6 +883,10 @@ const demoCSS = `
background: rgba(255, 255, 255);
border-radius: 50%;
}
+
+.send-modal-footer .footer-notice {
+ display: none;
+}
`;
/**
@@ -897,7 +902,7 @@ function interceptFetchRequest() {
if (args[0].startsWith('/')) return originalFetch(...args);
const url = new URL(args[0] as string);
- const isFastspotRequest = url.host === Config.fastspot.apiEndpoint;
+ const isFastspotRequest = url.host === (new URL(Config.fastspot.apiEndpoint).host);
const isLimitsRequest = url.pathname.includes('/limits');
const isEstimateRequest = url.pathname.includes('/estimate');
const isAssetsRequest = url.pathname.includes('/assets');
@@ -906,60 +911,92 @@ function interceptFetchRequest() {
return originalFetch(...args);
}
- if (isLimitsRequest) {
- // Return mock limits with higher values and proper structure
- const json = {
- asset: 'USD',
+ if (isLimitsRequest) {
+ const constants = {
+ current: '9800',
daily: '50000',
dailyRemaining: '49000',
monthly: '100000',
monthlyRemaining: '98000',
- swap: '10000',
- current: '9800',
+ } as const;
+
+ const [assetOrLimit,_] = url.pathname.split('/').slice(-2) as [SwapAsset | 'limits', string];
+
+ if (assetOrLimit === 'limits') {
+ const limits: FastspotUserLimits = {
+ asset: ReferenceAsset.USD,
+ swap: `${1000}`,
+ ...constants,
+ };
+ return new Response(JSON.stringify(limits));
+ }
+
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+ await sleep(1000 + Math.random() * 500);
+
+ const asset = assetOrLimit as SwapAsset;
+ // const { exchangeRates } = useFiatStore();
+ // const rate: number = exchangeRates.value[asset.toLocaleLowerCase().split('_')[0]].usd!;
+ const json: FastspotLimits = {
+ asset,
+ swap: `${1000}`,
+ referenceAsset: ReferenceAsset.USD,
+ referenceCurrent: constants.current,
+ referenceDaily: constants.daily,
+ referenceDailyRemaining: constants.dailyRemaining,
+ referenceMonthly: constants.monthly,
+ referenceMonthlyRemaining: constants.monthlyRemaining,
+ referenceSwap: `${1000}`,
+ ...constants,
};
+
return new Response(JSON.stringify(json));
}
if (isAssetsRequest) {
- // Return mock assets data with fees
- const json = {
- BTC: {
- enabled: true,
- fee: 0.0001, // BTC network fee
- minAmount: 0.001,
- maxAmount: 1,
- },
- NIM: {
- enabled: true,
- fee: 0.01, // NIM network fee
- minAmount: 1,
- maxAmount: 1000000,
+ // Return mock assets data with fees for all supported assets
+ const json: FastspotAsset[] = [
+ {
+ symbol: SwapAsset.BTC,
+ name: 'Bitcoin',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.BTC)}`,
+ limits: { minimum: '0.0001', maximum: '1' },
},
- USDC_MATIC: {
- enabled: true,
- fee: 0.1, // USDC fee
- minAmount: 1,
- maxAmount: 50000,
+ {
+ symbol: SwapAsset.NIM,
+ name: 'Nimiq',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.NIM)}`,
+ limits: { minimum: '1', maximum: '100000' },
},
- USDT_MATIC: {
- enabled: true,
- fee: 0.1, // USDT fee
- minAmount: 1,
- maxAmount: 50000,
+ {
+ symbol: SwapAsset.USDC_MATIC,
+ name: 'USDC (Polygon)',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDC_MATIC)}`,
+ limits: { minimum: '1', maximum: '100000' },
},
- };
+ {
+ symbol: SwapAsset.USDT_MATIC,
+ name: 'USDT (Polygon)',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDT_MATIC)}`,
+ limits: { minimum: '1', maximum: '100000' },
+ }
+ ]
return new Response(JSON.stringify(json));
}
if (isEstimateRequest) {
- // Parse the estimate request parameters
- const searchParams = new URLSearchParams(url.search);
- const from = searchParams.get('from');
- const to = searchParams.get('to');
- const value = Number(searchParams.get('value'));
+ const body = args[1]?.body
+ if (!body) throw new Error('[Demo] No body found in request')
+ type EstimateRequest = {
+ from: Record,
+ to: SwapAsset,
+ includedFees: 'required'
+ };
+ const { from: fromObj, to, includedFees: _includedFees } = JSON.parse(body.toString()) as EstimateRequest
+ const from = Object.keys(fromObj)[0] as SwapAsset
// Validate the request
- if (!from || !to || !value || Number.isNaN(value)) {
+ if (!from || !to) {
return new Response(JSON.stringify({
error: 'Invalid request parameters',
status: 400,
@@ -967,36 +1004,61 @@ function interceptFetchRequest() {
}
// Calculate fees based on the value
+
+ const value = fromObj[from];
const networkFee = Math.floor(value * 0.0005); // 0.05% network fee
const escrowFee = Math.floor(value * 0.001); // 0.1% escrow fee
// Calculate the destination amount using our mock rates
const estimatedAmount = calculateEstimatedAmount(from, to, value);
- // Note: These fee calculations are simplified for demo purposes
- const toNetworkFee = Math.floor(estimatedAmount * 0.0005);
- const toEscrowFee = Math.floor(estimatedAmount * 0.001);
-
// Create mock estimate response with a valid structure
- const estimate = {
- from: {
- asset: from,
- amount: value,
- fee: 0, // Network-specific fee (e.g., BTC mining fee)
- serviceNetworkFee: networkFee,
- serviceEscrowFee: escrowFee,
- },
- to: {
- asset: to,
- amount: estimatedAmount,
- fee: 0, // Network-specific fee
- serviceNetworkFee: toNetworkFee,
- serviceEscrowFee: toEscrowFee,
- },
- serviceFeePercentage: 0.025, // 2.5% service fee
- expires: Date.now() + 30000, // 30 seconds expiry
- swapProfit: 0,
- };
+ const estimate: FastspotEstimate[] = [{
+ from: [{
+ amount: `${value}`,
+ name: from,
+ symbol: from,
+ finalizeNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(from)}`,
+ },
+ fundingNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(from)}`,
+ },
+ operatingNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(from)}`,
+ }
+ }],
+ to: [{
+ amount: `${estimatedAmount}`,
+ name: to,
+ symbol: to,
+ finalizeNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(to)}`,
+ },
+ fundingNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(to)}`,
+ },
+ operatingNetworkFee: {
+ total: `${networkFee}`,
+ totalIsIncluded: false,
+ perUnit: `${getNetworkFeePerUnit(to)}`,
+ }
+ }],
+ direction: 'forward',
+ serviceFeePercentage: 0.01,
+ }];
+
+ console.log('[Demo] Mock estimate:', estimate); // eslint-disable no-console
return new Response(JSON.stringify(estimate));
}
@@ -1005,6 +1067,40 @@ function interceptFetchRequest() {
};
}
+/**
+ * Network fee helper function
+ */
+function getNetworkFee(asset: string): number {
+ switch (asset) {
+ case SwapAsset.BTC:
+ return 10000; // 10k sats
+ case SwapAsset.NIM:
+ return 1000; // 1000 luna
+ case SwapAsset.USDC_MATIC:
+ case SwapAsset.USDT_MATIC:
+ return 2000000000000000; // 0.002 MATIC in wei
+ default:
+ return 0;
+ }
+}
+
+/**
+ * Fee per unit helper function
+ */
+function getNetworkFeePerUnit(asset: string): number {
+ switch (asset) {
+ case SwapAsset.BTC:
+ return 10; // 10 sats/vbyte
+ case SwapAsset.NIM:
+ return 0; // luna per byte
+ case SwapAsset.USDC_MATIC:
+ case SwapAsset.USDT_MATIC:
+ return 1000000000; // 1 Gwei
+ default:
+ return 0;
+ }
+}
+
/**
* Calculate mock estimated amount for swaps based on predefined rates
*/
@@ -1022,7 +1118,7 @@ function calculateEstimatedAmount(fromAsset: string, toAsset: string, value: num
[`${SwapAsset.USDC_MATIC}-${SwapAsset.USDT_MATIC}`]: 1, // 1 USDC = 1 USDT
[`${SwapAsset.USDT_MATIC}-${SwapAsset.NIM}`]: 833.33, // 1 USDT = 833.33 NIM
[`${SwapAsset.USDT_MATIC}-${SwapAsset.BTC}`]: 0.000033, // 1 USDT = 0.000033 BTC
- [`${SwapAsset.USDT_MATIC}-${SwapAsset.USDC_MATIC}`]: 1, // 1 USDT = 1 USDC
+ [`${SwapAsset.USDT_MATIC}-${SwapAsset.USDC_MATIC}`]: 1, // 1 USDT = 1 USDT
};
const rate = rates[`${fromAsset}-${toAsset}`];
@@ -1038,7 +1134,44 @@ function calculateEstimatedAmount(fromAsset: string, toAsset: string, value: num
return Math.floor(value * rate);
}
-let swapCounter = 0;
+/**
+ * Ensures the Send button in modals is always enabled in demo mode, regardless of network state.
+ * This allows users to interact with the send functionality without waiting for network sync.
+ */
+function enableSendModalInDemoMode() {
+ let observing = false;
+ const observer = new MutationObserver(() => {
+ // Target the send modal footer button
+ const sendButton = document.querySelector('.send-modal-footer .nq-button');
+ if (!sendButton) return;
+
+ if (sendButton.hasAttribute('disabled')) {
+ sendButton.removeAttribute('disabled');
+
+ // Also remove any visual indications of being disabled
+ sendButton.classList.remove('disabled');
+
+ // Also find and hide any sync message if shown
+ const footer = document.querySelector('.send-modal-footer');
+ if (footer) {
+ const footerNotice = footer.querySelector('.footer-notice') as HTMLDivElement;
+ if (footerNotice && footerNotice.textContent &&
+ (footerNotice.textContent.includes('Connecting to Bitcoin') ||
+ footerNotice.textContent.includes('Syncing with Bitcoin'))) {
+ footerNotice.style.display = 'none';
+ }
+ }
+ }
+ });
+
+ // Observe document for changes - particularly when modals open
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled'] });
+}
+
+const ignoreHubRequests = [
+ 'addBtcAddresses',
+ 'on'
+]
/**
* Replacement of the Hub API class to capture and redirect calls to our demo modals instead.
@@ -1050,190 +1183,557 @@ export class DemoHubApi extends HubApi {
get(target, prop: keyof HubApi) {
if (typeof target[prop] === 'function') {
return async (...args: Parameters) => {
- const requestName = String(prop);
- const [firstArg] = args;
- // eslint-disable-next-line no-console
- console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
-
- if (requestName === 'on') {
- return;
- }
- if (requestName === 'setupSwap') {
- const leftColumn = document.querySelector('.swap-amounts .left-column');
- const leftAmountElement = leftColumn!.querySelector('.width-value');
- const leftValue = Number((leftAmountElement as HTMLDivElement).innerText);
- const leftAsset = leftColumn!.querySelector('.ticker')!.innerHTML.toUpperCase() as SwapAsset;
-
- const rightColumn = document.querySelector('.swap-amounts .right-column');
- const rightAmountElement = rightColumn!.querySelector('.width-value');
- const rightValue = Number((rightAmountElement as HTMLDivElement).innerText);
- const rightAsset = rightColumn!.querySelector('.ticker')!.innerHTML.toUpperCase() as SwapAsset;
-
- const swapHash = `0x${(++swapCounter).toString(16)}`;
- const { setActiveSwap, setSwap } = useSwapsStore();
-
- // Create initial swap state
- const swap = {
- id: `${swapCounter}`,
- contracts: {},
- from: {
- asset: leftAsset,
- amount: leftValue,
- fee: 0,
- serviceEscrowFee: Math.floor(leftValue * 0.001), // 0.1% fee
- serviceNetworkFee: Math.floor(leftValue * 0.0005), // 0.05% network fee
- },
- to: {
- asset: rightAsset,
- amount: rightValue,
- fee: 0,
- serviceNetworkFee: Math.floor(rightValue * 0.0005),
- serviceEscrowFee: Math.floor(rightValue * 0.001),
- },
- serviceFeePercentage: 0.025,
- direction: 'forward',
- state: SwapState.AWAIT_INCOMING,
- stateEnteredAt: Date.now(),
- expires: Date.now() + 1000 * 60 * 60 * 24, // 24h expiry
- status: SwapStatus.WAITING_FOR_TRANSACTIONS,
- hash: swapHash,
- watchtowerNotified: true,
- };
-
- setSwap(swapHash, {
- id: swap.id,
- fees: {
- totalFee: swap.from.serviceEscrowFee + swap.from.serviceNetworkFee,
- asset: leftAsset,
- },
+ return new Promise(async (resolve, reject) => {
+ const requestName = String(prop);
+ const [firstArg] = args;
+ // eslint-disable-next-line no-console
+ console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
+
+ if (ignoreHubRequests.includes(requestName)) {
+ return;
+ }
+ // Find the setupSwap handler in the DemoHubApi class and replace it with this:
+
+ if (requestName === 'setupSwap') {
+ console.log({
+ firstArg,
+ args,
+ })
+ return
+ }
+ // if (requestName === 'setupSwap') {
+ // (firstArg as Function)();
+ // // Get swap amount and asset details from the DOM
+ // const leftColumn = document.querySelector('.swap-amounts .left-column');
+ // const leftAmountElement = leftColumn?.querySelector('.width-value');
+ // const rightColumn = document.querySelector('.swap-amounts .right-column');
+ // const rightAmountElement = rightColumn?.querySelector('.width-value');
+
+ // if (!leftAmountElement || !rightAmountElement || !leftColumn || !rightColumn) {
+ // console.warn('[Demo] Could not find swap amount elements');
+ // return {};
+ // }
+
+ // const leftValue = Number((leftAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
+ // const leftAsset = leftColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
+
+ // const rightValue = Number((rightAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
+ // const rightAsset = rightColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
+
+ // console.log(`[Demo] Setting up swap: ${leftValue} ${leftAsset} -> ${rightValue} ${rightAsset}`);
+
+ // // Check if we have valid values
+ // if (!leftValue || !rightValue || !leftAsset || !rightAsset) {
+ // console.warn('[Demo] Missing swap values');
+ // return {};
+ // }
+
+ // const direction = leftValue < rightValue ? 'forward' : 'reverse';
+ // const fromAsset = direction === 'forward' ? leftAsset : rightAsset;
+ // // @ts-expect-error Object key not specific enough?
+ // const toAsset: RequestAsset = direction === 'forward'
+ // ? { [rightAsset]: rightValue }
+ // : { [leftAsset]: leftValue };
+
+ // const swapSuggestion = await createSwap(fromAsset, toAsset);
+
+ // const { config } = useConfig();
+
+ // let fund: HtlcCreationInstructions | null = null;
+ // let redeem: HtlcSettlementInstructions | null = null;
+
+ // const { activeAddressInfo } = useAddressStore();
+
+ // const { availableExternalAddresses } = useBtcAddressStore();
+ // const nimAddress = activeAddressInfo.value!.address;
+ // const btcAddress = availableExternalAddresses.value[0];
+
+ // if (swapSuggestion.from.asset === SwapAsset.NIM) {
+ // const nimiqClient = await getNetworkClient();
+ // await nimiqClient.waitForConsensusEstablished();
+ // const headHeight = await nimiqClient.getHeadHeight();
+ // if (headHeight > 100) {
+ // useNetworkStore().state.height = headHeight;
+ // } else {
+ // throw new Error('Invalid network state, try please reloading the app');
+ // }
+
+ // fund = {
+ // type: SwapAsset.NIM,
+ // sender: nimAddress,
+ // value: swapSuggestion.from.amount,
+ // fee: swapSuggestion.from.fee,
+ // validityStartHeight: useNetworkStore().state.height,
+ // };
+ // }
+
+ // const { accountUtxos, accountBalance: accountBtcBalance, } = useBtcAddressStore();
+ // if (swapSuggestion.from.asset === SwapAsset.BTC) {
+ // const electrumClient = await getElectrumClient();
+ // await electrumClient.waitForConsensusEstablished();
+
+
+ // // Assemble BTC inputs
+ // // Transactions to an HTLC are 46 weight units bigger because of the longer recipient address
+ // const requiredInputs = selectOutputs(
+ // accountUtxos.value, swapSuggestion.from.amount, swapSuggestion.from.feePerUnit, 48);
+ // let changeAddress: string;
+ // if (requiredInputs.changeAmount > 0) {
+ // const { nextChangeAddress } = useBtcAddressStore();
+ // if (!nextChangeAddress.value) {
+ // // FIXME: If no unused change address is found, need to request new ones from Hub!
+ // throw new Error('No more unused change addresses (not yet implemented)');
+ // }
+ // changeAddress = nextChangeAddress.value;
+ // }
+
+ // fund = {
+ // type: SwapAsset.BTC,
+ // inputs: requiredInputs.utxos.map((utxo) => ({
+ // address: utxo.address,
+ // transactionHash: utxo.transactionHash,
+ // outputIndex: utxo.index,
+ // outputScript: utxo.witness.script,
+ // value: utxo.witness.value,
+ // })),
+ // output: {
+ // value: swapSuggestion.from.amount,
+ // },
+ // ...(requiredInputs.changeAmount > 0 ? {
+ // changeOutput: {
+ // address: changeAddress!,
+ // value: requiredInputs.changeAmount,
+ // },
+ // } : {}),
+ // refundAddress: btcAddress,
+ // };
+ // }
+
+ // const {
+ // activeAddress: activePolygonAddress,
+ // accountUsdcBalance,
+ // accountUsdtBridgedBalance,
+ // } = usePolygonAddressStore();
+
+
+ // if (swapSuggestion.from.asset === SwapAsset.USDC_MATIC) {
+ // const [client, htlcContract] = await Promise.all([
+ // getPolygonClient(),
+ // getUsdcHtlcContract(),
+ // ]);
+ // const fromAddress = activePolygonAddress.value!;
+
+ // const [
+ // usdcNonce,
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // client.usdcToken.nonces(fromAddress) as Promise,
+ // htlcContract.getNonce(fromAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'open' && method !== 'openWithPermit') {
+ // throw new Error('Wrong USDC contract method');
+ // }
+
+ // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
+ // // Keyguard's SwapIFrameApi.
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address token */ config.polygon.usdc.tokenContract,
+ // /* uint256 amount */ swapSuggestion.from.amount,
+ // /* address refundAddress */ fromAddress,
+ // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
+ // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint256 timeout */ 0,
+ // /* uint256 fee */ fee,
+ // ...(method === 'openWithPermit' ? [
+ // // // Approve the maximum possible amount so afterwards we can use the `open` method for
+ // // // lower fees
+ // // /* uint256 value */ client.ethers
+ // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
+ // /* uint256 value */ swapSuggestion.from.amount + fee,
+
+ // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint8 sigV */ 0,
+ // ] : []),
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: fromAddress,
+ // to: config.polygon.usdc.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdc.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdc.htlcContract,
+ // },
+ // };
+
+ // fund = {
+ // type: SwapAsset.USDC_MATIC,
+ // ...relayRequest,
+ // ...(method === 'openWithPermit' ? {
+ // permit: {
+ // tokenNonce: usdcNonce.toNumber(),
+ // },
+ // } : null),
+ // };
+ // }
+
+ // if (swapSuggestion.from.asset === SwapAsset.USDT_MATIC) {
+ // const [client, htlcContract] = await Promise.all([
+ // getPolygonClient(),
+ // getUsdtBridgedHtlcContract(),
+ // ]);
+ // const fromAddress = activePolygonAddress.value!;
+
+ // const [
+ // usdtNonce,
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // client.usdtBridgedToken.getNonce(fromAddress) as Promise,
+ // htlcContract.getNonce(fromAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'open' && method !== 'openWithApproval') {
+ // throw new Error('Wrong USDT contract method');
+ // }
+
+ // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
+ // // Keyguard's SwapIFrameApi.
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address token */ config.polygon.usdt_bridged.tokenContract,
+ // /* uint256 amount */ swapSuggestion.from.amount,
+ // /* address refundAddress */ fromAddress,
+ // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
+ // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint256 timeout */ 0,
+ // /* uint256 fee */ fee,
+ // ...(method === 'openWithApproval' ? [
+ // // // Approve the maximum possible amount so afterwards we can use the `open` method for
+ // // // lower fees
+ // // /* uint256 approval */ client.ethers
+ // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
+ // /* uint256 approval */ swapSuggestion.from.amount + fee,
+
+ // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint8 sigV */ 0,
+ // ] : []),
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: fromAddress,
+ // to: config.polygon.usdt_bridged.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdt_bridged.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdt_bridged.htlcContract,
+ // },
+ // };
+
+ // fund = {
+ // type: SwapAsset.USDT_MATIC,
+ // ...relayRequest,
+ // ...(method === 'openWithApproval' ? {
+ // approval: {
+ // tokenNonce: usdtNonce.toNumber(),
+ // },
+ // } : null),
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.NIM) {
+ // const nimiqClient = await getNetworkClient();
+ // await nimiqClient.waitForConsensusEstablished();
+ // const headHeight = await nimiqClient.getHeadHeight();
+ // if (headHeight > 100) {
+ // useNetworkStore().state.height = headHeight;
+ // } else {
+ // throw new Error('Invalid network state, try please reloading the app');
+ // }
+
+ // redeem = {
+ // type: SwapAsset.NIM,
+ // recipient: nimAddress, // My address, must be redeem address of HTLC
+ // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Luna
+ // fee: swapSuggestion.to.fee, // Luna
+ // validityStartHeight: useNetworkStore().state.height,
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.BTC) {
+ // const electrumClient = await getElectrumClient();
+ // await electrumClient.waitForConsensusEstablished();
+
+ // redeem = {
+ // type: SwapAsset.BTC,
+ // input: {
+ // // transactionHash: transaction.transactionHash,
+ // // outputIndex: output.index,
+ // // outputScript: output.script,
+ // value: swapSuggestion.to.amount, // Sats
+ // },
+ // output: {
+ // address: btcAddress, // My address, must be redeem address of HTLC
+ // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Sats
+ // },
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.USDC_MATIC) {
+ // const htlcContract = await getUsdcHtlcContract();
+ // const toAddress = activePolygonAddress.value!;
+
+ // const [
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // htlcContract.getNonce(toAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'redeemWithSecretInData') {
+ // throw new Error('Wrong USDC contract method');
+ // }
+
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address target */ toAddress,
+ // /* uint256 fee */ fee,
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: toAddress,
+ // to: config.polygon.usdc.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdc.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdc.htlcContract,
+ // },
+ // };
+
+ // redeem = {
+ // type: SwapAsset.USDC_MATIC,
+ // ...relayRequest,
+ // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.USDT_MATIC) {
+ // const htlcContract = await getUsdtBridgedHtlcContract();
+ // const toAddress = activePolygonAddress.value!;
+
+ // const [
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // htlcContract.getNonce(toAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'redeemWithSecretInData') {
+ // throw new Error('Wrong USDT contract method');
+ // }
+
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address target */ toAddress,
+ // /* uint256 fee */ fee,
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: toAddress,
+ // to: config.polygon.usdt_bridged.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdt_bridged.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdt_bridged.htlcContract,
+ // },
+ // };
+
+ // redeem = {
+ // type: SwapAsset.USDT_MATIC,
+ // ...relayRequest,
+ // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
+ // };
+ // }
+
+ // if (!fund || !redeem) {
+ // reject(new Error('UNEXPECTED: No funding or redeeming data objects'));
+ // return;
+ // }
+
+ // const serviceSwapFee = Math.round(
+ // (swapSuggestion.from.amount - swapSuggestion.from.serviceNetworkFee)
+ // * swapSuggestion.serviceFeePercentage,
+ // );
+
+ // const { addressInfos } = useAddressStore();
+
+ // const { activeAccountInfo } = useAccountStore();
+
+ // const { currency, exchangeRates } = useFiatStore();
+
+ // const {
+ // activeAddress
+
+ // } = usePolygonAddressStore();
+
+ // const request: Omit = {
+ // accountId: activeAccountInfo.value!.id,
+ // swapId: swapSuggestion.id,
+ // fund,
+ // redeem,
+
+ // layout: 'slider',
+ // direction: direction === 'forward' ? 'left-to-right' : 'right-to-left',
+ // fiatCurrency: currency.value,
+ // fundingFiatRate: exchangeRates.value[assetToCurrency(
+ // fund.type as SupportedSwapAsset,
+ // )][currency.value]!,
+ // redeemingFiatRate: exchangeRates.value[assetToCurrency(
+ // redeem.type as SupportedSwapAsset,
+ // )][currency.value]!,
+ // fundFees: {
+ // processing: 0,
+ // redeeming: swapSuggestion.from.serviceNetworkFee,
+ // },
+ // redeemFees: {
+ // funding: swapSuggestion.to.serviceNetworkFee,
+ // processing: 0,
+ // },
+ // serviceSwapFee,
+ // nimiqAddresses: addressInfos.value.map((addressInfo) => ({
+ // address: addressInfo.address,
+ // balance: addressInfo.balance || 0,
+ // })),
+ // bitcoinAccount: {
+ // balance: accountBtcBalance.value,
+ // },
+ // polygonAddresses: activePolygonAddress.value ? [{
+ // address: activePolygonAddress.value,
+ // usdcBalance: accountUsdcBalance.value,
+ // usdtBalance: accountUsdtBridgedBalance.value,
+ // }] : [],
+ // };
+
+ // console.log('[Demo] Faking swap setup with request:', request);
+ // resolve(request)
+ // return;
+ // }
+
+ // Wait for router readiness
+ await new Promise((resolve) => {
+ demoRouter.onReady(resolve);
});
- setActiveSwap(swap);
-
- // Simulate swap progression
- setTimeout(() => {
- // Update to processing state after 2s
- swap.state = SwapState.SETTLE_SWAP;
- swap.stateEnteredAt = Date.now();
- setActiveSwap(swap);
-
- // Create transactions for both sides
- setTimeout(() => {
- const { addTransactions: addNimTx } = useTransactionsStore();
- const { addTransactions: addBtcTx } = useBtcTransactionsStore();
- const { addTransactions: addUsdcTx } = useUsdcTransactionsStore();
- const { addTransactions: addUsdtTx } = useUsdtTransactionsStore();
-
- // Create appropriate transactions based on assets
- if (leftAsset === SwapAsset.NIM) {
- addNimTx([createFakeTransaction({
- value: leftValue,
- sender: demoNimAddress,
- recipient: `0x${Math.random().toString(16).slice(2)}`,
- data: {
- type: 'raw',
- raw: encodeTextToHex(`Swap: NIM to ${rightAsset}`),
- },
- })]);
- }
- if (rightAsset === SwapAsset.NIM) {
- addNimTx([createFakeTransaction({
- value: rightValue,
- recipient: demoNimAddress,
- sender: `0x${Math.random().toString(16).slice(2)}`,
- data: {
- type: 'raw',
- raw: encodeTextToHex(`Swap: ${leftAsset} to NIM`),
- },
- })]);
- }
-
- // Handle BTC transactions
- if (leftAsset === SwapAsset.BTC || rightAsset === SwapAsset.BTC) {
- const isSending = leftAsset === SwapAsset.BTC;
- const btcValue = isSending ? leftValue : rightValue;
- addBtcTx([{
- isCoinbase: false,
- transactionHash: `btc-swap-${swapCounter}`,
- inputs: [{
- address: isSending ? demoBtcAddress : `1${Math.random().toString(36).slice(2)}`,
- outputIndex: 0,
- index: 0,
- script: 'swap',
- sequence: 4294967295,
- transactionHash: `prev-${swapCounter}`,
- witness: ['swap'],
- }],
- outputs: [{
- value: btcValue,
- address: isSending ? `1${Math.random().toString(36).slice(2)}` : demoBtcAddress,
- script: 'swap',
- index: 0,
- }],
- version: 1,
- vsize: 200,
- weight: 800,
- locktime: 0,
- confirmations: 1,
- replaceByFee: false,
- timestamp: Math.floor(Date.now() / 1000),
- state: ElectrumTransactionState.CONFIRMED,
- }]);
- }
-
- // Handle USDC transactions
- if (leftAsset === SwapAsset.USDC_MATIC || rightAsset === SwapAsset.USDC_MATIC) {
- const isSending = leftAsset === SwapAsset.USDC_MATIC;
- const value = isSending ? leftValue : rightValue;
- addUsdcTx([{
- token: Config.polygon.usdc.tokenContract,
- transactionHash: `usdc-swap-${swapCounter}`,
- logIndex: 0,
- sender: isSending ? demoPolygonAddress : `0x${Math.random().toString(16).slice(2)}`,
- recipient: isSending ? `0x${Math.random().toString(16).slice(2)}` : demoPolygonAddress,
- value,
- state: UsdcTransactionState.CONFIRMED,
- blockHeight: currentHead++,
- timestamp: Math.floor(Date.now() / 1000),
- }]);
- }
-
- // Handle USDT transactions
- if (leftAsset === SwapAsset.USDT_MATIC || rightAsset === SwapAsset.USDT_MATIC) {
- const isSending = leftAsset === SwapAsset.USDT_MATIC;
- const value = isSending ? leftValue : rightValue;
- addUsdtTx([{
- token: Config.polygon.usdt_bridged.tokenContract,
- transactionHash: `usdt-swap-${swapCounter}`,
- logIndex: 0,
- sender: isSending ? demoPolygonAddress : `0x${Math.random().toString(16).slice(2)}`,
- recipient: isSending ? `0x${Math.random().toString(16).slice(2)}` : demoPolygonAddress,
- value,
- state: UsdtTransactionState.CONFIRMED,
- blockHeight: currentHead++,
- timestamp: Math.floor(Date.now() / 1000),
- }]);
- }
-
- // Complete the swap
- swap.state = SwapState.COMPLETED;
- swap.stateEnteredAt = Date.now();
- swap.status = SwapStatus.SUCCESS;
- setActiveSwap(swap);
- }, 3000); // Complete after 3 more seconds
- }, 2000); // Start processing after 2s
-
- return;
- }
-
- // Wait for router readiness
- await new Promise((resolve) => {
- demoRouter.onReady(resolve);
- });
- // eslint-disable-next-line no-console
- console.log('[Demo] Redirecting to fallback modal');
- demoRouter.push(`/${DemoModal.Fallback}`);
- };
+ // eslint-disable-next-line no-console
+ console.log('[Demo] Redirecting to fallback modal');
+ demoRouter.push(`/${DemoModal.Fallback}`);
+ });
+ }
}
return target[prop];
},
From c7a763b6efd5e3a312a01ab682d5a53694955b0d Mon Sep 17 00:00:00 2001
From: Alberto Monterroso <14013679+Albermonte@users.noreply.github.com>
Date: Mon, 10 Mar 2025 08:28:46 +0100
Subject: [PATCH 04/31] feat(demo): added swap animation
---
src/stores/Demo.ts | 1580 +++++++++++++++++++++++++-------------------
1 file changed, 917 insertions(+), 663 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index 21994c3dd..711f3a123 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -15,14 +15,15 @@ import { useStakingStore } from '@/stores/Staking';
import { useAccountSettingsStore } from '@/stores/AccountSettings';
import { usePolygonAddressStore } from '@/stores/PolygonAddress';
import Config from 'config';
-import { AssetList, FastspotAsset, FastspotEstimate, FastspotFee, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset } from '@nimiq/fastspot-api';
+import { AssetList, FastspotAsset, FastspotEstimate, FastspotFee, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
+import HubApi, { SetupSwapResult } from '@nimiq/hub-api';
import { useBtcAddressStore } from './BtcAddress';
import { useContactsStore } from './Contacts';
import { useBtcLabelsStore } from './BtcLabels';
import { useUsdcContactsStore } from './UsdcContacts';
import { useUsdtContactsStore } from './UsdtContacts';
import { useFiatStore } from './Fiat';
-import HubApi from '@nimiq/hub-api';
+import { SwapState, useSwapsStore } from './Swaps';
export type DemoState = {
active: boolean,
@@ -60,6 +61,61 @@ const nimInitialBalance = 14041800000; // 14,041,800,000 - 14 april, 2018
const usdtInitialBalance = 5000000000; // 5000 USDT (6 decimals)
const usdcInitialBalance = 3000000000; // 3000 USDC (6 decimals)
+// Swaps
+const onGoingSwaps = new Map();
+
+let swapInterval: NodeJS.Timeout | null = null;
+function listenForSwapChanges() {
+ if (swapInterval) return;
+ swapInterval = setInterval(() => {
+ const swap = useSwapsStore().activeSwap.value;
+ if (!swap) return;
+ console.log('[Demo] Active swap:', { swap, state: swap.state });
+ switch (swap.state) {
+ case SwapState.AWAIT_INCOMING:
+ console.log('[Demo] Swap is in AWAIT_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.CREATE_OUTGOING,
+ });
+ break;
+ case SwapState.CREATE_OUTGOING:
+ console.log('[Demo] Swap is in CREATE_OUTGOING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_SECRET,
+ });
+ break;
+ case SwapState.AWAIT_SECRET:
+ console.log('[Demo] Swap is in AWAIT_SECRET state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.SETTLE_INCOMING,
+ });
+ break;
+ case SwapState.SETTLE_INCOMING:
+ console.log('[Demo] Swap is in SETTLE_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.COMPLETE,
+ });
+ break;
+ case SwapState.COMPLETE:
+ console.log('[Demo] Swap is in COMPLETE state');
+ if (swapInterval)clearInterval(swapInterval);
+ swapInterval = null;
+ break;
+ default:
+ console.log('[Demo] Swap is in unknown state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_INCOMING,
+ });
+ break;
+ }
+ }, 15000);
+}
+
// We keep a reference to the router here.
let demoRouter: VueRouter;
@@ -906,50 +962,60 @@ function interceptFetchRequest() {
const isLimitsRequest = url.pathname.includes('/limits');
const isEstimateRequest = url.pathname.includes('/estimate');
const isAssetsRequest = url.pathname.includes('/assets');
+ const isSwapRequest = url.pathname.includes('/swaps');
+ // return originalFetch(...args);
if (!isFastspotRequest) {
return originalFetch(...args);
}
- if (isLimitsRequest) {
+ console.log('[Demo] Intercepted fetch request:', url.pathname);
+
+ if (isLimitsRequest) {
const constants = {
current: '9800',
daily: '50000',
dailyRemaining: '49000',
monthly: '100000',
monthlyRemaining: '98000',
+ swap: '10000',
} as const;
-
- const [assetOrLimit,_] = url.pathname.split('/').slice(-2) as [SwapAsset | 'limits', string];
+
+ const [assetOrLimit] = url.pathname.split('/').slice(-2) as [SwapAsset | 'limits', string];
if (assetOrLimit === 'limits') {
const limits: FastspotUserLimits = {
asset: ReferenceAsset.USD,
- swap: `${1000}`,
...constants,
};
return new Response(JSON.stringify(limits));
}
-
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
await sleep(1000 + Math.random() * 500);
const asset = assetOrLimit as SwapAsset;
- // const { exchangeRates } = useFiatStore();
- // const rate: number = exchangeRates.value[asset.toLocaleLowerCase().split('_')[0]].usd!;
+
+ const { exchangeRates, currency } = useFiatStore();
+ const rate: number = exchangeRates.value[asset.toLocaleLowerCase().split('_')[0]][currency.value]!;
+
const json: FastspotLimits = {
asset,
- swap: `${1000}`,
referenceAsset: ReferenceAsset.USD,
referenceCurrent: constants.current,
referenceDaily: constants.daily,
referenceDailyRemaining: constants.dailyRemaining,
referenceMonthly: constants.monthly,
referenceMonthlyRemaining: constants.monthlyRemaining,
- referenceSwap: `${1000}`,
- ...constants,
+ referenceSwap: `${10000}`,
+ current: `${Number(constants.current) / rate}`,
+ daily: `${Number(constants.daily) / rate}`,
+ dailyRemaining: `${Number(constants.dailyRemaining) / rate}`,
+ monthly: `${Number(constants.monthly) / rate}`,
+ monthlyRemaining: `${Number(constants.monthlyRemaining) / rate}`,
+ swap: `${Number(constants.swap) / rate}`,
};
-
+
return new Response(JSON.stringify(json));
}
@@ -979,118 +1045,226 @@ function interceptFetchRequest() {
name: 'USDT (Polygon)',
feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDT_MATIC)}`,
limits: { minimum: '1', maximum: '100000' },
- }
- ]
+ },
+ ];
+
return new Response(JSON.stringify(json));
}
- if (isEstimateRequest) {
- const body = args[1]?.body
- if (!body) throw new Error('[Demo] No body found in request')
- type EstimateRequest = {
- from: Record,
- to: SwapAsset,
- includedFees: 'required'
- };
- const { from: fromObj, to, includedFees: _includedFees } = JSON.parse(body.toString()) as EstimateRequest
- const from = Object.keys(fromObj)[0] as SwapAsset
+ // if (isEstimateRequest) {
+ // const body = args[1]?.body;
+ // if (!body) throw new Error('[Demo] No body found in request');
+ // type EstimateRequest = {
+ // from: Record,
+ // to: SwapAsset,
+ // includedFees: 'required',
+ // };
+ // const { from: fromObj, to } = JSON.parse(body.toString()) as EstimateRequest;
+ // const from = Object.keys(fromObj)[0] as SwapAsset;
+
+ // // Validate the request
+ // if (!from || !to) {
+ // console.error('[Demo] Invalid request parameters:', { from, to });
+ // return new Response(JSON.stringify({
+ // error: 'Invalid request parameters',
+ // status: 400,
+ // }), { status: 400 });
+ // }
+
+ // // Calculate fees based on the value
+
+ // const value = fromObj[from];
+ // const networkFee = value * 0.0005; // 0.05% network fee
+ // // const escrowFee = value * 0.001; // 0.1% escrow fee
+
+ // // Calculate the destination amount using our mock rates
+ // const estimatedAmount = calculateEstimatedAmount(from, to, value);
+
+ // // Create mock estimate response with a valid structure
+ // const estimate: FastspotEstimate[] = [{
+ // from: [{
+ // amount: `${value}`,
+ // name: from,
+ // symbol: from,
+ // finalizeNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(from)}`,
+ // },
+ // fundingNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(from)}`,
+ // },
+ // operatingNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(from)}`,
+ // },
+ // }],
+ // to: [{
+ // amount: `${estimatedAmount}`,
+ // name: to,
+ // symbol: to,
+ // finalizeNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(to)}`,
+ // },
+ // fundingNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(to)}`,
+ // },
+ // operatingNetworkFee: {
+ // total: `${networkFee}`,
+ // totalIsIncluded: false,
+ // perUnit: `${getNetworkFeePerUnit(to)}`,
+ // },
+ // }],
+ // direction: 'forward',
+ // serviceFeePercentage: 0.01,
+ // }];
+
+ // console.log('[Demo] Mock estimate:', estimate);
+
+ // return new Response(JSON.stringify(estimate));
+ // }
+
+ if (isSwapRequest) {
+ const swapId = url.pathname.split('/').slice(-1)[0];
+
+ if (swapId === 'swaps') {
+ const { patchAccount, activeAccountInfo } = useAccountStore();
+ const newBtcAddress = demoBtcAddress + Math.random().toString(36).slice(2, 9);
+
+ patchAccount(activeAccountInfo.value?.id, {
+ ...activeAccountInfo,
+ btcAddresses: {
+ external: [...(activeAccountInfo.value?.btcAddresses.external || []), newBtcAddress],
+ internal: [...(activeAccountInfo.value?.btcAddresses.internal || []), newBtcAddress],
+ },
+ });
+
+ const { addAddressInfos } = useBtcAddressStore();
+ addAddressInfos([{
+ address: newBtcAddress,
+ txoCount: 0, // Total number of outputs received
+ utxos: [],
+ }]);
+ } else {
+ listenForSwapChanges();
+
+ console.log('[Demo] Swap request:', swapId);
+ const swap = onGoingSwaps.get(swapId);
+ if (!swap) {
+ return new Response(JSON.stringify({
+ error: 'Swap not found',
+ status: 404,
+ }), { status: 404 });
+ }
- // Validate the request
- if (!from || !to) {
- return new Response(JSON.stringify({
- error: 'Invalid request parameters',
- status: 400,
- }), { status: 400 });
- }
+ console.log('[Demo] Swap:', swap);
+ const expirationTimestamp = Math.floor(Date.now() / 1000) + 3600;
- // Calculate fees based on the value
-
- const value = fromObj[from];
- const networkFee = Math.floor(value * 0.0005); // 0.05% network fee
- const escrowFee = Math.floor(value * 0.001); // 0.1% escrow fee
-
- // Calculate the destination amount using our mock rates
- const estimatedAmount = calculateEstimatedAmount(from, to, value);
-
- // Create mock estimate response with a valid structure
- const estimate: FastspotEstimate[] = [{
- from: [{
- amount: `${value}`,
- name: from,
- symbol: from,
- finalizeNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(from)}`,
- },
- fundingNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(from)}`,
- },
- operatingNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(from)}`,
- }
- }],
- to: [{
- amount: `${estimatedAmount}`,
- name: to,
- symbol: to,
- finalizeNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(to)}`,
- },
- fundingNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(to)}`,
+ return new Response(JSON.stringify({
+ id: swapId,
+ status: SwapStatus.WAITING_FOR_CONFIRMATION,
+ expires: expirationTimestamp,
+ info: {
+ from: [
+ {
+ symbol: swap.fund.type,
+ amount: swap.fund.output.value / 1e8,
+ fundingNetworkFee: {
+ total: '0.000037',
+ perUnit: '0.000000240254',
+ totalIsIncluded: true,
+ },
+ operatingNetworkFee: {
+ total: '0',
+ perUnit: '0.000000240254',
+ totalIsIncluded: false,
+ },
+ finalizeNetworkFee: {
+ total: '0.0000346',
+ perUnit: '0.000000240254',
+ totalIsIncluded: false,
+ },
+ },
+ ],
+ to: [
+ {
+ symbol: swap.redeem.type,
+ amount: swap.redeem.value / 1e5,
+ fundingNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ operatingNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ finalizeNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ },
+ ],
+ serviceFeePercentage: 0.0025,
+ direction: 'reverse',
},
- operatingNetworkFee: {
- total: `${networkFee}`,
- totalIsIncluded: false,
- perUnit: `${getNetworkFeePerUnit(to)}`,
- }
- }],
- direction: 'forward',
- serviceFeePercentage: 0.01,
- }];
-
- console.log('[Demo] Mock estimate:', estimate); // eslint-disable no-console
-
- return new Response(JSON.stringify(estimate));
+ hash: '946dc06baf94ee49a1bd026eff8eb4f30d34c9e162211667dbebd5a5282e6294',
+ contracts: [
+ {
+ asset: swap.fund.type,
+ refund: {
+ address: swap.fund.refundAddress,
+ },
+ recipient: {
+ address: swap.fund.refundAddress,
+ },
+ amount: swap.fund.output.value,
+ timeout: expirationTimestamp,
+ direction: 'send',
+ status: 'pending',
+ id: '2MzQo4ehDrSEsxX7RnysLL6VePD3tuNyx4M',
+ intermediary: { },
+ },
+ {
+ asset: swap.redeem.type,
+ refund: {
+ address: swap.redeem.recipient,
+ },
+ recipient: {
+ address: swap.redeem.recipient,
+ },
+ amount: swap.redeem.value,
+ timeout: expirationTimestamp,
+ direction: 'receive',
+ status: 'pending',
+ id: 'eff8a1a5-4f4e-3895-b95c-fd5a40c99001',
+ intermediary: { },
+ },
+ ],
+ }));
+ }
}
return originalFetch(...args);
};
}
-/**
- * Network fee helper function
- */
-function getNetworkFee(asset: string): number {
- switch (asset) {
- case SwapAsset.BTC:
- return 10000; // 10k sats
- case SwapAsset.NIM:
- return 1000; // 1000 luna
- case SwapAsset.USDC_MATIC:
- case SwapAsset.USDT_MATIC:
- return 2000000000000000; // 0.002 MATIC in wei
- default:
- return 0;
- }
-}
-
/**
* Fee per unit helper function
*/
function getNetworkFeePerUnit(asset: string): number {
switch (asset) {
case SwapAsset.BTC:
- return 10; // 10 sats/vbyte
+ return Math.floor(Math.random() * 100) / 1e8; // 1 - 100 sats/vbyte
case SwapAsset.NIM:
return 0; // luna per byte
case SwapAsset.USDC_MATIC:
@@ -1139,7 +1313,7 @@ function calculateEstimatedAmount(fromAsset: string, toAsset: string, value: num
* This allows users to interact with the send functionality without waiting for network sync.
*/
function enableSendModalInDemoMode() {
- let observing = false;
+ const observing = false;
const observer = new MutationObserver(() => {
// Target the send modal footer button
const sendButton = document.querySelector('.send-modal-footer .nq-button');
@@ -1155,9 +1329,9 @@ function enableSendModalInDemoMode() {
const footer = document.querySelector('.send-modal-footer');
if (footer) {
const footerNotice = footer.querySelector('.footer-notice') as HTMLDivElement;
- if (footerNotice && footerNotice.textContent &&
- (footerNotice.textContent.includes('Connecting to Bitcoin') ||
- footerNotice.textContent.includes('Syncing with Bitcoin'))) {
+ if (footerNotice && footerNotice.textContent
+ && (footerNotice.textContent.includes('Connecting to Bitcoin')
+ || footerNotice.textContent.includes('Syncing with Bitcoin'))) {
footerNotice.style.display = 'none';
}
}
@@ -1170,8 +1344,58 @@ function enableSendModalInDemoMode() {
const ignoreHubRequests = [
'addBtcAddresses',
- 'on'
-]
+ 'on',
+];
+
+interface SetupSwapArgs {
+ accountId: string;
+ swapId: string;
+ fund: {
+ type: 'BTC' | 'NIM' | 'USDC' | 'USDT',
+ inputs: {
+ address: string,
+ transactionHash: string,
+ outputIndex: number,
+ outputScript: string,
+ value: number,
+ }[],
+ output: {
+ value: number,
+ },
+ changeOutput: {
+ address: string,
+ value: number,
+ },
+ refundAddress: string,
+ };
+ redeem: {
+ type: 'BTC' | 'NIM' | 'USDC' | 'USDT',
+ recipient: string,
+ value: number,
+ fee: number,
+ validityStartHeight: number,
+ };
+ fundingFiatRate: number;
+ redeemingFiatRate: number;
+ fundFees: {
+ processing: number,
+ redeeming: number,
+ };
+ redeemFees: {
+ funding: number,
+ processing: number,
+ };
+ serviceSwapFee: number;
+ nimiqAddresses: {
+ address: string,
+ balance: number,
+ }[];
+ polygonAddresses: {
+ address: string,
+ usdcBalance: number,
+ usdtBalance: number,
+ }[];
+}
/**
* Replacement of the Hub API class to capture and redirect calls to our demo modals instead.
@@ -1182,558 +1406,588 @@ export class DemoHubApi extends HubApi {
return new Proxy(instance, {
get(target, prop: keyof HubApi) {
if (typeof target[prop] === 'function') {
- return async (...args: Parameters) => {
- return new Promise(async (resolve, reject) => {
- const requestName = String(prop);
- const [firstArg] = args;
- // eslint-disable-next-line no-console
- console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
-
- if (ignoreHubRequests.includes(requestName)) {
- return;
- }
- // Find the setupSwap handler in the DemoHubApi class and replace it with this:
-
- if (requestName === 'setupSwap') {
- console.log({
- firstArg,
- args,
- })
- return
- }
- // if (requestName === 'setupSwap') {
- // (firstArg as Function)();
- // // Get swap amount and asset details from the DOM
- // const leftColumn = document.querySelector('.swap-amounts .left-column');
- // const leftAmountElement = leftColumn?.querySelector('.width-value');
- // const rightColumn = document.querySelector('.swap-amounts .right-column');
- // const rightAmountElement = rightColumn?.querySelector('.width-value');
-
- // if (!leftAmountElement || !rightAmountElement || !leftColumn || !rightColumn) {
- // console.warn('[Demo] Could not find swap amount elements');
- // return {};
- // }
-
- // const leftValue = Number((leftAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
- // const leftAsset = leftColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
-
- // const rightValue = Number((rightAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
- // const rightAsset = rightColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
-
- // console.log(`[Demo] Setting up swap: ${leftValue} ${leftAsset} -> ${rightValue} ${rightAsset}`);
-
- // // Check if we have valid values
- // if (!leftValue || !rightValue || !leftAsset || !rightAsset) {
- // console.warn('[Demo] Missing swap values');
- // return {};
- // }
-
- // const direction = leftValue < rightValue ? 'forward' : 'reverse';
- // const fromAsset = direction === 'forward' ? leftAsset : rightAsset;
- // // @ts-expect-error Object key not specific enough?
- // const toAsset: RequestAsset = direction === 'forward'
- // ? { [rightAsset]: rightValue }
- // : { [leftAsset]: leftValue };
-
- // const swapSuggestion = await createSwap(fromAsset, toAsset);
-
- // const { config } = useConfig();
-
- // let fund: HtlcCreationInstructions | null = null;
- // let redeem: HtlcSettlementInstructions | null = null;
-
- // const { activeAddressInfo } = useAddressStore();
-
- // const { availableExternalAddresses } = useBtcAddressStore();
- // const nimAddress = activeAddressInfo.value!.address;
- // const btcAddress = availableExternalAddresses.value[0];
-
- // if (swapSuggestion.from.asset === SwapAsset.NIM) {
- // const nimiqClient = await getNetworkClient();
- // await nimiqClient.waitForConsensusEstablished();
- // const headHeight = await nimiqClient.getHeadHeight();
- // if (headHeight > 100) {
- // useNetworkStore().state.height = headHeight;
- // } else {
- // throw new Error('Invalid network state, try please reloading the app');
- // }
-
- // fund = {
- // type: SwapAsset.NIM,
- // sender: nimAddress,
- // value: swapSuggestion.from.amount,
- // fee: swapSuggestion.from.fee,
- // validityStartHeight: useNetworkStore().state.height,
- // };
- // }
-
- // const { accountUtxos, accountBalance: accountBtcBalance, } = useBtcAddressStore();
- // if (swapSuggestion.from.asset === SwapAsset.BTC) {
- // const electrumClient = await getElectrumClient();
- // await electrumClient.waitForConsensusEstablished();
-
-
- // // Assemble BTC inputs
- // // Transactions to an HTLC are 46 weight units bigger because of the longer recipient address
- // const requiredInputs = selectOutputs(
- // accountUtxos.value, swapSuggestion.from.amount, swapSuggestion.from.feePerUnit, 48);
- // let changeAddress: string;
- // if (requiredInputs.changeAmount > 0) {
- // const { nextChangeAddress } = useBtcAddressStore();
- // if (!nextChangeAddress.value) {
- // // FIXME: If no unused change address is found, need to request new ones from Hub!
- // throw new Error('No more unused change addresses (not yet implemented)');
- // }
- // changeAddress = nextChangeAddress.value;
- // }
-
- // fund = {
- // type: SwapAsset.BTC,
- // inputs: requiredInputs.utxos.map((utxo) => ({
- // address: utxo.address,
- // transactionHash: utxo.transactionHash,
- // outputIndex: utxo.index,
- // outputScript: utxo.witness.script,
- // value: utxo.witness.value,
- // })),
- // output: {
- // value: swapSuggestion.from.amount,
- // },
- // ...(requiredInputs.changeAmount > 0 ? {
- // changeOutput: {
- // address: changeAddress!,
- // value: requiredInputs.changeAmount,
- // },
- // } : {}),
- // refundAddress: btcAddress,
- // };
- // }
-
- // const {
- // activeAddress: activePolygonAddress,
- // accountUsdcBalance,
- // accountUsdtBridgedBalance,
- // } = usePolygonAddressStore();
-
-
- // if (swapSuggestion.from.asset === SwapAsset.USDC_MATIC) {
- // const [client, htlcContract] = await Promise.all([
- // getPolygonClient(),
- // getUsdcHtlcContract(),
- // ]);
- // const fromAddress = activePolygonAddress.value!;
-
- // const [
- // usdcNonce,
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // client.usdcToken.nonces(fromAddress) as Promise,
- // htlcContract.getNonce(fromAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'open' && method !== 'openWithPermit') {
- // throw new Error('Wrong USDC contract method');
- // }
-
- // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
- // // Keyguard's SwapIFrameApi.
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address token */ config.polygon.usdc.tokenContract,
- // /* uint256 amount */ swapSuggestion.from.amount,
- // /* address refundAddress */ fromAddress,
- // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
- // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint256 timeout */ 0,
- // /* uint256 fee */ fee,
- // ...(method === 'openWithPermit' ? [
- // // // Approve the maximum possible amount so afterwards we can use the `open` method for
- // // // lower fees
- // // /* uint256 value */ client.ethers
- // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
- // /* uint256 value */ swapSuggestion.from.amount + fee,
-
- // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint8 sigV */ 0,
- // ] : []),
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: fromAddress,
- // to: config.polygon.usdc.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdc.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdc.htlcContract,
- // },
- // };
-
- // fund = {
- // type: SwapAsset.USDC_MATIC,
- // ...relayRequest,
- // ...(method === 'openWithPermit' ? {
- // permit: {
- // tokenNonce: usdcNonce.toNumber(),
- // },
- // } : null),
- // };
- // }
-
- // if (swapSuggestion.from.asset === SwapAsset.USDT_MATIC) {
- // const [client, htlcContract] = await Promise.all([
- // getPolygonClient(),
- // getUsdtBridgedHtlcContract(),
- // ]);
- // const fromAddress = activePolygonAddress.value!;
-
- // const [
- // usdtNonce,
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // client.usdtBridgedToken.getNonce(fromAddress) as Promise,
- // htlcContract.getNonce(fromAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'open' && method !== 'openWithApproval') {
- // throw new Error('Wrong USDT contract method');
- // }
-
- // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
- // // Keyguard's SwapIFrameApi.
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address token */ config.polygon.usdt_bridged.tokenContract,
- // /* uint256 amount */ swapSuggestion.from.amount,
- // /* address refundAddress */ fromAddress,
- // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
- // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint256 timeout */ 0,
- // /* uint256 fee */ fee,
- // ...(method === 'openWithApproval' ? [
- // // // Approve the maximum possible amount so afterwards we can use the `open` method for
- // // // lower fees
- // // /* uint256 approval */ client.ethers
- // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
- // /* uint256 approval */ swapSuggestion.from.amount + fee,
-
- // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint8 sigV */ 0,
- // ] : []),
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: fromAddress,
- // to: config.polygon.usdt_bridged.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdt_bridged.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdt_bridged.htlcContract,
- // },
- // };
-
- // fund = {
- // type: SwapAsset.USDT_MATIC,
- // ...relayRequest,
- // ...(method === 'openWithApproval' ? {
- // approval: {
- // tokenNonce: usdtNonce.toNumber(),
- // },
- // } : null),
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.NIM) {
- // const nimiqClient = await getNetworkClient();
- // await nimiqClient.waitForConsensusEstablished();
- // const headHeight = await nimiqClient.getHeadHeight();
- // if (headHeight > 100) {
- // useNetworkStore().state.height = headHeight;
- // } else {
- // throw new Error('Invalid network state, try please reloading the app');
- // }
-
- // redeem = {
- // type: SwapAsset.NIM,
- // recipient: nimAddress, // My address, must be redeem address of HTLC
- // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Luna
- // fee: swapSuggestion.to.fee, // Luna
- // validityStartHeight: useNetworkStore().state.height,
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.BTC) {
- // const electrumClient = await getElectrumClient();
- // await electrumClient.waitForConsensusEstablished();
-
- // redeem = {
- // type: SwapAsset.BTC,
- // input: {
- // // transactionHash: transaction.transactionHash,
- // // outputIndex: output.index,
- // // outputScript: output.script,
- // value: swapSuggestion.to.amount, // Sats
- // },
- // output: {
- // address: btcAddress, // My address, must be redeem address of HTLC
- // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Sats
- // },
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.USDC_MATIC) {
- // const htlcContract = await getUsdcHtlcContract();
- // const toAddress = activePolygonAddress.value!;
-
- // const [
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // htlcContract.getNonce(toAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'redeemWithSecretInData') {
- // throw new Error('Wrong USDC contract method');
- // }
-
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address target */ toAddress,
- // /* uint256 fee */ fee,
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: toAddress,
- // to: config.polygon.usdc.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdc.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdc.htlcContract,
- // },
- // };
-
- // redeem = {
- // type: SwapAsset.USDC_MATIC,
- // ...relayRequest,
- // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.USDT_MATIC) {
- // const htlcContract = await getUsdtBridgedHtlcContract();
- // const toAddress = activePolygonAddress.value!;
-
- // const [
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // htlcContract.getNonce(toAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'redeemWithSecretInData') {
- // throw new Error('Wrong USDT contract method');
- // }
-
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address target */ toAddress,
- // /* uint256 fee */ fee,
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: toAddress,
- // to: config.polygon.usdt_bridged.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdt_bridged.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdt_bridged.htlcContract,
- // },
- // };
-
- // redeem = {
- // type: SwapAsset.USDT_MATIC,
- // ...relayRequest,
- // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
- // };
- // }
-
- // if (!fund || !redeem) {
- // reject(new Error('UNEXPECTED: No funding or redeeming data objects'));
- // return;
- // }
-
- // const serviceSwapFee = Math.round(
- // (swapSuggestion.from.amount - swapSuggestion.from.serviceNetworkFee)
- // * swapSuggestion.serviceFeePercentage,
- // );
-
- // const { addressInfos } = useAddressStore();
-
- // const { activeAccountInfo } = useAccountStore();
-
- // const { currency, exchangeRates } = useFiatStore();
-
- // const {
- // activeAddress
-
- // } = usePolygonAddressStore();
-
- // const request: Omit = {
- // accountId: activeAccountInfo.value!.id,
- // swapId: swapSuggestion.id,
- // fund,
- // redeem,
-
- // layout: 'slider',
- // direction: direction === 'forward' ? 'left-to-right' : 'right-to-left',
- // fiatCurrency: currency.value,
- // fundingFiatRate: exchangeRates.value[assetToCurrency(
- // fund.type as SupportedSwapAsset,
- // )][currency.value]!,
- // redeemingFiatRate: exchangeRates.value[assetToCurrency(
- // redeem.type as SupportedSwapAsset,
- // )][currency.value]!,
- // fundFees: {
- // processing: 0,
- // redeeming: swapSuggestion.from.serviceNetworkFee,
- // },
- // redeemFees: {
- // funding: swapSuggestion.to.serviceNetworkFee,
- // processing: 0,
- // },
- // serviceSwapFee,
- // nimiqAddresses: addressInfos.value.map((addressInfo) => ({
- // address: addressInfo.address,
- // balance: addressInfo.balance || 0,
- // })),
- // bitcoinAccount: {
- // balance: accountBtcBalance.value,
- // },
- // polygonAddresses: activePolygonAddress.value ? [{
- // address: activePolygonAddress.value,
- // usdcBalance: accountUsdcBalance.value,
- // usdtBalance: accountUsdtBridgedBalance.value,
- // }] : [],
- // };
-
- // console.log('[Demo] Faking swap setup with request:', request);
- // resolve(request)
- // return;
- // }
-
- // Wait for router readiness
- await new Promise((resolve) => {
- demoRouter.onReady(resolve);
+ return async (...args: Parameters) => new Promise(async (resolve, reject) => {
+ const requestName = String(prop);
+ const [firstArg] = args;
+ // eslint-disable-next-line no-console
+ console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
+
+ if (ignoreHubRequests.includes(requestName)) {
+ return;
+ }
+ // Find the setupSwap handler in the DemoHubApi class and replace it with this:
+
+ if (requestName === 'setupSwap') {
+ console.log('[DEMO]', {
+ firstArg,
+ args,
+ promise: await firstArg,
});
-
- // eslint-disable-next-line no-console
- console.log('[Demo] Redirecting to fallback modal');
- demoRouter.push(`/${DemoModal.Fallback}`);
+ const swap = await firstArg as SetupSwapArgs;
+ const signerTransaction: SetupSwapResult = {
+ nim: {
+ transaction: new Uint8Array(),
+ serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
+ // serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925200000000000000000000000000000000000000000000000000000000000000000002003b571b3481bec4340ed715f6b59add1947667f2d51a70d05cc7b5778dae6e009a5342d2e62977c877342b1972ebe064e77c473603ac9a6b97ac1bdd0fa543f37487fab37256fad87d79314eee2dff3fc70623057fe17f07aa39f9cb9f99db0a',
+ hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
+ raw: {
+ signerPublicKey: new Uint8Array(),
+ signature: new Uint8Array(),
+ sender: 'NQ86 D3M0 SW4P NB59 U3F8 NDLX CE0P AFMX Y52S',
+ senderType: 2,
+ recipient: swap.redeem.recipient,
+ recipientType: 0,
+ value: swap.redeem.value,
+ fee: 0,
+ validityStartHeight: swap.redeem.validityStartHeight,
+ extraData: new Uint8Array(),
+ flags: 0,
+ networkId: 5,
+ proof: new Uint8Array(),
+ },
+ },
+ btc: {
+ serializedTx: '0200000000010168c8952af998f2c68412a848a72d1f9b0b7ff27417df1cb85514c97474b51ba40000000000ffffffff026515000000000000220020bf0ffdd2ffb9a579973455cfe9b56515538b79361d5ae8a4d255dea2519ef77864c501000000000016001428257447efe2d254ce850ea2760274d233d86e5c024730440220792fa932d9d0591e3c5eb03f47d05912a1e21f3e76d169e383af66e47896ac8c02205947df5523490e4138f2da0fc5c9da3039750fe43bd217b68d26730fdcae7fbe012102ef8d4b51d1a075e67d62baa78991d5fc36a658fec28d8b978826058168ed2a1a00000000',
+ hash: '3090808993a796c26a614f5a4a36a48e0b4af6cd3e28e39f3f006e9a447da2b3',
+ },
+ refundTx: '02000000000101b3a27d449a6e003f9fe3283ecdf64a0b8ea4364a5a4f616ac296a793898090300000000000feffffff011e020000000000001600146d2146bb49f6d1de6b4f14e0a8074c79b887cef50447304402202a7dce2e39cf86ee1d7c1e9cc55f1e0fb26932fd22e5437e5e5804a9e5d220b1022031aa177ea085c10c4d54b2f5aa528aac0013b67f9ee674070aa2fb51894de80e0121025b4d40682bbcb5456a9d658971b725666a3cccaa2fb45d269d2f1486bf85b3c000636382012088a820be8719b9427f1551c4234f8b02d8f8aa055ae282b2e9eef6c155326ae951061f8876a914e546b01d8c9d9bf35f9f115132ce8eab7191a68d88ac67046716ca67b17576a9146d2146bb49f6d1de6b4f14e0a8074c79b887cef588ac686816ca67',
+ };
+
+ // Add to onGoingSwaps map
+ onGoingSwaps.set(swap.swapId, swap);
+
+ resolve(signerTransaction);
+ }
+ // if (requestName === 'setupSwap') {
+ // (firstArg as Function)();
+ // // Get swap amount and asset details from the DOM
+ // const leftColumn = document.querySelector('.swap-amounts .left-column');
+ // const leftAmountElement = leftColumn?.querySelector('.width-value');
+ // const rightColumn = document.querySelector('.swap-amounts .right-column');
+ // const rightAmountElement = rightColumn?.querySelector('.width-value');
+
+ // if (!leftAmountElement || !rightAmountElement || !leftColumn || !rightColumn) {
+ // console.warn('[Demo] Could not find swap amount elements');
+ // return {};
+ // }
+
+ // const leftValue = Number((leftAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
+ // const leftAsset = leftColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
+
+ // const rightValue = Number((rightAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
+ // const rightAsset = rightColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
+
+ // console.log(`[Demo] Setting up swap: ${leftValue} ${leftAsset} -> ${rightValue} ${rightAsset}`);
+
+ // // Check if we have valid values
+ // if (!leftValue || !rightValue || !leftAsset || !rightAsset) {
+ // console.warn('[Demo] Missing swap values');
+ // return {};
+ // }
+
+ // const direction = leftValue < rightValue ? 'forward' : 'reverse';
+ // const fromAsset = direction === 'forward' ? leftAsset : rightAsset;
+ // // @ts-expect-error Object key not specific enough?
+ // const toAsset: RequestAsset = direction === 'forward'
+ // ? { [rightAsset]: rightValue }
+ // : { [leftAsset]: leftValue };
+
+ // const swapSuggestion = await createSwap(fromAsset, toAsset);
+
+ // const { config } = useConfig();
+
+ // let fund: HtlcCreationInstructions | null = null;
+ // let redeem: HtlcSettlementInstructions | null = null;
+
+ // const { activeAddressInfo } = useAddressStore();
+
+ // const { availableExternalAddresses } = useBtcAddressStore();
+ // const nimAddress = activeAddressInfo.value!.address;
+ // const btcAddress = availableExternalAddresses.value[0];
+
+ // if (swapSuggestion.from.asset === SwapAsset.NIM) {
+ // const nimiqClient = await getNetworkClient();
+ // await nimiqClient.waitForConsensusEstablished();
+ // const headHeight = await nimiqClient.getHeadHeight();
+ // if (headHeight > 100) {
+ // useNetworkStore().state.height = headHeight;
+ // } else {
+ // throw new Error('Invalid network state, try please reloading the app');
+ // }
+
+ // fund = {
+ // type: SwapAsset.NIM,
+ // sender: nimAddress,
+ // value: swapSuggestion.from.amount,
+ // fee: swapSuggestion.from.fee,
+ // validityStartHeight: useNetworkStore().state.height,
+ // };
+ // }
+
+ // const { accountUtxos, accountBalance: accountBtcBalance, } = useBtcAddressStore();
+ // if (swapSuggestion.from.asset === SwapAsset.BTC) {
+ // const electrumClient = await getElectrumClient();
+ // await electrumClient.waitForConsensusEstablished();
+
+ // // Assemble BTC inputs
+ // // Transactions to an HTLC are 46 weight units bigger because of the longer recipient address
+ // const requiredInputs = selectOutputs(
+ // accountUtxos.value, swapSuggestion.from.amount, swapSuggestion.from.feePerUnit, 48);
+ // let changeAddress: string;
+ // if (requiredInputs.changeAmount > 0) {
+ // const { nextChangeAddress } = useBtcAddressStore();
+ // if (!nextChangeAddress.value) {
+ // // FIXME: If no unused change address is found, need to request new ones from Hub!
+ // throw new Error('No more unused change addresses (not yet implemented)');
+ // }
+ // changeAddress = nextChangeAddress.value;
+ // }
+
+ // fund = {
+ // type: SwapAsset.BTC,
+ // inputs: requiredInputs.utxos.map((utxo) => ({
+ // address: utxo.address,
+ // transactionHash: utxo.transactionHash,
+ // outputIndex: utxo.index,
+ // outputScript: utxo.witness.script,
+ // value: utxo.witness.value,
+ // })),
+ // output: {
+ // value: swapSuggestion.from.amount,
+ // },
+ // ...(requiredInputs.changeAmount > 0 ? {
+ // changeOutput: {
+ // address: changeAddress!,
+ // value: requiredInputs.changeAmount,
+ // },
+ // } : {}),
+ // refundAddress: btcAddress,
+ // };
+ // }
+
+ // const {
+ // activeAddress: activePolygonAddress,
+ // accountUsdcBalance,
+ // accountUsdtBridgedBalance,
+ // } = usePolygonAddressStore();
+
+ // if (swapSuggestion.from.asset === SwapAsset.USDC_MATIC) {
+ // const [client, htlcContract] = await Promise.all([
+ // getPolygonClient(),
+ // getUsdcHtlcContract(),
+ // ]);
+ // const fromAddress = activePolygonAddress.value!;
+
+ // const [
+ // usdcNonce,
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // client.usdcToken.nonces(fromAddress) as Promise,
+ // htlcContract.getNonce(fromAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'open' && method !== 'openWithPermit') {
+ // throw new Error('Wrong USDC contract method');
+ // }
+
+ // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
+ // // Keyguard's SwapIFrameApi.
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address token */ config.polygon.usdc.tokenContract,
+ // /* uint256 amount */ swapSuggestion.from.amount,
+ // /* address refundAddress */ fromAddress,
+ // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
+ // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint256 timeout */ 0,
+ // /* uint256 fee */ fee,
+ // ...(method === 'openWithPermit' ? [
+ // // // Approve the maximum possible amount so afterwards we can use the `open` method for
+ // // // lower fees
+ // // /* uint256 value */ client.ethers
+ // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
+ // /* uint256 value */ swapSuggestion.from.amount + fee,
+
+ // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint8 sigV */ 0,
+ // ] : []),
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: fromAddress,
+ // to: config.polygon.usdc.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdc.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdc.htlcContract,
+ // },
+ // };
+
+ // fund = {
+ // type: SwapAsset.USDC_MATIC,
+ // ...relayRequest,
+ // ...(method === 'openWithPermit' ? {
+ // permit: {
+ // tokenNonce: usdcNonce.toNumber(),
+ // },
+ // } : null),
+ // };
+ // }
+
+ // if (swapSuggestion.from.asset === SwapAsset.USDT_MATIC) {
+ // const [client, htlcContract] = await Promise.all([
+ // getPolygonClient(),
+ // getUsdtBridgedHtlcContract(),
+ // ]);
+ // const fromAddress = activePolygonAddress.value!;
+
+ // const [
+ // usdtNonce,
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // client.usdtBridgedToken.getNonce(fromAddress) as Promise,
+ // htlcContract.getNonce(fromAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'open' && method !== 'openWithApproval') {
+ // throw new Error('Wrong USDT contract method');
+ // }
+
+ // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
+ // // Keyguard's SwapIFrameApi.
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address token */ config.polygon.usdt_bridged.tokenContract,
+ // /* uint256 amount */ swapSuggestion.from.amount,
+ // /* address refundAddress */ fromAddress,
+ // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
+ // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint256 timeout */ 0,
+ // /* uint256 fee */ fee,
+ // ...(method === 'openWithApproval' ? [
+ // // // Approve the maximum possible amount so afterwards we can use the `open` method for
+ // // // lower fees
+ // // /* uint256 approval */ client.ethers
+ // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
+ // /* uint256 approval */ swapSuggestion.from.amount + fee,
+
+ // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* uint8 sigV */ 0,
+ // ] : []),
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: fromAddress,
+ // to: config.polygon.usdt_bridged.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdt_bridged.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdt_bridged.htlcContract,
+ // },
+ // };
+
+ // fund = {
+ // type: SwapAsset.USDT_MATIC,
+ // ...relayRequest,
+ // ...(method === 'openWithApproval' ? {
+ // approval: {
+ // tokenNonce: usdtNonce.toNumber(),
+ // },
+ // } : null),
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.NIM) {
+ // const nimiqClient = await getNetworkClient();
+ // await nimiqClient.waitForConsensusEstablished();
+ // const headHeight = await nimiqClient.getHeadHeight();
+ // if (headHeight > 100) {
+ // useNetworkStore().state.height = headHeight;
+ // } else {
+ // throw new Error('Invalid network state, try please reloading the app');
+ // }
+
+ // redeem = {
+ // type: SwapAsset.NIM,
+ // recipient: nimAddress, // My address, must be redeem address of HTLC
+ // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Luna
+ // fee: swapSuggestion.to.fee, // Luna
+ // validityStartHeight: useNetworkStore().state.height,
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.BTC) {
+ // const electrumClient = await getElectrumClient();
+ // await electrumClient.waitForConsensusEstablished();
+
+ // redeem = {
+ // type: SwapAsset.BTC,
+ // input: {
+ // // transactionHash: transaction.transactionHash,
+ // // outputIndex: output.index,
+ // // outputScript: output.script,
+ // value: swapSuggestion.to.amount, // Sats
+ // },
+ // output: {
+ // address: btcAddress, // My address, must be redeem address of HTLC
+ // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Sats
+ // },
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.USDC_MATIC) {
+ // const htlcContract = await getUsdcHtlcContract();
+ // const toAddress = activePolygonAddress.value!;
+
+ // const [
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // htlcContract.getNonce(toAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'redeemWithSecretInData') {
+ // throw new Error('Wrong USDC contract method');
+ // }
+
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address target */ toAddress,
+ // /* uint256 fee */ fee,
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: toAddress,
+ // to: config.polygon.usdc.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdc.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdc.htlcContract,
+ // },
+ // };
+
+ // redeem = {
+ // type: SwapAsset.USDC_MATIC,
+ // ...relayRequest,
+ // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
+ // };
+ // }
+
+ // if (swapSuggestion.to.asset === SwapAsset.USDT_MATIC) {
+ // const htlcContract = await getUsdtBridgedHtlcContract();
+ // const toAddress = activePolygonAddress.value!;
+
+ // const [
+ // forwarderNonce,
+ // blockHeight,
+ // ] = await Promise.all([
+ // htlcContract.getNonce(toAddress) as Promise,
+ // getPolygonBlockNumber(),
+ // ]);
+
+ // const { fee, gasLimit, gasPrice, relay, method } = {
+ // fee: 1,
+ // gasLimit: 1,
+ // gasPrice: 1,
+ // relay: {
+ // pctRelayFee: 1,
+ // baseRelayFee: 1,
+ // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
+ // },
+ // method: 'open'
+ // };
+ // if (method !== 'redeemWithSecretInData') {
+ // throw new Error('Wrong USDT contract method');
+ // }
+
+ // const data = htlcContract.interface.encodeFunctionData(method, [
+ // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ // /* address target */ toAddress,
+ // /* uint256 fee */ fee,
+ // ]);
+
+ // const relayRequest: RelayRequest = {
+ // request: {
+ // from: toAddress,
+ // to: config.polygon.usdt_bridged.htlcContract,
+ // data,
+ // value: '0',
+ // nonce: forwarderNonce.toString(),
+ // gas: gasLimit.toString(),
+ // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
+ // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
+ // },
+ // relayData: {
+ // gasPrice: gasPrice.toString(),
+ // pctRelayFee: relay.pctRelayFee.toString(),
+ // baseRelayFee: relay.baseRelayFee.toString(),
+ // relayWorker: relay.relayWorkerAddress,
+ // paymaster: config.polygon.usdt_bridged.htlcContract,
+ // paymasterData: '0x',
+ // clientId: Math.floor(Math.random() * 1e6).toString(10),
+ // forwarder: config.polygon.usdt_bridged.htlcContract,
+ // },
+ // };
+
+ // redeem = {
+ // type: SwapAsset.USDT_MATIC,
+ // ...relayRequest,
+ // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
+ // };
+ // }
+
+ // if (!fund || !redeem) {
+ // reject(new Error('UNEXPECTED: No funding or redeeming data objects'));
+ // return;
+ // }
+
+ // const serviceSwapFee = Math.round(
+ // (swapSuggestion.from.amount - swapSuggestion.from.serviceNetworkFee)
+ // * swapSuggestion.serviceFeePercentage,
+ // );
+
+ // const { addressInfos } = useAddressStore();
+
+ // const { activeAccountInfo } = useAccountStore();
+
+ // const { currency, exchangeRates } = useFiatStore();
+
+ // const {
+ // activeAddress
+
+ // } = usePolygonAddressStore();
+
+ // const request: Omit = {
+ // accountId: activeAccountInfo.value!.id,
+ // swapId: swapSuggestion.id,
+ // fund,
+ // redeem,
+
+ // layout: 'slider',
+ // direction: direction === 'forward' ? 'left-to-right' : 'right-to-left',
+ // fiatCurrency: currency.value,
+ // fundingFiatRate: exchangeRates.value[assetToCurrency(
+ // fund.type as SupportedSwapAsset,
+ // )][currency.value]!,
+ // redeemingFiatRate: exchangeRates.value[assetToCurrency(
+ // redeem.type as SupportedSwapAsset,
+ // )][currency.value]!,
+ // fundFees: {
+ // processing: 0,
+ // redeeming: swapSuggestion.from.serviceNetworkFee,
+ // },
+ // redeemFees: {
+ // funding: swapSuggestion.to.serviceNetworkFee,
+ // processing: 0,
+ // },
+ // serviceSwapFee,
+ // nimiqAddresses: addressInfos.value.map((addressInfo) => ({
+ // address: addressInfo.address,
+ // balance: addressInfo.balance || 0,
+ // })),
+ // bitcoinAccount: {
+ // balance: accountBtcBalance.value,
+ // },
+ // polygonAddresses: activePolygonAddress.value ? [{
+ // address: activePolygonAddress.value,
+ // usdcBalance: accountUsdcBalance.value,
+ // usdtBalance: accountUsdtBridgedBalance.value,
+ // }] : [],
+ // };
+
+ // console.log('[Demo] Faking swap setup with request:', request);
+ // resolve(request)
+ // return;
+ // }
+
+ // Wait for router readiness
+ await new Promise((resolve) => {
+ demoRouter.onReady(resolve);
});
- }
+
+ // eslint-disable-next-line no-console
+ console.log('[Demo] Redirecting to fallback modal');
+ demoRouter.push(`/${DemoModal.Fallback}`);
+ });
}
return target[prop];
},
From 34c75ba44c23086fb3cdf5d925d9275cca333c37 Mon Sep 17 00:00:00 2001
From: onmax
Date: Mon, 10 Mar 2025 12:25:02 +0100
Subject: [PATCH 05/31] chore(demo): streamlined transactions functions
---
src/stores/Demo.ts | 1527 +++++++++++++++++---------------------------
1 file changed, 581 insertions(+), 946 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index 711f3a123..c6a6f417c 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -3,14 +3,13 @@ import { createStore } from 'pinia';
import VueRouter from 'vue-router';
import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
-import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
-import { STAKING_CONTRACT_ADDRESS } from '@/lib/Constants';
+import { Address, KeyPair, PlainTransactionDetails, PlainTransactionRecipientData, PrivateKey, TransactionFormat } from '@nimiq/core';
import { AccountType, useAccountStore } from '@/stores/Account';
import { AddressType, useAddressStore } from '@/stores/Address';
-import { toSecs, useTransactionsStore } from '@/stores/Transactions';
-import { useBtcTransactionsStore } from '@/stores/BtcTransactions';
-import { useUsdtTransactionsStore, TransactionState as UsdtTransactionState } from '@/stores/UsdtTransactions';
-import { useUsdcTransactionsStore, TransactionState as UsdcTransactionState } from '@/stores/UsdcTransactions';
+import { toSecs, type Transaction as NimTransaction, useTransactionsStore } from '@/stores/Transactions';
+import { useBtcTransactionsStore, type Transaction as BtcTransaction } from '@/stores/BtcTransactions';
+import { useUsdtTransactionsStore, TransactionState as UsdtTransactionState, type Transaction as UsdtTransaction } from '@/stores/UsdtTransactions';
+import { useUsdcTransactionsStore, TransactionState as UsdcTransactionState, type Transaction as UsdcTransaction } from '@/stores/UsdcTransactions';
import { useStakingStore } from '@/stores/Staking';
import { useAccountSettingsStore } from '@/stores/AccountSettings';
import { usePolygonAddressStore } from '@/stores/PolygonAddress';
@@ -24,6 +23,8 @@ import { useUsdcContactsStore } from './UsdcContacts';
import { useUsdtContactsStore } from './UsdtContacts';
import { useFiatStore } from './Fiat';
import { SwapState, useSwapsStore } from './Swaps';
+import { getUsdcHtlcContract } from '@/ethers';
+import { useConfig } from '@/composables/useConfig';
export type DemoState = {
active: boolean,
@@ -57,68 +58,19 @@ const demoPolygonAddress = '0xabc123DemoPolygonAddress';
const buyFromAddress = 'NQ04 JG63 HYXL H3QF PPNA 7ED7 426M 3FQE FHE5';
// We keep this as our global/final balance, which should result from the transactions
-const nimInitialBalance = 14041800000; // 14,041,800,000 - 14 april, 2018
-const usdtInitialBalance = 5000000000; // 5000 USDT (6 decimals)
-const usdcInitialBalance = 3000000000; // 3000 USDC (6 decimals)
+const nimInitialBalance = 140_418_00000; // 14,041,800,000 - 14 april, 2018. 5 decimals.
+const btcInitialBalance = 1_0000000; // 1 BTC (8 decimals)
+const usdtInitialBalance = 5_000_000000; // 5000 USDT (6 decimals)
+const usdcInitialBalance = 3_000_000000; // 3000 USDC (6 decimals)
// Swaps
const onGoingSwaps = new Map();
-let swapInterval: NodeJS.Timeout | null = null;
-function listenForSwapChanges() {
- if (swapInterval) return;
- swapInterval = setInterval(() => {
- const swap = useSwapsStore().activeSwap.value;
- if (!swap) return;
- console.log('[Demo] Active swap:', { swap, state: swap.state });
- switch (swap.state) {
- case SwapState.AWAIT_INCOMING:
- console.log('[Demo] Swap is in AWAIT_INCOMING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.CREATE_OUTGOING,
- });
- break;
- case SwapState.CREATE_OUTGOING:
- console.log('[Demo] Swap is in CREATE_OUTGOING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.AWAIT_SECRET,
- });
- break;
- case SwapState.AWAIT_SECRET:
- console.log('[Demo] Swap is in AWAIT_SECRET state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.SETTLE_INCOMING,
- });
- break;
- case SwapState.SETTLE_INCOMING:
- console.log('[Demo] Swap is in SETTLE_INCOMING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.COMPLETE,
- });
- break;
- case SwapState.COMPLETE:
- console.log('[Demo] Swap is in COMPLETE state');
- if (swapInterval)clearInterval(swapInterval);
- swapInterval = null;
- break;
- default:
- console.log('[Demo] Swap is in unknown state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.AWAIT_INCOMING,
- });
- break;
- }
- }, 15000);
-}
-
// We keep a reference to the router here.
let demoRouter: VueRouter;
+// #region Store definition
+
/**
* Main store for the demo environment.
*/
@@ -149,37 +101,44 @@ export const useDemoStore = createStore({
setupDemoAddresses();
setupDemoAccount();
- generateFakeNimTransactions();
+ insertFakeNimTransactions();
+ insertFakeBtcTransactions();
- const { addTransactions: addBtcTransactions } = useBtcTransactionsStore();
- addBtcTransactions(generateFakeBtcTransactions());
+ if (useConfig().config.polygon.enabled) {
+ insertFakeUsdcTransactions();
+ insertFakeUsdtTransactions();
+ }
attachIframeListeners();
replaceStakingFlow();
replaceBuyNimFlow();
enableSendModalInDemoMode();
+
+ listenForSwapChanges();
},
/**
* Adds a pretend buy transaction to show a deposit coming in.
*/
async buyDemoNim(amount: number) {
- const { addTransactions } = useTransactionsStore();
- addTransactions([
- createFakeTransaction({
- value: amount,
- recipient: demoNimAddress,
- sender: buyFromAddress,
- data: {
- type: 'raw',
- raw: encodeTextToHex('Online Purchase'),
- },
- }),
- ]);
+ const tx: Partial = {
+ value: amount,
+ recipient: demoNimAddress,
+ sender: buyFromAddress,
+ data: {
+ type: 'raw',
+ raw: encodeTextToHex('Online Purchase'),
+ },
+ };
+ insertFakeNimTransactions(transformNimTransaction([tx]));
},
},
});
+// #endregion
+
+// #region App setup
+
/**
* Checks if the 'demo' query param is present in the URL.
*/
@@ -287,9 +246,6 @@ function setupDemoAddresses() {
balanceUsdtBridged: usdtInitialBalance,
pol: 1,
}]);
-
- // Setup polygon token transactions
- generateFakePolygonTransactions();
}
/**
@@ -319,37 +275,109 @@ function setupDemoAccount() {
setActiveCurrency(CryptoCurrency.NIM);
}
+enum MessageEventName {
+ FlowChange = 'FlowChange'
+}
+
+/**
+ * Listens for messages from iframes (or parent frames) about changes in the user flow.
+ */
+function attachIframeListeners() {
+ window.addEventListener('message', (event) => {
+ if (!event.data || typeof event.data !== 'object') return;
+ const { kind, data } = event.data as DemoFlowMessage;
+ if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
+ useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
+ demoRouter.push(demoRoutes[data]);
+ }
+ });
+
+ demoRouter.afterEach((to) => {
+ const match = Object.entries(demoRoutes).find(([, route]) => route === to.path);
+ if (!match) return;
+ window.parent.postMessage({ kind: MessageEventName.FlowChange, data: match[0] as DemoFlowType }, '*');
+ });
+}
+
+// #endregion
+
+// #region NIM txs
+
+interface NimTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+}
+
/**
- * Generates fake NIM transactions spanning the last ~4 years.
- * They net to the global nimInitialBalance.
- * We use a helper function to calculate each transaction value so it doesn't end in 0.
+ * Defines transaction definitions for demo NIM transactions
*/
-function generateFakeNimTransactions() {
- // We'll define total fractions so the net sum is 1.
- const txDefinitions = [
- { fraction: -0.14, daysAgo: 1442, description: 'New Designer Backpack for Hiking Trip' },
- { fraction: -0.05, daysAgo: 1390, description: 'Streaming Subscription - Watch Anything Anytime', recipientLabel: 'Stream & Chill Co.' },
- { fraction: 0.1, daysAgo: 1370, description: 'Sold Vintage Camera to Photography Enthusiast', recipientLabel: 'Retro Photo Guy' },
- { fraction: -0.2, daysAgo: 1240, description: 'Groceries at Farmers Market' },
- { fraction: 0.2, daysAgo: 1185, description: 'Birthday Gift from Uncle Bob', recipientLabel: 'Uncle Bob 🎁' },
- { fraction: 0.3, daysAgo: 1120, description: 'Website Design Freelance Project', recipientLabel: 'Digital Nomad Inc.' },
- { fraction: 0.02, daysAgo: 980, description: 'Lunch Money Payback from Alex' },
- { fraction: 0.07, daysAgo: 940, description: 'Community Raffle Prize' },
- { fraction: -0.15, daysAgo: 875, description: 'Car Repair at Thunder Road Garage', recipientLabel: 'FixMyCar Workshop' },
- { fraction: -0.3, daysAgo: 780, description: 'Quarterly Apartment Rent', recipientLabel: 'Skyview Properties' },
- { fraction: -0.1, daysAgo: 720, description: 'Anniversary Dinner at Skyline Restaurant' },
- { fraction: -0.02, daysAgo: 650, description: 'Digital Book: "Blockchain for Beginners"' },
- { fraction: -0.03, daysAgo: 580, description: 'Music Festival Weekend Pass' },
- { fraction: 0.05, daysAgo: 540, description: 'Refund for Cancelled Flight' },
- { fraction: 0.5, daysAgo: 470, description: 'Software Development Project Payment', recipientLabel: 'Tech Solutions Ltd' },
- { fraction: 0.02, daysAgo: 390, description: 'Coffee Shop Reward Program Refund' },
- { fraction: -0.11, daysAgo: 320, description: 'Custom Tailored Suit Purchase' },
- { fraction: 0.06, daysAgo: 270, description: 'Website Testing Gig Payment' },
- { fraction: -0.08, daysAgo: 210, description: 'Electric Scooter Rental for Month' },
- { fraction: -0.12, daysAgo: 180, description: 'Online Course: "Advanced Crypto Trading"' },
- { fraction: 0.05, daysAgo: 120, description: 'Sold Digital Artwork', recipientLabel: 'NFT Collector' },
- { fraction: -0.1, daysAgo: 90, description: 'Quarterly Utility Bills', recipientLabel: 'City Power & Water' },
- { fraction: -0.1, daysAgo: 45, description: 'Winter Wardrobe Shopping' },
+function defineNimFakeTransactions(): Partial[] {
+ const txDefinitions: NimTransactionDefinition[] = [
+ { fraction: -0.05, daysAgo: 0.4, description: 'Local cafe coffee' },
+ { fraction: 0.1, daysAgo: 0.6, description: 'Red envelope from neighbor', recipientLabel: 'Neighbor' },
+ { fraction: -0.03, daysAgo: 1, description: 'Food truck snack' },
+ { fraction: -0.06, daysAgo: 2, description: 'Local store book' },
+ { fraction: -0.01, daysAgo: 2, description: 'Chicken waffle', recipientLabel: 'Roberto\'s Waffle' },
+ { fraction: -0.04, daysAgo: 3, description: 'Corner shop chai & snack' },
+ { fraction: -0.12, daysAgo: 3, description: 'Thai massage session' },
+ { fraction: -0.15, daysAgo: 6, description: 'Swedish flat-pack chair', recipientLabel: 'Furniture Mart' },
+ { fraction: 0.1, daysAgo: 6, description: 'Red envelope from family', recipientLabel: 'Family' },
+ { fraction: -0.08, daysAgo: 7, description: 'Cozy diner dinner', recipientLabel: 'Melissa' },
+ { fraction: -0.02, daysAgo: 7, description: 'Coworker snack', recipientLabel: 'Coworker' },
+ { fraction: -0.03, daysAgo: 8, description: 'Morning bus fare' },
+ { fraction: -0.05, daysAgo: 8, description: 'Local fruit pack' },
+ { fraction: 0.02, daysAgo: 10, description: 'Neighbor bill', recipientLabel: 'Neighbor' },
+ { fraction: -0.07, daysAgo: 12, description: 'Movie ticket night' },
+ { fraction: -0.04, daysAgo: 14, description: 'Trendy cafe coffee', recipientLabel: 'Cafe' },
+ { fraction: -0.03, daysAgo: 15, description: 'Market street food snack' },
+ { fraction: -0.1, daysAgo: 18, description: '' },
+ { fraction: -0.05, daysAgo: 20, description: 'Street vendor souvenir' },
+ { fraction: -0.08, daysAgo: 22, description: 'Quick haircut', recipientLabel: 'Barber' },
+ { fraction: -0.04, daysAgo: 25, description: 'Local dessert', recipientLabel: 'Jose' },
+ { fraction: -0.02, daysAgo: 27, description: 'Mall parking' },
+ { fraction: -0.1, daysAgo: 30, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
+ { fraction: 0.03, daysAgo: 32, description: 'Mistaken charge refund', recipientLabel: 'Store' },
+ { fraction: -0.06, daysAgo: 35, description: 'Taxi fare', recipientLabel: 'Crazy Taxi' },
+ { fraction: -0.04, daysAgo: 38, description: 'Local shop mug' },
+ { fraction: -0.01, daysAgo: 40, description: 'Local newspaper' },
+ { fraction: -0.05, daysAgo: 42, description: 'Coworker ride', recipientLabel: 'Coworker' },
+ { fraction: -0.07, daysAgo: 45, description: 'Bistro lunch' },
+ { fraction: -0.12, daysAgo: 45, description: 'Weekly market shopping', recipientLabel: 'Market' },
+ { fraction: -0.1, daysAgo: 50, description: 'Utility bill', recipientLabel: 'Utility Co.' },
+ { fraction: -0.03, daysAgo: 55, description: 'Corner snack pack' },
+ { fraction: -0.1, daysAgo: 60, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
+ { fraction: 0.05, daysAgo: 62, description: 'Client tip', recipientLabel: 'Client' },
+ { fraction: -0.06, daysAgo: 65, description: 'Hair trim', recipientLabel: 'Barber' },
+ { fraction: -0.09, daysAgo: 68, description: 'Takeaway meal' },
+ { fraction: -0.04, daysAgo: 70, description: 'Stall fresh juice' },
+ { fraction: -0.05, daysAgo: 72, description: 'Park picnic' },
+ { fraction: -0.03, daysAgo: 75, description: 'Local event fee', recipientLabel: 'Event Org' },
+ { fraction: -0.1, daysAgo: 78, description: 'Neighbors dinner', recipientLabel: 'Neighbors' },
+ { fraction: -0.12, daysAgo: 80, description: 'New shoes sale' },
+ { fraction: 0.1, daysAgo: 85, description: 'Festive cash gift', recipientLabel: 'Family' },
+ { fraction: -0.1, daysAgo: 90, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
+ { fraction: -0.05, daysAgo: 95, description: 'Bakery fresh bread', recipientLabel: 'Bakery' },
+ { fraction: -0.04, daysAgo: 100, description: 'Ice cream treat', recipientLabel: 'Ice Cream' },
+ { fraction: -0.08, daysAgo: 110, description: 'Fitness class fee', recipientLabel: 'Gym' },
+ { fraction: -0.03, daysAgo: 115, description: 'Meal discount' },
+ { fraction: 0.04, daysAgo: 120, description: 'Double charge refund', recipientLabel: 'Store' },
+ { fraction: -0.07, daysAgo: 125, description: 'Boutique trendy hat' },
+ { fraction: -0.02, daysAgo: 130, description: 'Local cause donation', recipientLabel: 'Charity' },
+ { fraction: -0.09, daysAgo: 140, description: 'Neighborhood dinner', recipientLabel: 'Food Joint' },
+ { fraction: -0.05, daysAgo: 150, description: 'Gadget repair fee', recipientLabel: 'Repair Shop' },
+ { fraction: -0.08, daysAgo: 200, description: 'Local play ticket', recipientLabel: 'Theater' },
+ { fraction: -0.1, daysAgo: 250, description: 'Community event bill', recipientLabel: 'Community' },
+ { fraction: 0.07, daysAgo: 300, description: 'Work bonus', recipientLabel: 'Employer' },
+ { fraction: -0.04, daysAgo: 400, description: 'Local art fair entry' },
+ { fraction: -0.06, daysAgo: 500, description: 'Online shop gadget', recipientLabel: 'Online Shop' },
+ { fraction: -0.1, daysAgo: 600, description: 'Popular local dinner', recipientLabel: 'Diner' },
+ { fraction: -0.12, daysAgo: 800, description: 'Home repair bill', recipientLabel: 'Repair Co.' },
+ { fraction: 0.15, daysAgo: 1000, description: 'Freelance project check', recipientLabel: 'Client' },
+ { fraction: -0.09, daysAgo: 1200, description: 'Local kitchen gear', recipientLabel: 'Kitchen Shop' },
+ { fraction: -0.1, daysAgo: 1442, description: 'Family reunion dinner', recipientLabel: 'Family' },
+
];
// Calculate sum of existing transactions to ensure they add up to exactly 1
@@ -360,7 +388,7 @@ function generateFakeNimTransactions() {
if (Math.abs(remainingFraction) > 0.001) { // Only add if there's a meaningful amount to balance
txDefinitions.push({
fraction: remainingFraction,
- daysAgo: 14,
+ daysAgo: 1450,
description: remainingFraction > 0
? 'Blockchain Hackathon Prize!'
: 'Annual Software Subscription Renewal',
@@ -368,8 +396,8 @@ function generateFakeNimTransactions() {
});
}
- const { addTransactions } = useTransactionsStore();
const { setContact } = useContactsStore();
+ const txs: Partial[] = [];
for (const def of txDefinitions) {
let txValue = Math.floor(nimInitialBalance * def.fraction);
@@ -387,125 +415,196 @@ function generateFakeNimTransactions() {
const value = Math.abs(txValue);
const timestamp = calculateDaysAgo(def.daysAgo);
const tx: Partial = { value, recipient, sender, timestamp, data };
- addTransactions([createFakeTransaction(tx)]);
-
+ txs.push(tx);
// Add contact if a recipientLabel is provided
if (def.recipientLabel && def.fraction < 0) {
setContact(address, def.recipientLabel);
}
}
+
+ return txs.sort((a, b) => a.timestamp! - b.timestamp!);
+}
+
+let head = 0;
+let nonce = 0;
+
+function transformNimTransaction(txs: Partial[]): NimTransaction[] {
+ return txs.map((tx) => {
+ head++;
+ nonce++;
+
+ return {
+ network: 'mainnet',
+ state: 'confirmed',
+ transactionHash: `0x${nonce.toString(16)}`,
+ sender: '',
+ senderType: 'basic',
+ recipient: '',
+ recipientType: 'basic',
+ value: 50000000,
+ fee: 0,
+ feePerByte: 0,
+ format: 'basic',
+ validityStartHeight: head,
+ blockHeight: head,
+ flags: 0,
+ timestamp: Date.now(),
+ size: 0,
+ valid: true,
+ proof: { raw: '', type: 'raw' },
+ data: tx.data || { type: 'raw', raw: '' },
+ ...tx,
+ }
+ });
+}
+
+/**
+ * Inserts NIM transactions into the store. If no definitions provided, uses default demo transactions.
+ */
+function insertFakeNimTransactions(txs = defineNimFakeTransactions()) {
+ const { addTransactions } = useTransactionsStore();
+ addTransactions(transformNimTransaction(txs));
+}
+
+// #region BTC txs
+
+interface BtcTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
+ address: string
}
/**
- * Generates fake Bitcoin transactions spanning 5.5 years
+ * Defines transaction definitions for demo BTC transactions.
+ * Note: We add the "address" field so that incoming transactions use the correct sender address.
*/
-function generateFakeBtcTransactions() {
- // Define transaction history with a similar structure to other currencies
- const txDefinitions = [
+function defineBtcFakeTransactions(): BtcTransaction[] {
+ const txDefinitions: BtcTransactionDefinition[] = [
{
+ fraction: 0.2,
daysAgo: 2000,
- value: 20000000, // 0.2 BTC
- incoming: true,
description: 'Initial BTC purchase from exchange',
+ incoming: true,
address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
- label: 'Satoshi Exchange',
+ recipientLabel: 'Satoshi Exchange'
},
{
+ fraction: 0.15,
daysAgo: 1600,
- value: 15000000, // 0.15 BTC
- incoming: true,
description: 'Mining pool payout',
+ incoming: true,
address: '1Hz7vQrRjnu3z9k7gxDYhKjEmABqChDvJr',
- label: 'Genesis Mining Pool',
+ recipientLabel: 'Genesis Mining Pool'
},
{
+ fraction: -0.19,
daysAgo: 1200,
- value: 19000000, // 0.19 BTC
- incoming: false,
description: 'Purchase from online marketplace',
+ incoming: false,
address: '1LxKe5kKdgGVwXukEgqFxh6DrCXF2Pturc',
- label: 'Digital Bazaar Shop',
+ recipientLabel: 'Digital Bazaar Shop'
},
{
+ fraction: 0.3,
daysAgo: 800,
- value: 30000000, // 0.3 BTC
- incoming: true,
description: 'Company payment for consulting',
+ incoming: true,
address: '1N7aecJuKGDXzYK8CgpnNRYxdhZvXPxp3B',
- label: 'Corporate Treasury',
+ recipientLabel: 'Corporate Treasury'
},
{
+ fraction: -0.15,
daysAgo: 365,
- value: 15000000, // 0.15 BTC
- incoming: false,
description: 'Auto-DCA investment program',
- address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz',
+ incoming: false,
+ address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz'
},
{
+ fraction: 0.075,
daysAgo: 180,
- value: 7500000, // 0.075 BTC
- incoming: true,
description: 'P2P sale of digital goods',
- address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS',
+ incoming: true,
+ address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS'
},
{
+ fraction: 0.05,
daysAgo: 60,
- value: 5000000, // 0.05 BTC
- incoming: true,
description: 'Recent purchase from exchange',
- address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
+ incoming: true,
+ address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg'
},
- ];
+ ].sort((a, b) => b.daysAgo - a.daysAgo);
- const { setSenderLabel } = useBtcLabelsStore();
+ // If the sum of fractions does not add up to 1, add a balancing transaction.
+ const existingSum = txDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const remainingFraction = 1 - existingSum;
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: remainingFraction,
+ daysAgo: 0,
+ description: remainingFraction > 0 ? 'Initial purchase' : 'Investment allocation',
+ incoming: remainingFraction > 0,
+ address: remainingFraction > 0 ? '1ExampleAddressForInitialPurchase' : '1ExampleAddressForInvestment',
+ recipientLabel: remainingFraction > 0 ? 'Prime Exchange' : 'Investment Fund',
+ });
+ }
+
+ return transformBtcTransaction(txDefinitions);
+}
- // Convert to BTC transaction format with inputs/outputs
+/**
+ * Transforms BTC transaction definitions into actual transactions.
+ */
+function transformBtcTransaction(txDefinitions: BtcTransactionDefinition[]): BtcTransaction[] {
const transactions = [];
const knownUtxos = new Map();
let txCounter = 1;
for (const def of txDefinitions) {
- // Create a transaction hash for this transaction
const txHash = `btc-tx-${txCounter++}`;
+ // Compute the value using the initial BTC balance and the fraction.
+ const value = Math.floor(btcInitialBalance * Math.abs(def.fraction));
- // Only add labels to select transactions to make the history look realistic
- if (def.label) {
- setSenderLabel(def.address, def.label);
- }
-
- const tx = {
+ const tx: BtcTransaction = {
+ addresses: [demoBtcAddress],
isCoinbase: false,
inputs: [
{
+ // For incoming tx, use the external address; for outgoing, use our demo address.
address: def.incoming ? def.address : demoBtcAddress,
outputIndex: 0,
index: 0,
- script: 'abcd',
+ script: 'script',
sequence: 4294967295,
- transactionHash: def.incoming ? txHash : getUTXOToSpend(knownUtxos)?.txHash || txHash,
- witness: ['abcd'],
+ transactionHash: def.incoming ? txHash : (getUTXOToSpend(knownUtxos)?.txHash || txHash),
+ witness: ['witness'],
},
],
outputs: def.incoming
? [
{
- value: def.value,
+ value,
address: demoBtcAddress,
- script: 'abcd',
+ script: 'script',
index: 0,
},
]
: [
{
- value: def.value,
+ value,
address: def.address,
- script: 'abcd',
+ script: 'script',
index: 0,
},
- { // Change output
- value: 900000,
+ {
+ // Change output.
+ value: 1000000,
address: demoBtcAddress,
- script: 'abcd',
+ script: 'script',
index: 1,
},
],
@@ -520,17 +619,21 @@ function generateFakeBtcTransactions() {
state: ElectrumTransactionState.CONFIRMED,
};
- // Update UTXOs for this transaction
updateUTXOs(knownUtxos, tx);
-
transactions.push(tx);
+
+ // Set labels if a recipient label is provided.
+ if (def.recipientLabel) {
+ const { setSenderLabel } = useBtcLabelsStore();
+ setSenderLabel(def.incoming ? def.address : demoBtcAddress, def.recipientLabel);
+ }
}
- // Set up the address with current UTXOs
+ // Update the address store with the current UTXOs.
const { addAddressInfos } = useBtcAddressStore();
addAddressInfos([{
address: demoBtcAddress,
- txoCount: transactions.length + 2, // Total number of outputs received
+ txoCount: transactions.length + 2, // Total number of outputs received.
utxos: Array.from(knownUtxos.values()),
}]);
@@ -538,10 +641,10 @@ function generateFakeBtcTransactions() {
}
/**
- * Tracks UTXO changes for BTC transactions
+ * Tracks UTXO changes for BTC transactions.
*/
function updateUTXOs(knownUtxos: Map, tx: any) {
- // Remove spent inputs
+ // Remove spent inputs.
for (const input of tx.inputs) {
if (input.address === demoBtcAddress) {
const utxoKey = `${input.transactionHash}:${input.outputIndex}`;
@@ -549,7 +652,7 @@ function updateUTXOs(knownUtxos: Map, tx: any) {
}
}
- // Add new outputs for our address
+ // Add new outputs for our address.
for (const output of tx.outputs) {
if (output.address === demoBtcAddress) {
const utxoKey = `${tx.transactionHash}:${output.index}`;
@@ -566,7 +669,7 @@ function updateUTXOs(knownUtxos: Map, tx: any) {
}
/**
- * Helper to get a UTXO to spend
+ * Helper to get a UTXO to spend.
*/
function getUTXOToSpend(knownUtxos: Map) {
if (knownUtxos.size === 0) return null;
@@ -579,131 +682,177 @@ function getUTXOToSpend(knownUtxos: Map) {
}
/**
- * Generates fake transactions for both USDC and USDT on Polygon
+ * Insert fake BTC transactions into the store.
*/
-function generateFakePolygonTransactions() {
- // Generate USDC transactions
- const usdcTxDefinitions = [
- { fraction: 0.3, daysAgo: 910, incoming: true, label: 'Yield Farming Protocol' },
- { fraction: -0.05, daysAgo: 840 },
- { fraction: 0.2, daysAgo: 730, incoming: true, label: 'Virtual Worlds Inc.' },
- { fraction: -0.1, daysAgo: 650 },
- { fraction: 0.4, daysAgo: 480, incoming: true, label: 'Innovation DAO' },
- { fraction: -0.15, daysAgo: 360, label: 'MetaGames Marketplace' },
- { fraction: 0.15, daysAgo: 180, incoming: true },
- { fraction: -0.08, daysAgo: 90 },
- ];
+function insertFakeBtcTransactions(txs = defineBtcFakeTransactions()) {
+ const { addTransactions } = useBtcTransactionsStore();
+ addTransactions(txs);
+}
- // Calculate sum of existing transactions and add balancing transaction
- const usdcExistingSum = usdcTxDefinitions.reduce((sum, def) =>
- sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
- const usdcRemainingFraction = 1 - usdcExistingSum;
-
- // Add balancing transaction
- if (Math.abs(usdcRemainingFraction) > 0.001) {
- usdcTxDefinitions.push({
- fraction: usdcRemainingFraction,
- daysAgo: 30,
- incoming: usdcRemainingFraction > 0,
- label: usdcRemainingFraction > 0 ? 'CryptoCreators Guild' : 'Annual Platform Subscription',
- });
- }
- // Generate USDT transactions
- const usdtTxDefinitions = [
- { fraction: 0.4, daysAgo: 360, incoming: true, label: 'LaunchPad Exchange' },
- { fraction: -0.1, daysAgo: 320 },
- { fraction: 0.2, daysAgo: 280, incoming: true, label: 'Crypto Consultants LLC' },
- { fraction: -0.15, daysAgo: 210 },
- { fraction: 0.3, daysAgo: 150, incoming: true },
- { fraction: -0.05, daysAgo: 90 },
- { fraction: 0.1, daysAgo: 45, incoming: true, label: 'Chain Testers' },
- { fraction: -0.12, daysAgo: 20 },
+// #endregion
+
+// #region Polygon txs
+
+const getRandomPolygonHash = () => `0x${Math.random().toString(16).slice(2, 66)}`;
+const getRandomPolygonAddress = () => `0x${Math.random().toString(16).slice(2, 42)}`;
+
+interface UsdcTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
+}
+
+/**
+ * Defines transaction definitions for demo USDC transactions
+ */
+function defineUsdcFakeTransactions(): UsdcTransaction[] {
+ const txDefinitions: UsdcTransactionDefinition[] = [
+ { fraction: 0.3, daysAgo: 0, description: 'DeFi yield harvest', incoming: true, recipientLabel: 'Yield Protocol' },
+ { fraction: -0.1, daysAgo: 2, description: 'NFT marketplace purchase', incoming: false, recipientLabel: 'NFT Market' },
+ { fraction: 0.2, daysAgo: 5, description: 'Bridge transfer', incoming: true, recipientLabel: 'Polygon Bridge' },
+ { fraction: -0.15, daysAgo: 10, description: 'DEX liquidity provision', incoming: false },
+ { fraction: 0.25, daysAgo: 20, description: 'Staking rewards', incoming: true, recipientLabel: 'Staking Pool' },
+ { fraction: -0.12, daysAgo: 30, description: 'GameFi item purchase', incoming: false },
+ { fraction: 0.3, daysAgo: 45, description: 'DAO compensation', incoming: true, recipientLabel: 'Treasury DAO' },
+ { fraction: -0.2, daysAgo: 60, description: 'Lending deposit', incoming: false, recipientLabel: 'Lending Protocol' },
];
- // Calculate sum of existing transactions and add balancing transaction
- const usdtExistingSum = usdtTxDefinitions.reduce((sum, def) =>
+ const existingSum = txDefinitions.reduce((sum, def) =>
sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
- const usdtRemainingFraction = 1 - usdtExistingSum;
-
- // Add balancing transaction
- if (Math.abs(usdtRemainingFraction) > 0.001) {
- usdtTxDefinitions.push({
- fraction: usdtRemainingFraction,
- daysAgo: 7,
- incoming: usdtRemainingFraction > 0,
- label: usdtRemainingFraction > 0 ? 'Protocol Testing Reward' : 'Annual Service Renewal',
+ const remainingFraction = 1 - existingSum;
+
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: remainingFraction,
+ daysAgo: remainingFraction > 0 ? 90 : 100,
+ description: remainingFraction > 0 ? 'Initial bridge deposit' : 'Protocol investment',
+ incoming: remainingFraction > 0,
+ recipientLabel: remainingFraction > 0 ? 'Bridge Service' : 'DeFi Protocol',
});
}
- // Generate and add USDC transactions
- const { addTransactions: addUsdcTxs } = useUsdcTransactionsStore();
- addUsdcTxs(generateTokenTransactions(usdcTxDefinitions, usdcInitialBalance, Config.polygon.usdc.tokenContract, UsdcTransactionState.CONFIRMED, useUsdcContactsStore));
-
- // Generate and add USDT transactions
- const { addTransactions: addUsdtTxs } = useUsdtTransactionsStore();
- addUsdtTxs(generateTokenTransactions(usdtTxDefinitions, usdtInitialBalance, Config.polygon.usdt_bridged.tokenContract, UsdtTransactionState.CONFIRMED, useUsdtContactsStore));
+ return transformUsdcTransaction(txDefinitions);
}
/**
- * Shared function to generate token transactions for Polygon tokens
+ * Transform USDC transaction definitions into actual transactions
*/
-function generateTokenTransactions(txDefinitions: any, initialBalance: number, tokenContract: string, confirmState: any, contactsStore: any) {
- const transactions = [];
- const { setContact } = contactsStore();
-
- for (const def of txDefinitions) {
- const value = Math.floor(initialBalance * Math.abs(def.fraction));
- const randomAddress = `0x${Math.random().toString(16).slice(2, 42)}`;
- const sender = def.incoming ? randomAddress : demoPolygonAddress;
- const recipient = def.incoming ? demoPolygonAddress : randomAddress;
-
- // Add contacts for select transactions (only if label is provided)
- if (def.label) {
- const addressToLabel = def.incoming ? randomAddress : randomAddress;
- setContact(addressToLabel, def.label);
+function transformUsdcTransaction(txDefinitions: UsdcTransactionDefinition[]): UsdcTransaction[] {
+ return txDefinitions.map((def, index) => {
+ const value = Math.floor(usdcInitialBalance * Math.abs(def.fraction));
+ const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
+ const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
+
+ if (def.recipientLabel) {
+ const { setContact } = useUsdcContactsStore();
+ setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
}
- transactions.push({
- token: tokenContract,
- transactionHash: `token-tx-${Math.random().toString(36).substr(2, 9)}`,
- logIndex: transactions.length,
- sender,
- recipient,
+ return {
+ token: Config.polygon.usdc.tokenContract,
+ transactionHash: getRandomPolygonHash(),
+ logIndex: index,
+ sender: randomAddress,
+ recipient: recipientAddress,
value,
- state: confirmState,
- blockHeight: 1000000 + transactions.length,
+ state: UsdcTransactionState.CONFIRMED,
+ blockHeight: 1000000 + index,
timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
- });
- }
+ };
+ });
+}
- return transactions;
+/**
+ * Insert fake USDC transactions into the store
+ */
+function insertFakeUsdcTransactions(txs = defineUsdcFakeTransactions()) {
+ const { addTransactions } = useUsdcTransactionsStore();
+ addTransactions(txs);
}
-enum MessageEventName {
- FlowChange = 'FlowChange'
+interface UsdtTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
}
/**
- * Listens for messages from iframes (or parent frames) about changes in the user flow.
+ * Defines transaction definitions for demo USDT transactions
*/
-function attachIframeListeners() {
- window.addEventListener('message', (event) => {
- if (!event.data || typeof event.data !== 'object') return;
- const { kind, data } = event.data as DemoFlowMessage;
- if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
- useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
- demoRouter.push(demoRoutes[data]);
+function defineUsdtFakeTransactions(): UsdtTransaction[] {
+ const txDefinitions: UsdtTransactionDefinition[] = [
+ { fraction: 0.25, daysAgo: 0, description: 'Trading profit', incoming: true, recipientLabel: 'DEX Trading' },
+ { fraction: -0.08, daysAgo: 3, description: 'Merchandise payment', incoming: false },
+ { fraction: 0.15, daysAgo: 7, description: 'Freelance payment', incoming: true, recipientLabel: 'Client Pay' },
+ { fraction: -0.12, daysAgo: 15, description: 'Service subscription', incoming: false, recipientLabel: 'Web3 Service' },
+ { fraction: 0.3, daysAgo: 25, description: 'P2P exchange', incoming: true },
+ { fraction: -0.18, daysAgo: 35, description: 'Platform fees', incoming: false },
+ { fraction: 0.2, daysAgo: 50, description: 'Revenue share', incoming: true, recipientLabel: 'Revenue Pool' },
+ { fraction: -0.15, daysAgo: 70, description: 'Marketing campaign', incoming: false, recipientLabel: 'Marketing DAO' },
+ ];
+
+ const existingSum = txDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const remainingFraction = 1 - existingSum;
+
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: remainingFraction,
+ daysAgo: remainingFraction > 0 ? 85 : 95,
+ description: remainingFraction > 0 ? 'Initial USDT deposit' : 'Portfolio rebalance',
+ incoming: remainingFraction > 0,
+ recipientLabel: remainingFraction > 0 ? 'Bridge Protocol' : 'Portfolio Manager',
+ });
+ }
+
+ return transformUsdtTransaction(txDefinitions);
+}
+
+/**
+ * Transform USDT transaction definitions into actual transactions
+ */
+function transformUsdtTransaction(txDefinitions: UsdtTransactionDefinition[]): UsdtTransaction[] {
+ return txDefinitions.map((def, index) => {
+ const value = Math.floor(usdtInitialBalance * Math.abs(def.fraction));
+ const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
+ const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
+
+ if (def.recipientLabel) {
+ const { setContact } = useUsdtContactsStore();
+ setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
}
- });
- demoRouter.afterEach((to) => {
- const match = Object.entries(demoRoutes).find(([, route]) => route === to.path);
- if (!match) return;
- window.parent.postMessage({ kind: MessageEventName.FlowChange, data: match[0] as DemoFlowType }, '*');
+ return {
+ token: Config.polygon.usdt_bridged.tokenContract,
+ transactionHash: getRandomPolygonHash(),
+ logIndex: index,
+ sender: randomAddress,
+ recipient: recipientAddress,
+ value,
+ state: UsdtTransactionState.CONFIRMED,
+ blockHeight: 1000000 + index,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ };
});
}
+/**
+ * Insert fake USDT transactions into the store
+ */
+function insertFakeUsdtTransactions(txs = defineUsdtFakeTransactions()) {
+ const { addTransactions } = useUsdtTransactionsStore();
+ addTransactions(txs);
+}
+
+// #endregion
+
+// #region Flows
+
/**
* Observes the staking modal and prevents from validating the info and instead fakes the staking process.
*/
@@ -779,19 +928,14 @@ function replaceStakingFlow() {
retiredBalance: 0,
validator: validatorAddress,
});
- const { addTransactions } = useTransactionsStore();
- addTransactions([
- createFakeTransaction({
- value: amount,
- recipient: STAKING_CONTRACT_ADDRESS,
- sender: demoNimAddress,
- data: {
- type: 'add-stake',
- raw: '',
- staker: demoNimAddress,
- },
- }),
- ]);
+ const stakeTx: Partial = {
+ value: amount,
+ recipient: validatorAddress,
+ sender: demoNimAddress,
+ timestamp: Date.now(),
+ data: { type: 'add-stake', raw: '', staker: demoNimAddress },
+ };
+ insertFakeNimTransactions([stakeTx]);
});
});
@@ -805,35 +949,6 @@ function replaceStakingFlow() {
});
}
-/**
- * Creates a fake transaction. Each call increments a global counter for the hash and block heights.
- */
-let txCounter = 0;
-let currentHead = 0;
-function createFakeTransaction(tx: Partial) {
- return {
- network: 'mainnet',
- state: 'confirmed',
- transactionHash: `0x${(txCounter++).toString(16)}`,
- value: 50000000,
- recipient: '',
- fee: 0,
- feePerByte: 0,
- format: 'basic',
- sender: '',
- senderType: 'basic',
- recipientType: 'basic',
- validityStartHeight: currentHead++,
- blockHeight: currentHead++,
- flags: 0,
- timestamp: Date.now(),
- proof: { type: 'raw', raw: '' },
- size: 0,
- valid: true,
- ...tx,
- } as PlainTransactionDetails;
-}
-
/**
* Returns the hex encoding of a UTF-8 string.
*/
@@ -1051,86 +1166,6 @@ function interceptFetchRequest() {
return new Response(JSON.stringify(json));
}
- // if (isEstimateRequest) {
- // const body = args[1]?.body;
- // if (!body) throw new Error('[Demo] No body found in request');
- // type EstimateRequest = {
- // from: Record,
- // to: SwapAsset,
- // includedFees: 'required',
- // };
- // const { from: fromObj, to } = JSON.parse(body.toString()) as EstimateRequest;
- // const from = Object.keys(fromObj)[0] as SwapAsset;
-
- // // Validate the request
- // if (!from || !to) {
- // console.error('[Demo] Invalid request parameters:', { from, to });
- // return new Response(JSON.stringify({
- // error: 'Invalid request parameters',
- // status: 400,
- // }), { status: 400 });
- // }
-
- // // Calculate fees based on the value
-
- // const value = fromObj[from];
- // const networkFee = value * 0.0005; // 0.05% network fee
- // // const escrowFee = value * 0.001; // 0.1% escrow fee
-
- // // Calculate the destination amount using our mock rates
- // const estimatedAmount = calculateEstimatedAmount(from, to, value);
-
- // // Create mock estimate response with a valid structure
- // const estimate: FastspotEstimate[] = [{
- // from: [{
- // amount: `${value}`,
- // name: from,
- // symbol: from,
- // finalizeNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(from)}`,
- // },
- // fundingNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(from)}`,
- // },
- // operatingNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(from)}`,
- // },
- // }],
- // to: [{
- // amount: `${estimatedAmount}`,
- // name: to,
- // symbol: to,
- // finalizeNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(to)}`,
- // },
- // fundingNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(to)}`,
- // },
- // operatingNetworkFee: {
- // total: `${networkFee}`,
- // totalIsIncluded: false,
- // perUnit: `${getNetworkFeePerUnit(to)}`,
- // },
- // }],
- // direction: 'forward',
- // serviceFeePercentage: 0.01,
- // }];
-
- // console.log('[Demo] Mock estimate:', estimate);
-
- // return new Response(JSON.stringify(estimate));
- // }
-
if (isSwapRequest) {
const swapId = url.pathname.split('/').slice(-1)[0];
@@ -1232,7 +1267,7 @@ function interceptFetchRequest() {
direction: 'send',
status: 'pending',
id: '2MzQo4ehDrSEsxX7RnysLL6VePD3tuNyx4M',
- intermediary: { },
+ intermediary: {},
},
{
asset: swap.redeem.type,
@@ -1247,7 +1282,7 @@ function interceptFetchRequest() {
direction: 'receive',
status: 'pending',
id: 'eff8a1a5-4f4e-3895-b95c-fd5a40c99001',
- intermediary: { },
+ intermediary: {},
},
],
}));
@@ -1275,45 +1310,11 @@ function getNetworkFeePerUnit(asset: string): number {
}
}
-/**
- * Calculate mock estimated amount for swaps based on predefined rates
- */
-function calculateEstimatedAmount(fromAsset: string, toAsset: string, value: number): number {
- // Define mock exchange rates (realistic market rates)
- const rates: Record = {
- [`${SwapAsset.NIM}-${SwapAsset.BTC}`]: 0.00000004, // 1 NIM = 0.00000004 BTC
- [`${SwapAsset.NIM}-${SwapAsset.USDC_MATIC}`]: 0.0012, // 1 NIM = 0.0012 USDC
- [`${SwapAsset.NIM}-${SwapAsset.USDT_MATIC}`]: 0.0012, // 1 NIM = 0.0012 USDT
- [`${SwapAsset.BTC}-${SwapAsset.NIM}`]: 25000000, // 1 BTC = 25,000,000 NIM
- [`${SwapAsset.BTC}-${SwapAsset.USDC_MATIC}`]: 30000, // 1 BTC = 30,000 USDC
- [`${SwapAsset.BTC}-${SwapAsset.USDT_MATIC}`]: 30000, // 1 BTC = 30,000 USDT
- [`${SwapAsset.USDC_MATIC}-${SwapAsset.NIM}`]: 833.33, // 1 USDC = 833.33 NIM
- [`${SwapAsset.USDC_MATIC}-${SwapAsset.BTC}`]: 0.000033, // 1 USDC = 0.000033 BTC
- [`${SwapAsset.USDC_MATIC}-${SwapAsset.USDT_MATIC}`]: 1, // 1 USDC = 1 USDT
- [`${SwapAsset.USDT_MATIC}-${SwapAsset.NIM}`]: 833.33, // 1 USDT = 833.33 NIM
- [`${SwapAsset.USDT_MATIC}-${SwapAsset.BTC}`]: 0.000033, // 1 USDT = 0.000033 BTC
- [`${SwapAsset.USDT_MATIC}-${SwapAsset.USDC_MATIC}`]: 1, // 1 USDT = 1 USDT
- };
-
- const rate = rates[`${fromAsset}-${toAsset}`];
- if (!rate) {
- // If no direct rate, check reverse rate
- const reverseRate = rates[`${toAsset}-${fromAsset}`];
- if (reverseRate) {
- return Math.floor(value * (1 / reverseRate));
- }
- return value; // 1:1 if no rate defined
- }
-
- return Math.floor(value * rate);
-}
-
/**
* Ensures the Send button in modals is always enabled in demo mode, regardless of network state.
* This allows users to interact with the send functionality without waiting for network sync.
*/
function enableSendModalInDemoMode() {
- const observing = false;
const observer = new MutationObserver(() => {
// Target the send modal footer button
const sendButton = document.querySelector('.send-modal-footer .nq-button');
@@ -1342,6 +1343,165 @@ function enableSendModalInDemoMode() {
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled'] });
}
+let swapInterval: NodeJS.Timeout | null = null;
+function listenForSwapChanges() {
+ if (swapInterval) return;
+ swapInterval = setInterval(() => {
+ // Check if there are any active swaps that need to be processed
+ const { activeSwap } = useSwapsStore();
+ if (activeSwap && activeSwap.value?.state === SwapState.COMPLETE) {
+ completeSwap(activeSwap);
+ }
+ }, 2_000);
+}
+
+/**
+ * Completes an active swap by creating transactions for both sides of the swap
+ */
+function completeSwap(activeSwap: any) {
+ const { setActiveSwap } = useSwapsStore();
+ const startTime = Date.now();
+
+ // Create a timeout to simulate processing time
+ setTimeout(() => {
+ // Update the swap state to SUCCESS
+ setActiveSwap({
+ ...activeSwap,
+ state: SwapState.COMPLETE,
+ stateEnteredAt: startTime,
+ });
+
+ // Add transactions for both sides of the swap
+ const fromAsset = activeSwap.from.asset;
+ const toAsset = activeSwap.to.asset;
+ const fromAmount = activeSwap.from.amount;
+ const toAmount = activeSwap.to.amount;
+ const fromFee = activeSwap.from.fee || 0;
+ const toFee = activeSwap.to.fee || 0;
+
+ // Create outgoing transaction (from asset)
+ switch (fromAsset) {
+ case 'NIM': {
+ const tx: Partial = {
+ value: -fromAmount,
+ recipient: 'HTLC-ADDRESS',
+ sender: demoNimAddress,
+ timestamp: Date.now(),
+ data: {
+ type: 'htlc',
+ hashAlgorithm: '',
+ hashCount: 0,
+ hashRoot: '',
+ raw: '',
+ recipient: 'HTLC-ADDRESS',
+ sender: demoNimAddress,
+ timeout: 0,
+ },
+ }
+ insertFakeNimTransactions(transformNimTransaction([tx]));
+ break;
+ }
+ case 'BTC': {
+ const tx: BtcTransactionDefinition = {
+ address: 'HTLC-ADDRESS',
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: -fromAmount / btcInitialBalance,
+ incoming: false,
+ recipientLabel: 'HTLC-ADDRESS',
+ }
+ insertFakeBtcTransactions(transformBtcTransaction([tx]));
+ break;
+ }
+ case 'USDC_MATIC': {
+ const tx: UsdcTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: false,
+ recipientLabel: demoPolygonAddress,
+ };
+
+ insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ break;
+ }
+ case 'USDT_MATIC': {
+ const tx: UsdtTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: false,
+ recipientLabel: demoPolygonAddress,
+ };
+
+ insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ break;
+ }
+ }
+
+ // Create incoming transaction (to asset)
+ switch (toAsset) {
+ case 'NIM': {
+ const tx: Partial = {
+ value: toAmount,
+ recipient: demoNimAddress,
+ sender: 'HTLC-ADDRESS',
+ timestamp: Date.now(),
+ data: {
+ type: 'htlc',
+ hashAlgorithm: '',
+ hashCount: 0,
+ hashRoot: '',
+ raw: '',
+ recipient: demoNimAddress,
+ sender: 'HTLC-ADDRESS',
+ timeout: 0,
+ },
+ }
+ insertFakeNimTransactions(transformNimTransaction([tx]));
+ break;
+ }
+ case 'BTC': {
+ const tx: BtcTransactionDefinition = {
+ address: demoBtcAddress,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: toAmount / btcInitialBalance,
+ incoming: true,
+ recipientLabel: demoBtcAddress,
+ }
+ insertFakeBtcTransactions(transformBtcTransaction([tx]));
+ break;
+ }
+ case 'USDC_MATIC': {
+ const tx: UsdcTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: true,
+ recipientLabel: demoPolygonAddress,
+ };
+ insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ break;
+ }
+ case 'USDT_MATIC': {
+ const tx: UsdtTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: true,
+ recipientLabel: demoPolygonAddress,
+ };
+
+ insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ break;
+ }
+ }
+ // eslint-disable-next-line no-console
+ console.log('Demo swap completed:', { fromAsset, toAsset, fromAmount, toAmount });
+ }, 3000); // Simulate a delay for the swap to complete
+}
+
const ignoreHubRequests = [
'addBtcAddresses',
'on',
@@ -1415,20 +1575,13 @@ export class DemoHubApi extends HubApi {
if (ignoreHubRequests.includes(requestName)) {
return;
}
- // Find the setupSwap handler in the DemoHubApi class and replace it with this:
if (requestName === 'setupSwap') {
- console.log('[DEMO]', {
- firstArg,
- args,
- promise: await firstArg,
- });
const swap = await firstArg as SetupSwapArgs;
const signerTransaction: SetupSwapResult = {
nim: {
transaction: new Uint8Array(),
serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
- // serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925200000000000000000000000000000000000000000000000000000000000000000002003b571b3481bec4340ed715f6b59add1947667f2d51a70d05cc7b5778dae6e009a5342d2e62977c877342b1972ebe064e77c473603ac9a6b97ac1bdd0fa543f37487fab37256fad87d79314eee2dff3fc70623057fe17f07aa39f9cb9f99db0a',
hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
raw: {
signerPublicKey: new Uint8Array(),
@@ -1457,527 +1610,8 @@ export class DemoHubApi extends HubApi {
onGoingSwaps.set(swap.swapId, swap);
resolve(signerTransaction);
+ return
}
- // if (requestName === 'setupSwap') {
- // (firstArg as Function)();
- // // Get swap amount and asset details from the DOM
- // const leftColumn = document.querySelector('.swap-amounts .left-column');
- // const leftAmountElement = leftColumn?.querySelector('.width-value');
- // const rightColumn = document.querySelector('.swap-amounts .right-column');
- // const rightAmountElement = rightColumn?.querySelector('.width-value');
-
- // if (!leftAmountElement || !rightAmountElement || !leftColumn || !rightColumn) {
- // console.warn('[Demo] Could not find swap amount elements');
- // return {};
- // }
-
- // const leftValue = Number((leftAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
- // const leftAsset = leftColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
-
- // const rightValue = Number((rightAmountElement as HTMLDivElement).innerText.replace(/,/g, ''));
- // const rightAsset = rightColumn.querySelector('.ticker')?.innerHTML.toUpperCase().trim() as SwapAsset;
-
- // console.log(`[Demo] Setting up swap: ${leftValue} ${leftAsset} -> ${rightValue} ${rightAsset}`);
-
- // // Check if we have valid values
- // if (!leftValue || !rightValue || !leftAsset || !rightAsset) {
- // console.warn('[Demo] Missing swap values');
- // return {};
- // }
-
- // const direction = leftValue < rightValue ? 'forward' : 'reverse';
- // const fromAsset = direction === 'forward' ? leftAsset : rightAsset;
- // // @ts-expect-error Object key not specific enough?
- // const toAsset: RequestAsset = direction === 'forward'
- // ? { [rightAsset]: rightValue }
- // : { [leftAsset]: leftValue };
-
- // const swapSuggestion = await createSwap(fromAsset, toAsset);
-
- // const { config } = useConfig();
-
- // let fund: HtlcCreationInstructions | null = null;
- // let redeem: HtlcSettlementInstructions | null = null;
-
- // const { activeAddressInfo } = useAddressStore();
-
- // const { availableExternalAddresses } = useBtcAddressStore();
- // const nimAddress = activeAddressInfo.value!.address;
- // const btcAddress = availableExternalAddresses.value[0];
-
- // if (swapSuggestion.from.asset === SwapAsset.NIM) {
- // const nimiqClient = await getNetworkClient();
- // await nimiqClient.waitForConsensusEstablished();
- // const headHeight = await nimiqClient.getHeadHeight();
- // if (headHeight > 100) {
- // useNetworkStore().state.height = headHeight;
- // } else {
- // throw new Error('Invalid network state, try please reloading the app');
- // }
-
- // fund = {
- // type: SwapAsset.NIM,
- // sender: nimAddress,
- // value: swapSuggestion.from.amount,
- // fee: swapSuggestion.from.fee,
- // validityStartHeight: useNetworkStore().state.height,
- // };
- // }
-
- // const { accountUtxos, accountBalance: accountBtcBalance, } = useBtcAddressStore();
- // if (swapSuggestion.from.asset === SwapAsset.BTC) {
- // const electrumClient = await getElectrumClient();
- // await electrumClient.waitForConsensusEstablished();
-
- // // Assemble BTC inputs
- // // Transactions to an HTLC are 46 weight units bigger because of the longer recipient address
- // const requiredInputs = selectOutputs(
- // accountUtxos.value, swapSuggestion.from.amount, swapSuggestion.from.feePerUnit, 48);
- // let changeAddress: string;
- // if (requiredInputs.changeAmount > 0) {
- // const { nextChangeAddress } = useBtcAddressStore();
- // if (!nextChangeAddress.value) {
- // // FIXME: If no unused change address is found, need to request new ones from Hub!
- // throw new Error('No more unused change addresses (not yet implemented)');
- // }
- // changeAddress = nextChangeAddress.value;
- // }
-
- // fund = {
- // type: SwapAsset.BTC,
- // inputs: requiredInputs.utxos.map((utxo) => ({
- // address: utxo.address,
- // transactionHash: utxo.transactionHash,
- // outputIndex: utxo.index,
- // outputScript: utxo.witness.script,
- // value: utxo.witness.value,
- // })),
- // output: {
- // value: swapSuggestion.from.amount,
- // },
- // ...(requiredInputs.changeAmount > 0 ? {
- // changeOutput: {
- // address: changeAddress!,
- // value: requiredInputs.changeAmount,
- // },
- // } : {}),
- // refundAddress: btcAddress,
- // };
- // }
-
- // const {
- // activeAddress: activePolygonAddress,
- // accountUsdcBalance,
- // accountUsdtBridgedBalance,
- // } = usePolygonAddressStore();
-
- // if (swapSuggestion.from.asset === SwapAsset.USDC_MATIC) {
- // const [client, htlcContract] = await Promise.all([
- // getPolygonClient(),
- // getUsdcHtlcContract(),
- // ]);
- // const fromAddress = activePolygonAddress.value!;
-
- // const [
- // usdcNonce,
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // client.usdcToken.nonces(fromAddress) as Promise,
- // htlcContract.getNonce(fromAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'open' && method !== 'openWithPermit') {
- // throw new Error('Wrong USDC contract method');
- // }
-
- // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
- // // Keyguard's SwapIFrameApi.
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address token */ config.polygon.usdc.tokenContract,
- // /* uint256 amount */ swapSuggestion.from.amount,
- // /* address refundAddress */ fromAddress,
- // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
- // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint256 timeout */ 0,
- // /* uint256 fee */ fee,
- // ...(method === 'openWithPermit' ? [
- // // // Approve the maximum possible amount so afterwards we can use the `open` method for
- // // // lower fees
- // // /* uint256 value */ client.ethers
- // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
- // /* uint256 value */ swapSuggestion.from.amount + fee,
-
- // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint8 sigV */ 0,
- // ] : []),
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: fromAddress,
- // to: config.polygon.usdc.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdc.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdc.htlcContract,
- // },
- // };
-
- // fund = {
- // type: SwapAsset.USDC_MATIC,
- // ...relayRequest,
- // ...(method === 'openWithPermit' ? {
- // permit: {
- // tokenNonce: usdcNonce.toNumber(),
- // },
- // } : null),
- // };
- // }
-
- // if (swapSuggestion.from.asset === SwapAsset.USDT_MATIC) {
- // const [client, htlcContract] = await Promise.all([
- // getPolygonClient(),
- // getUsdtBridgedHtlcContract(),
- // ]);
- // const fromAddress = activePolygonAddress.value!;
-
- // const [
- // usdtNonce,
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // client.usdtBridgedToken.getNonce(fromAddress) as Promise,
- // htlcContract.getNonce(fromAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'open' && method !== 'openWithApproval') {
- // throw new Error('Wrong USDT contract method');
- // }
-
- // // Zeroed data fields are replaced by Fastspot's proposed data (passed in from Hub) in
- // // Keyguard's SwapIFrameApi.
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address token */ config.polygon.usdt_bridged.tokenContract,
- // /* uint256 amount */ swapSuggestion.from.amount,
- // /* address refundAddress */ fromAddress,
- // /* address recipientAddress */ '0x0000000000000000000000000000000000000000',
- // /* bytes32 hash */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint256 timeout */ 0,
- // /* uint256 fee */ fee,
- // ...(method === 'openWithApproval' ? [
- // // // Approve the maximum possible amount so afterwards we can use the `open` method for
- // // // lower fees
- // // /* uint256 approval */ client.ethers
- // // .BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
- // /* uint256 approval */ swapSuggestion.from.amount + fee,
-
- // /* bytes32 sigR */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* bytes32 sigS */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* uint8 sigV */ 0,
- // ] : []),
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: fromAddress,
- // to: config.polygon.usdt_bridged.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdt_bridged.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdt_bridged.htlcContract,
- // },
- // };
-
- // fund = {
- // type: SwapAsset.USDT_MATIC,
- // ...relayRequest,
- // ...(method === 'openWithApproval' ? {
- // approval: {
- // tokenNonce: usdtNonce.toNumber(),
- // },
- // } : null),
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.NIM) {
- // const nimiqClient = await getNetworkClient();
- // await nimiqClient.waitForConsensusEstablished();
- // const headHeight = await nimiqClient.getHeadHeight();
- // if (headHeight > 100) {
- // useNetworkStore().state.height = headHeight;
- // } else {
- // throw new Error('Invalid network state, try please reloading the app');
- // }
-
- // redeem = {
- // type: SwapAsset.NIM,
- // recipient: nimAddress, // My address, must be redeem address of HTLC
- // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Luna
- // fee: swapSuggestion.to.fee, // Luna
- // validityStartHeight: useNetworkStore().state.height,
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.BTC) {
- // const electrumClient = await getElectrumClient();
- // await electrumClient.waitForConsensusEstablished();
-
- // redeem = {
- // type: SwapAsset.BTC,
- // input: {
- // // transactionHash: transaction.transactionHash,
- // // outputIndex: output.index,
- // // outputScript: output.script,
- // value: swapSuggestion.to.amount, // Sats
- // },
- // output: {
- // address: btcAddress, // My address, must be redeem address of HTLC
- // value: swapSuggestion.to.amount - swapSuggestion.to.fee, // Sats
- // },
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.USDC_MATIC) {
- // const htlcContract = await getUsdcHtlcContract();
- // const toAddress = activePolygonAddress.value!;
-
- // const [
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // htlcContract.getNonce(toAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'redeemWithSecretInData') {
- // throw new Error('Wrong USDC contract method');
- // }
-
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address target */ toAddress,
- // /* uint256 fee */ fee,
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: toAddress,
- // to: config.polygon.usdc.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdc.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdc.htlcContract,
- // },
- // };
-
- // redeem = {
- // type: SwapAsset.USDC_MATIC,
- // ...relayRequest,
- // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
- // };
- // }
-
- // if (swapSuggestion.to.asset === SwapAsset.USDT_MATIC) {
- // const htlcContract = await getUsdtBridgedHtlcContract();
- // const toAddress = activePolygonAddress.value!;
-
- // const [
- // forwarderNonce,
- // blockHeight,
- // ] = await Promise.all([
- // htlcContract.getNonce(toAddress) as Promise,
- // getPolygonBlockNumber(),
- // ]);
-
- // const { fee, gasLimit, gasPrice, relay, method } = {
- // fee: 1,
- // gasLimit: 1,
- // gasPrice: 1,
- // relay: {
- // pctRelayFee: 1,
- // baseRelayFee: 1,
- // relayWorkerAddress: '0x0000000000111111111122222222223333333333',
- // },
- // method: 'open'
- // };
- // if (method !== 'redeemWithSecretInData') {
- // throw new Error('Wrong USDT contract method');
- // }
-
- // const data = htlcContract.interface.encodeFunctionData(method, [
- // /* bytes32 id */ '0x0000000000000000000000000000000000000000000000000000000000000000',
- // /* address target */ toAddress,
- // /* uint256 fee */ fee,
- // ]);
-
- // const relayRequest: RelayRequest = {
- // request: {
- // from: toAddress,
- // to: config.polygon.usdt_bridged.htlcContract,
- // data,
- // value: '0',
- // nonce: forwarderNonce.toString(),
- // gas: gasLimit.toString(),
- // validUntil: (blockHeight + 3000 + 3 * 60 * POLYGON_BLOCKS_PER_MINUTE)
- // .toString(10), // 3 hours + 3000 blocks (minimum relay expectancy)
- // },
- // relayData: {
- // gasPrice: gasPrice.toString(),
- // pctRelayFee: relay.pctRelayFee.toString(),
- // baseRelayFee: relay.baseRelayFee.toString(),
- // relayWorker: relay.relayWorkerAddress,
- // paymaster: config.polygon.usdt_bridged.htlcContract,
- // paymasterData: '0x',
- // clientId: Math.floor(Math.random() * 1e6).toString(10),
- // forwarder: config.polygon.usdt_bridged.htlcContract,
- // },
- // };
-
- // redeem = {
- // type: SwapAsset.USDT_MATIC,
- // ...relayRequest,
- // amount: swapSuggestion.to.amount - swapSuggestion.to.fee,
- // };
- // }
-
- // if (!fund || !redeem) {
- // reject(new Error('UNEXPECTED: No funding or redeeming data objects'));
- // return;
- // }
-
- // const serviceSwapFee = Math.round(
- // (swapSuggestion.from.amount - swapSuggestion.from.serviceNetworkFee)
- // * swapSuggestion.serviceFeePercentage,
- // );
-
- // const { addressInfos } = useAddressStore();
-
- // const { activeAccountInfo } = useAccountStore();
-
- // const { currency, exchangeRates } = useFiatStore();
-
- // const {
- // activeAddress
-
- // } = usePolygonAddressStore();
-
- // const request: Omit = {
- // accountId: activeAccountInfo.value!.id,
- // swapId: swapSuggestion.id,
- // fund,
- // redeem,
-
- // layout: 'slider',
- // direction: direction === 'forward' ? 'left-to-right' : 'right-to-left',
- // fiatCurrency: currency.value,
- // fundingFiatRate: exchangeRates.value[assetToCurrency(
- // fund.type as SupportedSwapAsset,
- // )][currency.value]!,
- // redeemingFiatRate: exchangeRates.value[assetToCurrency(
- // redeem.type as SupportedSwapAsset,
- // )][currency.value]!,
- // fundFees: {
- // processing: 0,
- // redeeming: swapSuggestion.from.serviceNetworkFee,
- // },
- // redeemFees: {
- // funding: swapSuggestion.to.serviceNetworkFee,
- // processing: 0,
- // },
- // serviceSwapFee,
- // nimiqAddresses: addressInfos.value.map((addressInfo) => ({
- // address: addressInfo.address,
- // balance: addressInfo.balance || 0,
- // })),
- // bitcoinAccount: {
- // balance: accountBtcBalance.value,
- // },
- // polygonAddresses: activePolygonAddress.value ? [{
- // address: activePolygonAddress.value,
- // usdcBalance: accountUsdcBalance.value,
- // usdtBalance: accountUsdtBridgedBalance.value,
- // }] : [],
- // };
-
- // console.log('[Demo] Faking swap setup with request:', request);
- // resolve(request)
- // return;
- // }
// Wait for router readiness
await new Promise((resolve) => {
@@ -1994,3 +1628,4 @@ export class DemoHubApi extends HubApi {
});
}
}
+
From 93460052464e0d8902e33aef4dcd5ae7ef28ba0c Mon Sep 17 00:00:00 2001
From: onmax
Date: Mon, 10 Mar 2025 15:40:22 +0100
Subject: [PATCH 06/31] chore(demo): linter
---
src/stores/Demo.ts | 158 ++++++++++++++++++++++-----------------------
1 file changed, 79 insertions(+), 79 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index c6a6f417c..b779826c7 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -1,9 +1,9 @@
-/* eslint-disable max-len */
+/* eslint-disable max-len, consistent-return, no-console, no-async-promise-executor */
import { createStore } from 'pinia';
import VueRouter from 'vue-router';
import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
-import { Address, KeyPair, PlainTransactionDetails, PlainTransactionRecipientData, PrivateKey, TransactionFormat } from '@nimiq/core';
+import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
import { AccountType, useAccountStore } from '@/stores/Account';
import { AddressType, useAddressStore } from '@/stores/Address';
import { toSecs, type Transaction as NimTransaction, useTransactionsStore } from '@/stores/Transactions';
@@ -14,8 +14,9 @@ import { useStakingStore } from '@/stores/Staking';
import { useAccountSettingsStore } from '@/stores/AccountSettings';
import { usePolygonAddressStore } from '@/stores/PolygonAddress';
import Config from 'config';
-import { AssetList, FastspotAsset, FastspotEstimate, FastspotFee, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
+import { FastspotAsset, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
import HubApi, { SetupSwapResult } from '@nimiq/hub-api';
+import { useConfig } from '@/composables/useConfig';
import { useBtcAddressStore } from './BtcAddress';
import { useContactsStore } from './Contacts';
import { useBtcLabelsStore } from './BtcLabels';
@@ -23,8 +24,6 @@ import { useUsdcContactsStore } from './UsdcContacts';
import { useUsdtContactsStore } from './UsdtContacts';
import { useFiatStore } from './Fiat';
import { SwapState, useSwapsStore } from './Swaps';
-import { getUsdcHtlcContract } from '@/ethers';
-import { useConfig } from '@/composables/useConfig';
export type DemoState = {
active: boolean,
@@ -87,7 +86,6 @@ export const useDemoStore = createStore({
* Initializes the demo environment and sets up various routes, data, and watchers.
*/
async initialize(router: VueRouter) {
- // eslint-disable-next-line no-console
console.warn('[Demo] Initializing demo environment...');
demoRouter = router;
@@ -454,7 +452,7 @@ function transformNimTransaction(txs: Partial[]): NimTransaction
proof: { raw: '', type: 'raw' },
data: tx.data || { type: 'raw', raw: '' },
...tx,
- }
+ };
});
}
@@ -474,7 +472,7 @@ interface BtcTransactionDefinition {
description: string;
recipientLabel?: string;
incoming: boolean;
- address: string
+ address: string;
}
/**
@@ -489,7 +487,7 @@ function defineBtcFakeTransactions(): BtcTransaction[] {
description: 'Initial BTC purchase from exchange',
incoming: true,
address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
- recipientLabel: 'Satoshi Exchange'
+ recipientLabel: 'Satoshi Exchange',
},
{
fraction: 0.15,
@@ -497,7 +495,7 @@ function defineBtcFakeTransactions(): BtcTransaction[] {
description: 'Mining pool payout',
incoming: true,
address: '1Hz7vQrRjnu3z9k7gxDYhKjEmABqChDvJr',
- recipientLabel: 'Genesis Mining Pool'
+ recipientLabel: 'Genesis Mining Pool',
},
{
fraction: -0.19,
@@ -505,7 +503,7 @@ function defineBtcFakeTransactions(): BtcTransaction[] {
description: 'Purchase from online marketplace',
incoming: false,
address: '1LxKe5kKdgGVwXukEgqFxh6DrCXF2Pturc',
- recipientLabel: 'Digital Bazaar Shop'
+ recipientLabel: 'Digital Bazaar Shop',
},
{
fraction: 0.3,
@@ -513,28 +511,28 @@ function defineBtcFakeTransactions(): BtcTransaction[] {
description: 'Company payment for consulting',
incoming: true,
address: '1N7aecJuKGDXzYK8CgpnNRYxdhZvXPxp3B',
- recipientLabel: 'Corporate Treasury'
+ recipientLabel: 'Corporate Treasury',
},
{
fraction: -0.15,
daysAgo: 365,
description: 'Auto-DCA investment program',
incoming: false,
- address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz'
+ address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz',
},
{
fraction: 0.075,
daysAgo: 180,
description: 'P2P sale of digital goods',
incoming: true,
- address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS'
+ address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS',
},
{
fraction: 0.05,
daysAgo: 60,
description: 'Recent purchase from exchange',
incoming: true,
- address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg'
+ address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
},
].sort((a, b) => b.daysAgo - a.daysAgo);
@@ -689,7 +687,6 @@ function insertFakeBtcTransactions(txs = defineBtcFakeTransactions()) {
addTransactions(txs);
}
-
// #endregion
// #region Polygon txs
@@ -1075,7 +1072,6 @@ function interceptFetchRequest() {
const url = new URL(args[0] as string);
const isFastspotRequest = url.host === (new URL(Config.fastspot.apiEndpoint).host);
const isLimitsRequest = url.pathname.includes('/limits');
- const isEstimateRequest = url.pathname.includes('/estimate');
const isAssetsRequest = url.pathname.includes('/assets');
const isSwapRequest = url.pathname.includes('/swaps');
@@ -1106,6 +1102,7 @@ function interceptFetchRequest() {
return new Response(JSON.stringify(limits));
}
+ // eslint-disable-next-line no-promise-executor-return
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
await sleep(1000 + Math.random() * 500);
@@ -1376,8 +1373,8 @@ function completeSwap(activeSwap: any) {
const toAsset = activeSwap.to.asset;
const fromAmount = activeSwap.from.amount;
const toAmount = activeSwap.to.amount;
- const fromFee = activeSwap.from.fee || 0;
- const toFee = activeSwap.to.fee || 0;
+ // const fromFee = activeSwap.from.fee || 0;
+ // const toFee = activeSwap.to.fee || 0;
// Create outgoing transaction (from asset)
switch (fromAsset) {
@@ -1397,7 +1394,7 @@ function completeSwap(activeSwap: any) {
sender: demoNimAddress,
timeout: 0,
},
- }
+ };
insertFakeNimTransactions(transformNimTransaction([tx]));
break;
}
@@ -1409,7 +1406,7 @@ function completeSwap(activeSwap: any) {
fraction: -fromAmount / btcInitialBalance,
incoming: false,
recipientLabel: 'HTLC-ADDRESS',
- }
+ };
insertFakeBtcTransactions(transformBtcTransaction([tx]));
break;
}
@@ -1437,6 +1434,9 @@ function completeSwap(activeSwap: any) {
insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
break;
}
+ default: {
+ console.warn(`Unsupported asset type: ${fromAsset}`);
+ }
}
// Create incoming transaction (to asset)
@@ -1457,7 +1457,7 @@ function completeSwap(activeSwap: any) {
sender: 'HTLC-ADDRESS',
timeout: 0,
},
- }
+ };
insertFakeNimTransactions(transformNimTransaction([tx]));
break;
}
@@ -1469,7 +1469,7 @@ function completeSwap(activeSwap: any) {
fraction: toAmount / btcInitialBalance,
incoming: true,
recipientLabel: demoBtcAddress,
- }
+ };
insertFakeBtcTransactions(transformBtcTransaction([tx]));
break;
}
@@ -1496,8 +1496,10 @@ function completeSwap(activeSwap: any) {
insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
break;
}
+ default: {
+ console.warn(`Unsupported asset type: ${toAsset}`);
+ }
}
- // eslint-disable-next-line no-console
console.log('Demo swap completed:', { fromAsset, toAsset, fromAmount, toAmount });
}, 3000); // Simulate a delay for the swap to complete
}
@@ -1565,67 +1567,65 @@ export class DemoHubApi extends HubApi {
const instance = new DemoHubApi();
return new Proxy(instance, {
get(target, prop: keyof HubApi) {
- if (typeof target[prop] === 'function') {
- return async (...args: Parameters) => new Promise(async (resolve, reject) => {
- const requestName = String(prop);
- const [firstArg] = args;
- // eslint-disable-next-line no-console
- console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
-
- if (ignoreHubRequests.includes(requestName)) {
- return;
- }
-
- if (requestName === 'setupSwap') {
- const swap = await firstArg as SetupSwapArgs;
- const signerTransaction: SetupSwapResult = {
- nim: {
- transaction: new Uint8Array(),
- serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
- hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
- raw: {
- signerPublicKey: new Uint8Array(),
- signature: new Uint8Array(),
- sender: 'NQ86 D3M0 SW4P NB59 U3F8 NDLX CE0P AFMX Y52S',
- senderType: 2,
- recipient: swap.redeem.recipient,
- recipientType: 0,
- value: swap.redeem.value,
- fee: 0,
- validityStartHeight: swap.redeem.validityStartHeight,
- extraData: new Uint8Array(),
- flags: 0,
- networkId: 5,
- proof: new Uint8Array(),
- },
- },
- btc: {
- serializedTx: '0200000000010168c8952af998f2c68412a848a72d1f9b0b7ff27417df1cb85514c97474b51ba40000000000ffffffff026515000000000000220020bf0ffdd2ffb9a579973455cfe9b56515538b79361d5ae8a4d255dea2519ef77864c501000000000016001428257447efe2d254ce850ea2760274d233d86e5c024730440220792fa932d9d0591e3c5eb03f47d05912a1e21f3e76d169e383af66e47896ac8c02205947df5523490e4138f2da0fc5c9da3039750fe43bd217b68d26730fdcae7fbe012102ef8d4b51d1a075e67d62baa78991d5fc36a658fec28d8b978826058168ed2a1a00000000',
- hash: '3090808993a796c26a614f5a4a36a48e0b4af6cd3e28e39f3f006e9a447da2b3',
- },
- refundTx: '02000000000101b3a27d449a6e003f9fe3283ecdf64a0b8ea4364a5a4f616ac296a793898090300000000000feffffff011e020000000000001600146d2146bb49f6d1de6b4f14e0a8074c79b887cef50447304402202a7dce2e39cf86ee1d7c1e9cc55f1e0fb26932fd22e5437e5e5804a9e5d220b1022031aa177ea085c10c4d54b2f5aa528aac0013b67f9ee674070aa2fb51894de80e0121025b4d40682bbcb5456a9d658971b725666a3cccaa2fb45d269d2f1486bf85b3c000636382012088a820be8719b9427f1551c4234f8b02d8f8aa055ae282b2e9eef6c155326ae951061f8876a914e546b01d8c9d9bf35f9f115132ce8eab7191a68d88ac67046716ca67b17576a9146d2146bb49f6d1de6b4f14e0a8074c79b887cef588ac686816ca67',
- };
+ if (typeof target[prop] !== 'function') {
+ return target[prop];
+ }
- // Add to onGoingSwaps map
- onGoingSwaps.set(swap.swapId, swap);
+ return async (...args: Parameters) => new Promise(async (resolveInterceptedAction) => {
+ const requestName = String(prop);
+ const [firstArg] = args;
+ console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
+
+ if (ignoreHubRequests.includes(requestName)) {
+ return;
+ }
+
+ if (requestName === 'setupSwap') {
+ const swap = await firstArg as SetupSwapArgs;
+ const signerTransaction: SetupSwapResult = {
+ nim: {
+ transaction: new Uint8Array(),
+ serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
+ hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
+ raw: {
+ signerPublicKey: new Uint8Array(),
+ signature: new Uint8Array(),
+ sender: 'NQ86 D3M0 SW4P NB59 U3F8 NDLX CE0P AFMX Y52S',
+ senderType: 2,
+ recipient: swap.redeem.recipient,
+ recipientType: 0,
+ value: swap.redeem.value,
+ fee: 0,
+ validityStartHeight: swap.redeem.validityStartHeight,
+ extraData: new Uint8Array(),
+ flags: 0,
+ networkId: 5,
+ proof: new Uint8Array(),
+ },
+ },
+ btc: {
+ serializedTx: '0200000000010168c8952af998f2c68412a848a72d1f9b0b7ff27417df1cb85514c97474b51ba40000000000ffffffff026515000000000000220020bf0ffdd2ffb9a579973455cfe9b56515538b79361d5ae8a4d255dea2519ef77864c501000000000016001428257447efe2d254ce850ea2760274d233d86e5c024730440220792fa932d9d0591e3c5eb03f47d05912a1e21f3e76d169e383af66e47896ac8c02205947df5523490e4138f2da0fc5c9da3039750fe43bd217b68d26730fdcae7fbe012102ef8d4b51d1a075e67d62baa78991d5fc36a658fec28d8b978826058168ed2a1a00000000',
+ hash: '3090808993a796c26a614f5a4a36a48e0b4af6cd3e28e39f3f006e9a447da2b3',
+ },
+ refundTx: '02000000000101b3a27d449a6e003f9fe3283ecdf64a0b8ea4364a5a4f616ac296a793898090300000000000feffffff011e020000000000001600146d2146bb49f6d1de6b4f14e0a8074c79b887cef50447304402202a7dce2e39cf86ee1d7c1e9cc55f1e0fb26932fd22e5437e5e5804a9e5d220b1022031aa177ea085c10c4d54b2f5aa528aac0013b67f9ee674070aa2fb51894de80e0121025b4d40682bbcb5456a9d658971b725666a3cccaa2fb45d269d2f1486bf85b3c000636382012088a820be8719b9427f1551c4234f8b02d8f8aa055ae282b2e9eef6c155326ae951061f8876a914e546b01d8c9d9bf35f9f115132ce8eab7191a68d88ac67046716ca67b17576a9146d2146bb49f6d1de6b4f14e0a8074c79b887cef588ac686816ca67',
+ };
- resolve(signerTransaction);
- return
- }
+ // Add to onGoingSwaps map
+ onGoingSwaps.set(swap.swapId, swap);
- // Wait for router readiness
- await new Promise((resolve) => {
- demoRouter.onReady(resolve);
- });
+ resolveInterceptedAction(signerTransaction);
+ return;
+ }
- // eslint-disable-next-line no-console
- console.log('[Demo] Redirecting to fallback modal');
- demoRouter.push(`/${DemoModal.Fallback}`);
+ // Wait for router readiness
+ await new Promise((resolve) => {
+ demoRouter.onReady(resolve);
});
- }
- return target[prop];
+
+ console.log('[Demo] Redirecting to fallback modal');
+ demoRouter.push(`/${DemoModal.Fallback}`);
+ });
},
});
}
}
-
From b62a75b3253a69bdfa3727e50016d5f4270bd126 Mon Sep 17 00:00:00 2001
From: Alberto Monterroso <14013679+Albermonte@users.noreply.github.com>
Date: Mon, 10 Mar 2025 11:34:36 -0400
Subject: [PATCH 07/31] fix(demo): swap not working after bug introduced in
refactor
---
src/stores/Demo.ts | 313 ++++++++++++++++++++++++---------------------
1 file changed, 169 insertions(+), 144 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index b779826c7..29f848b26 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -1102,10 +1102,6 @@ function interceptFetchRequest() {
return new Response(JSON.stringify(limits));
}
- // eslint-disable-next-line no-promise-executor-return
- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
- await sleep(1000 + Math.random() * 500);
-
const asset = assetOrLimit as SwapAsset;
const { exchangeRates, currency } = useFiatStore();
@@ -1345,163 +1341,192 @@ function listenForSwapChanges() {
if (swapInterval) return;
swapInterval = setInterval(() => {
// Check if there are any active swaps that need to be processed
- const { activeSwap } = useSwapsStore();
- if (activeSwap && activeSwap.value?.state === SwapState.COMPLETE) {
- completeSwap(activeSwap);
+ const swap = useSwapsStore().activeSwap.value;
+ if (!swap) return;
+ console.log('[Demo] Active swap:', { swap, state: swap.state });
+ switch (swap.state) {
+ case SwapState.AWAIT_INCOMING:
+ console.log('[Demo] Swap is in AWAIT_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.CREATE_OUTGOING,
+ });
+ break;
+ case SwapState.CREATE_OUTGOING:
+ console.log('[Demo] Swap is in CREATE_OUTGOING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_SECRET,
+ });
+ break;
+ case SwapState.AWAIT_SECRET:
+ console.log('[Demo] Swap is in AWAIT_SECRET state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.SETTLE_INCOMING,
+ });
+ break;
+ case SwapState.SETTLE_INCOMING:
+ console.log('[Demo] Swap is in SETTLE_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.COMPLETE,
+ });
+ completeSwap(swap);
+ break;
+ case SwapState.COMPLETE:
+ console.log('[Demo] Swap is in COMPLETE state');
+ if (swapInterval)clearInterval(swapInterval);
+ swapInterval = null;
+ break;
+ default:
+ console.log('[Demo] Swap is in unknown state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_INCOMING,
+ });
+ break;
}
- }, 2_000);
+ }, 15_000);
}
/**
* Completes an active swap by creating transactions for both sides of the swap
*/
function completeSwap(activeSwap: any) {
- const { setActiveSwap } = useSwapsStore();
- const startTime = Date.now();
-
- // Create a timeout to simulate processing time
- setTimeout(() => {
- // Update the swap state to SUCCESS
- setActiveSwap({
- ...activeSwap,
- state: SwapState.COMPLETE,
- stateEnteredAt: startTime,
- });
-
- // Add transactions for both sides of the swap
- const fromAsset = activeSwap.from.asset;
- const toAsset = activeSwap.to.asset;
- const fromAmount = activeSwap.from.amount;
- const toAmount = activeSwap.to.amount;
- // const fromFee = activeSwap.from.fee || 0;
- // const toFee = activeSwap.to.fee || 0;
-
- // Create outgoing transaction (from asset)
- switch (fromAsset) {
- case 'NIM': {
- const tx: Partial = {
- value: -fromAmount,
+ // Add transactions for both sides of the swap
+ const fromAsset = activeSwap.from.asset;
+ const toAsset = activeSwap.to.asset;
+ const fromAmount = activeSwap.from.amount;
+ const toAmount = activeSwap.to.amount;
+ // const fromFee = activeSwap.from.fee || 0;
+ // const toFee = activeSwap.to.fee || 0;
+
+ // Create outgoing transaction (from asset)
+ switch (fromAsset) {
+ case 'NIM': {
+ const tx: Partial = {
+ value: -fromAmount,
+ recipient: 'HTLC-ADDRESS',
+ sender: demoNimAddress,
+ timestamp: Date.now(),
+ data: {
+ type: 'htlc',
+ hashAlgorithm: '',
+ hashCount: 0,
+ hashRoot: '',
+ raw: '',
recipient: 'HTLC-ADDRESS',
sender: demoNimAddress,
- timestamp: Date.now(),
- data: {
- type: 'htlc',
- hashAlgorithm: '',
- hashCount: 0,
- hashRoot: '',
- raw: '',
- recipient: 'HTLC-ADDRESS',
- sender: demoNimAddress,
- timeout: 0,
- },
- };
- insertFakeNimTransactions(transformNimTransaction([tx]));
- break;
- }
- case 'BTC': {
- const tx: BtcTransactionDefinition = {
- address: 'HTLC-ADDRESS',
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- fraction: -fromAmount / btcInitialBalance,
- incoming: false,
- recipientLabel: 'HTLC-ADDRESS',
- };
- insertFakeBtcTransactions(transformBtcTransaction([tx]));
- break;
- }
- case 'USDC_MATIC': {
- const tx: UsdcTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: false,
- recipientLabel: demoPolygonAddress,
- };
+ timeout: 0,
+ },
+ };
+ insertFakeNimTransactions(transformNimTransaction([tx]));
+ break;
+ }
+ case 'BTC': {
+ const tx: BtcTransactionDefinition = {
+ address: 'HTLC-ADDRESS',
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: -fromAmount / btcInitialBalance,
+ incoming: false,
+ recipientLabel: 'HTLC-ADDRESS',
+ };
+ insertFakeBtcTransactions(transformBtcTransaction([tx]));
+ break;
+ }
+ case 'USDC_MATIC': {
+ const tx: UsdcTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: false,
+ recipientLabel: demoPolygonAddress,
+ };
- insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
- break;
- }
- case 'USDT_MATIC': {
- const tx: UsdtTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: false,
- recipientLabel: demoPolygonAddress,
- };
+ insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ break;
+ }
+ case 'USDT_MATIC': {
+ const tx: UsdtTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: false,
+ recipientLabel: demoPolygonAddress,
+ };
- insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
- break;
- }
- default: {
- console.warn(`Unsupported asset type: ${fromAsset}`);
- }
+ insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ break;
}
+ default: {
+ console.warn(`Unsupported asset type: ${fromAsset}`);
+ }
+ }
- // Create incoming transaction (to asset)
- switch (toAsset) {
- case 'NIM': {
- const tx: Partial = {
- value: toAmount,
+ // Create incoming transaction (to asset)
+ switch (toAsset) {
+ case 'NIM': {
+ const tx: Partial = {
+ value: toAmount,
+ recipient: demoNimAddress,
+ sender: 'HTLC-ADDRESS',
+ timestamp: Date.now(),
+ data: {
+ type: 'htlc',
+ hashAlgorithm: '',
+ hashCount: 0,
+ hashRoot: '',
+ raw: '',
recipient: demoNimAddress,
sender: 'HTLC-ADDRESS',
- timestamp: Date.now(),
- data: {
- type: 'htlc',
- hashAlgorithm: '',
- hashCount: 0,
- hashRoot: '',
- raw: '',
- recipient: demoNimAddress,
- sender: 'HTLC-ADDRESS',
- timeout: 0,
- },
- };
- insertFakeNimTransactions(transformNimTransaction([tx]));
- break;
- }
- case 'BTC': {
- const tx: BtcTransactionDefinition = {
- address: demoBtcAddress,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- fraction: toAmount / btcInitialBalance,
- incoming: true,
- recipientLabel: demoBtcAddress,
- };
- insertFakeBtcTransactions(transformBtcTransaction([tx]));
- break;
- }
- case 'USDC_MATIC': {
- const tx: UsdcTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: true,
- recipientLabel: demoPolygonAddress,
- };
- insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
- break;
- }
- case 'USDT_MATIC': {
- const tx: UsdtTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: true,
- recipientLabel: demoPolygonAddress,
- };
+ timeout: 0,
+ },
+ };
+ insertFakeNimTransactions(transformNimTransaction([tx]));
+ break;
+ }
+ case 'BTC': {
+ const tx: BtcTransactionDefinition = {
+ address: demoBtcAddress,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: toAmount / btcInitialBalance,
+ incoming: true,
+ recipientLabel: demoBtcAddress,
+ };
+ insertFakeBtcTransactions(transformBtcTransaction([tx]));
+ break;
+ }
+ case 'USDC_MATIC': {
+ const tx: UsdcTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: true,
+ recipientLabel: demoPolygonAddress,
+ };
+ insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ break;
+ }
+ case 'USDT_MATIC': {
+ const tx: UsdtTransactionDefinition = {
+ fraction: 1,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ incoming: true,
+ recipientLabel: demoPolygonAddress,
+ };
- insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
- break;
- }
- default: {
- console.warn(`Unsupported asset type: ${toAsset}`);
- }
+ insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ break;
}
- console.log('Demo swap completed:', { fromAsset, toAsset, fromAmount, toAmount });
- }, 3000); // Simulate a delay for the swap to complete
+ default: {
+ console.warn(`Unsupported asset type: ${toAsset}`);
+ }
+ }
+ console.log('Demo swap completed:', { fromAsset, toAsset, fromAmount, toAmount });
}
const ignoreHubRequests = [
From a4d3ddcacbe2d30683ec17079d1c4e0bf9c6ffa1 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 11 Mar 2025 09:33:13 +0100
Subject: [PATCH 08/31] feat(demo): enable sell and swap modals in demo mode
---
src/stores/Demo.ts | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index 29f848b26..e97096807 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -110,7 +110,7 @@ export const useDemoStore = createStore({
attachIframeListeners();
replaceStakingFlow();
replaceBuyNimFlow();
- enableSendModalInDemoMode();
+ enableSellAndSwapModals();
listenForSwapChanges();
},
@@ -1058,10 +1058,7 @@ const demoCSS = `
`;
/**
- * Intercepts fetch request:
- * - fastspot.apiEndpoint will return a mock limit response
- * - estimate endpoint will return mock estimate response
- * - the rest of requests will be passed through
+ * Intercepts fetch request for swaps
*/
function interceptFetchRequest() {
const originalFetch = window.fetch;
@@ -1307,17 +1304,17 @@ function getNetworkFeePerUnit(asset: string): number {
* Ensures the Send button in modals is always enabled in demo mode, regardless of network state.
* This allows users to interact with the send functionality without waiting for network sync.
*/
-function enableSendModalInDemoMode() {
+function enableSellAndSwapModals() {
const observer = new MutationObserver(() => {
- // Target the send modal footer button
- const sendButton = document.querySelector('.send-modal-footer .nq-button');
- if (!sendButton) return;
+ // Target the send modal and swap footer button
+ const bottomButton = document.querySelector('.send-modal-footer .nq-button');
+ if (!bottomButton) return;
- if (sendButton.hasAttribute('disabled')) {
- sendButton.removeAttribute('disabled');
+ if (bottomButton.hasAttribute('disabled')) {
+ bottomButton.removeAttribute('disabled');
// Also remove any visual indications of being disabled
- sendButton.classList.remove('disabled');
+ bottomButton.classList.remove('disabled');
// Also find and hide any sync message if shown
const footer = document.querySelector('.send-modal-footer');
From b9c06c131990870f7699454129ad8f656ee1d6f7 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 11 Mar 2025 10:02:35 +0100
Subject: [PATCH 09/31] chore(demo): only allow nim-btc swaps
---
src/stores/Demo.ts | 122 +++++++++++++++++++++++++++------------------
1 file changed, 73 insertions(+), 49 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index e97096807..5c4111512 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -94,6 +94,7 @@ export const useDemoStore = createStore({
rewriteDemoRoutes();
setupVisualCues();
addDemoModalRoutes();
+ disableSwapTriggers();
interceptFetchRequest();
setupDemoAddresses();
@@ -203,6 +204,18 @@ function setupVisualCues() {
mutationObserver.observe(document.body, { childList: true, subtree: true });
}
+function disableSwapTriggers() {
+ const mutationObserver = new MutationObserver(() => {
+ const swapTriggers = document.querySelectorAll(
+ '.account-grid > :where(.nim-usdc-swap-button, .nim-btc-swap-button, .btc-usdc-swap-button, .account-backgrounds)'
+ ) as NodeListOf;
+ swapTriggers.forEach((trigger) => trigger.remove());
+ const pairSelection = document.querySelector('.pair-selection');
+ if (pairSelection) pairSelection.remove();
+ });
+ mutationObserver.observe(document.body, { childList: true, subtree: true });
+}
+
/**
* Adds routes pointing to our demo modals.
*/
@@ -1055,6 +1068,17 @@ const demoCSS = `
.send-modal-footer .footer-notice {
display: none;
}
+
+.account-grid > button.reset,
+.account-grid > .nimiq-account {
+ background: #e7e8ea;
+ border-radius: 8px;
+ transition: background 0.2s var(--nimiq-ease);
+}
+
+.account-grid > button:where(:hover, :focus-visible) {
+ background: #dedee2 !important;
+}
`;
/**
@@ -1433,30 +1457,30 @@ function completeSwap(activeSwap: any) {
insertFakeBtcTransactions(transformBtcTransaction([tx]));
break;
}
- case 'USDC_MATIC': {
- const tx: UsdcTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: false,
- recipientLabel: demoPolygonAddress,
- };
-
- insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
- break;
- }
- case 'USDT_MATIC': {
- const tx: UsdtTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: false,
- recipientLabel: demoPolygonAddress,
- };
-
- insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
- break;
- }
+ // case 'USDC_MATIC': {
+ // const tx: UsdcTransactionDefinition = {
+ // fraction: 1,
+ // daysAgo: 0,
+ // description: `Swap ${fromAsset} to ${toAsset}`,
+ // incoming: false,
+ // recipientLabel: demoPolygonAddress,
+ // };
+
+ // insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ // break;
+ // }
+ // case 'USDT_MATIC': {
+ // const tx: UsdtTransactionDefinition = {
+ // fraction: 1,
+ // daysAgo: 0,
+ // description: `Swap ${fromAsset} to ${toAsset}`,
+ // incoming: false,
+ // recipientLabel: demoPolygonAddress,
+ // };
+
+ // insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ // break;
+ // }
default: {
console.warn(`Unsupported asset type: ${fromAsset}`);
}
@@ -1496,29 +1520,29 @@ function completeSwap(activeSwap: any) {
insertFakeBtcTransactions(transformBtcTransaction([tx]));
break;
}
- case 'USDC_MATIC': {
- const tx: UsdcTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: true,
- recipientLabel: demoPolygonAddress,
- };
- insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
- break;
- }
- case 'USDT_MATIC': {
- const tx: UsdtTransactionDefinition = {
- fraction: 1,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- incoming: true,
- recipientLabel: demoPolygonAddress,
- };
-
- insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
- break;
- }
+ // case 'USDC_MATIC': {
+ // const tx: UsdcTransactionDefinition = {
+ // fraction: 1,
+ // daysAgo: 0,
+ // description: `Swap ${fromAsset} to ${toAsset}`,
+ // incoming: true,
+ // recipientLabel: demoPolygonAddress,
+ // };
+ // insertFakeUsdcTransactions(transformUsdcTransaction([tx]));
+ // break;
+ // }
+ // case 'USDT_MATIC': {
+ // const tx: UsdtTransactionDefinition = {
+ // fraction: 1,
+ // daysAgo: 0,
+ // description: `Swap ${fromAsset} to ${toAsset}`,
+ // incoming: true,
+ // recipientLabel: demoPolygonAddress,
+ // };
+
+ // insertFakeUsdtTransactions(transformUsdtTransaction([tx]));
+ // break;
+ // }
default: {
console.warn(`Unsupported asset type: ${toAsset}`);
}
@@ -1535,7 +1559,7 @@ interface SetupSwapArgs {
accountId: string;
swapId: string;
fund: {
- type: 'BTC' | 'NIM' | 'USDC' | 'USDT',
+ type: 'BTC' | 'NIM' /*| 'USDC' | 'USDT' */,
inputs: {
address: string,
transactionHash: string,
@@ -1553,7 +1577,7 @@ interface SetupSwapArgs {
refundAddress: string,
};
redeem: {
- type: 'BTC' | 'NIM' | 'USDC' | 'USDT',
+ type: 'BTC' | 'NIM' /*| 'USDC' | 'USDT' */,
recipient: string,
value: number,
fee: number,
From 7641cbab11d4c1c79c8cc00223d0f928938c42fc Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 11 Mar 2025 10:33:30 +0100
Subject: [PATCH 10/31] chore(demo): lint
---
src/stores/Demo.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/stores/Demo.ts b/src/stores/Demo.ts
index 5c4111512..8b73bcced 100644
--- a/src/stores/Demo.ts
+++ b/src/stores/Demo.ts
@@ -207,7 +207,7 @@ function setupVisualCues() {
function disableSwapTriggers() {
const mutationObserver = new MutationObserver(() => {
const swapTriggers = document.querySelectorAll(
- '.account-grid > :where(.nim-usdc-swap-button, .nim-btc-swap-button, .btc-usdc-swap-button, .account-backgrounds)'
+ '.account-grid > :where(.nim-usdc-swap-button, .nim-btc-swap-button, .btc-usdc-swap-button, .account-backgrounds)',
) as NodeListOf;
swapTriggers.forEach((trigger) => trigger.remove());
const pairSelection = document.querySelector('.pair-selection');
@@ -1559,7 +1559,7 @@ interface SetupSwapArgs {
accountId: string;
swapId: string;
fund: {
- type: 'BTC' | 'NIM' /*| 'USDC' | 'USDT' */,
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
inputs: {
address: string,
transactionHash: string,
@@ -1577,7 +1577,7 @@ interface SetupSwapArgs {
refundAddress: string,
};
redeem: {
- type: 'BTC' | 'NIM' /*| 'USDC' | 'USDT' */,
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
recipient: string,
value: number,
fee: number,
From db88ea27162d6249a258ccdc20ef7faeb440548b Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 11 Mar 2025 11:07:57 +0100
Subject: [PATCH 11/31] chore(demo): reverted changes in BuyOptionsModal.
Optimized MutationObserver
---
src/components/modals/BuyOptionsModal.vue | 4 -
src/stores/Demo.ts | 303 ++++++++++++----------
2 files changed, 173 insertions(+), 134 deletions(-)
diff --git a/src/components/modals/BuyOptionsModal.vue b/src/components/modals/BuyOptionsModal.vue
index e3070b727..a9a0604bf 100644
--- a/src/components/modals/BuyOptionsModal.vue
+++ b/src/components/modals/BuyOptionsModal.vue
@@ -202,7 +202,6 @@
+
+
From f02abf382410960327e8072f7788edb8450a5812 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 15 Apr 2025 10:16:05 +0200
Subject: [PATCH 20/31] chore: speed up swap process
---
src/lib/Demo.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index 71923f4e2..8f8c3dace 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -1497,7 +1497,7 @@ function listenForSwapChanges() {
});
break;
}
- }, 15_000);
+ }, 1_800);
}
/**
From 08b974006906a96233a879dac6cbfb44256eddc7 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 15 Apr 2025 12:48:50 +0200
Subject: [PATCH 21/31] chore(demo): obfuscate addresses
---
src/lib/Demo.ts | 87 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index 8f8c3dace..94e1863d2 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -153,6 +153,7 @@ function setupSingleMutationObserver() {
setupVisualCues(processedElements);
disableSwapTriggers(processedElements);
enableSellAndSwapModals(processedElements);
+ obfuscateAddresses(processedElements);
};
// Create one mutation observer for all DOM modifications
@@ -1117,6 +1118,17 @@ const demoCSS = `
display: none;
}
+/* Demo address tooltip styling */
+.tooltip.demo-tooltip {
+ width: max-content;
+ background: var(--nimiq-orange-bg);
+ margin-left: -7rem;
+}
+
+.tooltip.demo-tooltip::after {
+ background: #fc750c; /* Match the red theme for the demo warning */
+}
+
.demo-highlight-badge {
position: absolute;
width: 34px;
@@ -1843,3 +1855,78 @@ export class DemoHubApi extends HubApi {
});
}
}
+
+/**
+ * Obfuscates addresses in the UI by:
+ * - Showing only first 3 chunks of addresses (rest are XXXX) for NIM addresses
+ * - Showing only the first few characters for BTC and polygon addresses
+ * - Changing the copy tooltip message
+ * - Changing the copy functionality to provide a demo disclaimer
+ */
+function obfuscateAddresses(processedElements: WeakSet) {
+ // Adds the common clipboard click handler to an element.
+ function addDemoClickHandler(el: HTMLElement) {
+ el.addEventListener('click', (e: MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ el.classList.add('copied');
+ setTimeout(() => el.classList.remove('copied'), 1500);
+ navigator.clipboard.writeText('This is a demo address - not for actual use');
+ }, true);
+ }
+
+ // Updates the tooltip for an element.
+ function updateTooltip(el: HTMLElement) {
+ const tooltip = el.querySelector('.tooltip') as HTMLElement;
+ if (tooltip && !processedElements.has(tooltip)) {
+ processedElements.add(tooltip);
+ tooltip.textContent = 'Demo address';
+ tooltip.classList.add('demo-tooltip');
+ addDemoClickHandler(tooltip);
+ }
+ }
+
+ // Processes an element: marks it as processed, applies any extra changes, updates tooltip, and adds a click handler.
+ function processElement(el: HTMLElement, extraProcess: ((el: HTMLElement) => void) | null = null) {
+ if (processedElements.has(el)) return;
+ processedElements.add(el);
+ if (extraProcess) extraProcess(el);
+ updateTooltip(el);
+ addDemoClickHandler(el);
+ }
+
+ // Process NIM address displays: obfuscate address chunks beyond the first three.
+ const nimAddressElements = document.querySelectorAll('.copyable.address-display') as NodeListOf;
+ nimAddressElements.forEach(el =>
+ processElement(el, (element) => {
+ const chunks = element.querySelectorAll('.chunk');
+ for (let i = 3; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const space = chunk.querySelector('.space');
+ chunk.textContent = 'XXXX';
+ if (space) chunk.appendChild(space);
+ }
+ })
+ );
+
+ // Process short address displays: change the last chunk of the short address.
+ const shortAddressElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable') as NodeListOf;
+ shortAddressElements.forEach(el =>
+ processElement(el, (element) => {
+ const lastChunk = element.querySelector('.short-address .address:last-child');
+ if (lastChunk) {
+ lastChunk.textContent = 'xxxx';
+ }
+ })
+ );
+
+ // Process tooltip boxes inside short address displays.
+ const tooltipBoxElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable .tooltip-box') as NodeListOf;
+ tooltipBoxElements.forEach(el => {
+ if (processedElements.has(el)) return;
+ processedElements.add(el);
+ el.textContent = 'Demo address';
+ el.classList.add('demo-tooltip');
+ addDemoClickHandler(el);
+ });
+}
From 84a9b7346a7e0ee2e0f1663160520af7713c9cb7 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 15 Apr 2025 18:50:39 +0200
Subject: [PATCH 22/31] chore(demo): handle receive modal
---
src/lib/Demo.ts | 60 +++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 51 insertions(+), 9 deletions(-)
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index 94e1863d2..a82906f51 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -56,10 +56,10 @@ const demoPolygonAddress = '0xabc123DemoPolygonAddress';
const buyFromAddress = 'NQ04 JG63 HYXL H3QF PPNA 7ED7 426M 3FQE FHE5';
// We keep this as our global/final balance, which should result from the transactions
-const nimInitialBalance = 140_418_00000; // 14,041,800,000 - 14 april, 2018. 5 decimals.
-const btcInitialBalance = 1_0000000; // 1 BTC (8 decimals)
-const usdtInitialBalance = 5_000_000000; // 5000 USDT (6 decimals)
-const usdcInitialBalance = 3_000_000000; // 3000 USDC (6 decimals)
+const nimInitialBalance = 140_418 * 1e5; // 14,041,800,000 - 14 april, 2018. 5 decimals.
+const btcInitialBalance = 0.0025 * 1e8; // 1 BTC (8 decimals)
+const usdtInitialBalance = 514.83 * 1e6; // 5000 USDT (6 decimals)
+const usdcInitialBalance = 357.38 * 1e6; // 3000 USDC (6 decimals)
// Swaps
const onGoingSwaps = new Map();
@@ -126,6 +126,12 @@ function insertCustomDemoStyles() {
*/
function rewriteDemoRoutes() {
demoRouter.beforeEach((to, _from, next) => {
+ if (to.path.startsWith('/receive/') && !to.path.startsWith('/receive/nim')) {
+ return next({
+ path: `/${DemoModal.Fallback}`,
+ query: { ...to.query, [DEMO_PARAM]: '' },
+ })
+ }
// Redirect certain known paths to the Buy demo modal
if (to.path === '/buy') {
return next({
@@ -154,6 +160,7 @@ function setupSingleMutationObserver() {
disableSwapTriggers(processedElements);
enableSellAndSwapModals(processedElements);
obfuscateAddresses(processedElements);
+ observeReceiveModal(processedElements);
};
// Create one mutation observer for all DOM modifications
@@ -1897,7 +1904,7 @@ function obfuscateAddresses(processedElements: WeakSet) {
// Process NIM address displays: obfuscate address chunks beyond the first three.
const nimAddressElements = document.querySelectorAll('.copyable.address-display') as NodeListOf;
- nimAddressElements.forEach(el =>
+ nimAddressElements.forEach((el) =>
processElement(el, (element) => {
const chunks = element.querySelectorAll('.chunk');
for (let i = 3; i < chunks.length; i++) {
@@ -1906,23 +1913,23 @@ function obfuscateAddresses(processedElements: WeakSet) {
chunk.textContent = 'XXXX';
if (space) chunk.appendChild(space);
}
- })
+ }),
);
// Process short address displays: change the last chunk of the short address.
const shortAddressElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable') as NodeListOf;
- shortAddressElements.forEach(el =>
+ shortAddressElements.forEach((el) =>
processElement(el, (element) => {
const lastChunk = element.querySelector('.short-address .address:last-child');
if (lastChunk) {
lastChunk.textContent = 'xxxx';
}
- })
+ }),
);
// Process tooltip boxes inside short address displays.
const tooltipBoxElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable .tooltip-box') as NodeListOf;
- tooltipBoxElements.forEach(el => {
+ tooltipBoxElements.forEach((el) => {
if (processedElements.has(el)) return;
processedElements.add(el);
el.textContent = 'Demo address';
@@ -1930,3 +1937,38 @@ function obfuscateAddresses(processedElements: WeakSet) {
addDemoClickHandler(el);
});
}
+
+/**
+ * Observes the receive modal and redirects relevant button clicks to the fallback modal
+ */
+function observeReceiveModal(processedElements: WeakSet) {
+ // Find the receive modal
+ const receiveModal = document.querySelector('.receive-modal');
+ if (!receiveModal) return;
+
+ // Look for buttons that should redirect to the fallback modal
+ const buttons = receiveModal.querySelectorAll('.nq-button-s, .qr-button');
+
+ buttons.forEach(button => {
+ // Skip if we've already processed this button
+ if (processedElements.has(button)) return;
+
+ // Mark as processed to avoid adding multiple listeners
+ processedElements.add(button);
+
+ // Replace the original click handler with our redirect
+ button.addEventListener('click', (event) => {
+ // Prevent the default action and stop propagation
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Redirect to the fallback modal
+ demoRouter.replace({
+ path: `/${DemoModal.Fallback}`,
+ query: { [DEMO_PARAM]: '' },
+ });
+
+ console.log('[Demo] Redirected receive modal button click to fallback modal');
+ }, true); // Use capture to intercept the event before other handlers
+ });
+}
From 9ea22a8237b94f9756d2f0eeb1f5b39022856251 Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 17 Apr 2025 13:55:10 +0200
Subject: [PATCH 23/31] fix: keep `demo` query after each navigation
---
src/lib/Demo.ts | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index a82906f51..491d46519 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -125,25 +125,25 @@ function insertCustomDemoStyles() {
* Sets up a router guard to handle redirects for the demo environment.
*/
function rewriteDemoRoutes() {
- demoRouter.beforeEach((to, _from, next) => {
+ demoRouter.beforeResolve(async (to, from, next) => {
+ // Avoid displaying receive modal
if (to.path.startsWith('/receive/') && !to.path.startsWith('/receive/nim')) {
- return next({
- path: `/${DemoModal.Fallback}`,
- query: { ...to.query, [DEMO_PARAM]: '' },
- })
+ return next({ path: `/${DemoModal.Fallback}`, query: { ...to.query, [DEMO_PARAM]: '' } });
}
+
// Redirect certain known paths to the Buy demo modal
if (to.path === '/buy') {
- return next({
- path: `/${DemoModal.Buy}`,
- query: { ...to.query, [DEMO_PARAM]: '' },
- });
+ return next({ path: `/${DemoModal.Buy}`, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
}
- // Ensure the ?demo param is in place
- if (to.query.demo === '') return next();
- return next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' } });
+ // FIXME: When clicking the hamburger menu, nothing opens
+ if (to.query[DEMO_PARAM] === undefined) {
+ next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
+ }
+
+ next();
});
+
}
/**
@@ -1945,29 +1945,29 @@ function observeReceiveModal(processedElements: WeakSet) {
// Find the receive modal
const receiveModal = document.querySelector('.receive-modal');
if (!receiveModal) return;
-
+
// Look for buttons that should redirect to the fallback modal
const buttons = receiveModal.querySelectorAll('.nq-button-s, .qr-button');
-
+
buttons.forEach(button => {
// Skip if we've already processed this button
if (processedElements.has(button)) return;
-
+
// Mark as processed to avoid adding multiple listeners
processedElements.add(button);
-
+
// Replace the original click handler with our redirect
button.addEventListener('click', (event) => {
// Prevent the default action and stop propagation
event.preventDefault();
event.stopPropagation();
-
+
// Redirect to the fallback modal
demoRouter.replace({
path: `/${DemoModal.Fallback}`,
query: { [DEMO_PARAM]: '' },
});
-
+
console.log('[Demo] Redirected receive modal button click to fallback modal');
}, true); // Use capture to intercept the event before other handlers
});
From 3867fd6faefb79ff022adca247b67739b7247a11 Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 17 Apr 2025 19:24:15 +0200
Subject: [PATCH 24/31] feat(demo): improved buy flow
---
src/components/modals/demos/DemoModalBuy.vue | 335 ++++++++++++++----
.../modals/demos/DemoModalFallback.vue | 1 +
src/lib/Demo.ts | 60 +++-
3 files changed, 320 insertions(+), 76 deletions(-)
diff --git a/src/components/modals/demos/DemoModalBuy.vue b/src/components/modals/demos/DemoModalBuy.vue
index 537d70bf1..4dc5d58a3 100644
--- a/src/components/modals/demos/DemoModalBuy.vue
+++ b/src/components/modals/demos/DemoModalBuy.vue
@@ -1,140 +1,333 @@
-
+
{{ $t('Buy NIM') }}
-
- {{ $t('DEMO') }}
-
-
+
+
+
+
+
+
+ {{ selectedFiatCurrency.toUpperCase() }}
+
+
+
+
+
+
+ NIM
+
+
+
+
-
+
+
+
+
diff --git a/src/components/modals/demos/DemoModalFallback.vue b/src/components/modals/demos/DemoModalFallback.vue
index f203bdda4..2b44779db 100644
--- a/src/components/modals/demos/DemoModalFallback.vue
+++ b/src/components/modals/demos/DemoModalFallback.vue
@@ -9,6 +9,7 @@
{{ $t('You don\'t even need your email.') }}
+
{{ $t('Create a wallet simply by setting a password and give the real deal a try. No download, no costs, no personal data.') }}
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index 491d46519..b649ce353 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -133,17 +133,16 @@ function rewriteDemoRoutes() {
// Redirect certain known paths to the Buy demo modal
if (to.path === '/buy') {
- return next({ path: `/${DemoModal.Buy}`, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
+ return next({ path: `/${DemoModal.Buy}`, query: { ...to.query, [DEMO_PARAM]: '' } });
}
// FIXME: When clicking the hamburger menu, nothing opens
if (to.query[DEMO_PARAM] === undefined) {
- next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
+ return next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
}
next();
});
-
}
/**
@@ -160,6 +159,7 @@ function setupSingleMutationObserver() {
disableSwapTriggers(processedElements);
enableSellAndSwapModals(processedElements);
obfuscateAddresses(processedElements);
+ observeTransactionList(processedElements);
observeReceiveModal(processedElements);
};
@@ -520,7 +520,7 @@ export function dangerouslyInsertFakeBuyNimTransaction(amount: number) {
sender: buyFromAddress,
data: {
type: 'raw',
- raw: encodeTextToHex('Online Purchase'),
+ raw: encodeTextToHex('NIM Bank purchase'),
},
};
@@ -1949,7 +1949,7 @@ function observeReceiveModal(processedElements: WeakSet) {
// Look for buttons that should redirect to the fallback modal
const buttons = receiveModal.querySelectorAll('.nq-button-s, .qr-button');
- buttons.forEach(button => {
+ buttons.forEach((button) => {
// Skip if we've already processed this button
if (processedElements.has(button)) return;
@@ -1972,3 +1972,53 @@ function observeReceiveModal(processedElements: WeakSet) {
}, true); // Use capture to intercept the event before other handlers
});
}
+
+/**
+ * Observes the transaction list items to replace the identicon and address.
+ */
+function observeTransactionList(processedElements: WeakSet) {
+ const buttons = document.querySelectorAll('.transaction-list button.reset.transaction.confirmed');
+ buttons.forEach((button) => {
+ if (processedElements.has(button)) return;
+ processedElements.add(button);
+
+ const message = button.querySelector('.message') as HTMLDivElement;
+ if (!message || message.innerText !== '·NIM Bank purchase') return;
+
+ // Replace identicon with bankSvg
+ const iconDiv = button.querySelector(':scope > .identicon');
+ if (iconDiv) {
+ iconDiv.innerHTML = bankSvg;
+ }
+
+ // Replace address text
+ const addressDiv = button.querySelector(':scope > .data > .address');
+ if (addressDiv) {
+ addressDiv.textContent = 'Demo Bank';
+ }
+ });
+
+ // Replace the identicon in the transaction modal for the bank
+ const transactionModal = document.querySelector('.transaction-modal') as HTMLDivElement;
+ if (!transactionModal) return;
+ if (processedElements.has(transactionModal)) return;
+ processedElements.add(transactionModal);
+ const message = transactionModal.querySelector('.message') as HTMLDivElement;
+ if (message && message.innerText === 'NIM Bank purchase') {
+ const iconDiv = transactionModal.querySelector('.identicon > .identicon');
+ if (iconDiv) {
+ iconDiv.innerHTML= bankSvg.replaceAll('="48"', '="72"');
+ }
+ }
+}
+
+const bankSvg = ``;
From 9a8574ca5825bfc2b38ee391379e878882fc5ff0 Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 15 May 2025 08:13:35 +0200
Subject: [PATCH 25/31] fix: rename `isDemoEnabled` to `isDemoActive` for
consistency
---
src/components/layouts/Sidebar.vue | 6 +++---
src/main.ts | 10 +++++-----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue
index e28395385..0679cb996 100644
--- a/src/components/layouts/Sidebar.vue
+++ b/src/components/layouts/Sidebar.vue
@@ -20,7 +20,7 @@
-
+
{{ $t('Demo mode') }}
@@ -380,7 +380,7 @@ export default defineComponent({
: null;
});
- const isDemoEnabled = checkIfDemoIsActive();
+ const isDemoActive = checkIfDemoIsActive();
return {
CryptoCurrency,
@@ -408,7 +408,7 @@ export default defineComponent({
openSellModal,
nimSellOptions,
activeCurrency,
- isDemoEnabled,
+ isDemoActive ,
};
},
components: {
diff --git a/src/main.ts b/src/main.ts
index 1efa4984f..7a9eff898 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -51,9 +51,9 @@ Vue.use(VuePortal, { name: 'Portal' });
async function start() {
initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
- const isDemoEnabled = checkIfDemoIsActive();
+ const isDemoActive = checkIfDemoIsActive();
- if (!isDemoEnabled) {
+ if (!isDemoActive ) {
await initStorage(); // Must be awaited before starting Vue
initTrials(); // Must be called after storage was initialized, can affect Config
// Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
@@ -104,13 +104,13 @@ async function start() {
const { language } = useSettingsStore();
loadLanguage(language.value);
- if (!isDemoEnabled) {
+ if (!isDemoActive ) {
startSentry();
}
const { config } = useConfig();
- if (isDemoEnabled) {
+ if (isDemoActive ) {
document.title = 'Nimiq Wallet Demo';
} else if (config.environment !== ENV_MAIN) {
document.title = 'Nimiq Testnet Wallet';
@@ -121,7 +121,7 @@ async function start() {
initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
});
- if (!isDemoEnabled) {
+ if (!isDemoActive ) {
watch(() => {
if (!config.oasis.apiEndpoint) return;
initOasisApi(config.oasis.apiEndpoint);
From 1edee3fc3e9cd64311eb6009ded933cf06d6d662 Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 15 May 2025 08:40:22 +0200
Subject: [PATCH 26/31] lint
---
src/components/modals/demos/DemoModalBuy.vue | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/src/components/modals/demos/DemoModalBuy.vue b/src/components/modals/demos/DemoModalBuy.vue
index 4dc5d58a3..1f8cb4305 100644
--- a/src/components/modals/demos/DemoModalBuy.vue
+++ b/src/components/modals/demos/DemoModalBuy.vue
@@ -58,16 +58,6 @@
message="This transaction is instant and secure."
/>
-
@@ -326,8 +316,8 @@ export default defineComponent({
::v-deep .status-screen {
height: 100%;
- position: absolute;
- inset: -6px;
- width: calc(100% + 6px);
+ position: absolute;
+ inset: -6px;
+ width: calc(100% + 6px);
}
From 256fcb5c4f8543cc67823c990af3f50e4e968b0d Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 15 May 2025 08:40:40 +0200
Subject: [PATCH 27/31] feat(demo): add demo mode configuration and
initialization logic
---
src/config/config.local.ts | 8 ++-
src/config/config.mainnet.ts | 6 ++
src/config/config.testnet.ts | 6 ++
src/lib/Demo.ts | 128 ++++++++++++++++++++++++++---------
4 files changed, 114 insertions(+), 34 deletions(-)
diff --git a/src/config/config.local.ts b/src/config/config.local.ts
index 060592cce..ff571b789 100644
--- a/src/config/config.local.ts
+++ b/src/config/config.local.ts
@@ -20,6 +20,12 @@ export default {
enableBitcoin: true,
pageVisibilityTxRefreshInterval: 1 * 60e3, // 1 minute
+ demo: {
+ // Controls if demo mode is enabled. When set to a string, demo mode is only enabled
+ // if the current hostname matches this value. When true, demo mode is always enabled.
+ enabled: false,
+ },
+
staking: {
// The block heights determining the on-chain pre-staking window. All transactions inside this window count
// for pre-staking.
@@ -37,7 +43,7 @@ export default {
},
polygon: {
- enabled: false,
+ enabled: true,
networkId: 80002,
rpcEndpoint: 'wss://polygon-amoy.g.alchemy.com/v2/#ALCHEMY_API_KEY#',
rpcMaxBlockRange: 1_296_000, // 30 days - Range not limited, only limited by number of logs returned
diff --git a/src/config/config.mainnet.ts b/src/config/config.mainnet.ts
index 98a4fcfa9..78b48d003 100644
--- a/src/config/config.mainnet.ts
+++ b/src/config/config.mainnet.ts
@@ -30,6 +30,12 @@ export default {
enableBitcoin: true,
pageVisibilityTxRefreshInterval: 5 * 60e3, // 5 minutes
+ demo: {
+ // Controls if demo mode is enabled. When set to a string, demo mode is only enabled
+ // if the current hostname matches this value. When true, demo mode is always enabled.
+ enabled: 'wallet-demo.nimiq.com',
+ },
+
staking: {
// The block heights determining the on-chain pre-staking window. All transactions inside this window count
// for pre-staking.
diff --git a/src/config/config.testnet.ts b/src/config/config.testnet.ts
index c4a71d48e..55c5782bb 100644
--- a/src/config/config.testnet.ts
+++ b/src/config/config.testnet.ts
@@ -20,6 +20,12 @@ export default {
enableBitcoin: true,
pageVisibilityTxRefreshInterval: 2 * 60e3, // 2 minutes
+ demo: {
+ // Controls if demo mode is enabled. When set to a string, demo mode is only enabled
+ // if the current hostname matches this value. When true, demo mode is always enabled.
+ enabled: false,
+ },
+
staking: {
prestakingStartBlock: 3_023_730,
prestakingEndBlock: 3_028_050,
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index b649ce353..bffbacfc9 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -1,4 +1,27 @@
/* eslint-disable max-len, consistent-return, no-console, no-async-promise-executor */
+
+/**
+ * Demo Mode for the Nimiq Wallet
+ * ------------------------------
+ *
+ * This module provides a complete demo environment for the Nimiq Wallet, allowing users to
+ * try out wallet features without connecting to real blockchain networks.
+ *
+ * Demo mode can be activated in three ways:
+ * 1. By adding the '?demo=' URL parameter to any wallet URL
+ * 2. Setting config.demo.enabled = true in the configuration (always enabled)
+ * 3. Setting config.demo.enabled = 'domain.com' to enable only on a specific domain
+ *
+ * When active, demo mode:
+ * - Creates fake accounts with demo balances
+ * - Generates fake transaction history
+ * - Simulates blockchain interactions like sending, receiving, and swapping
+ * - Obfuscates addresses and disables certain features that wouldn't work in demo mode
+ * - Redirects certain actions to explainer modals
+ *
+ * This is intended for demonstration, educational, and testing purposes only.
+*/
+
import VueRouter from 'vue-router';
import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
@@ -31,6 +54,8 @@ export type DemoState = {
// The query param that activates the demo. e.g. https://wallet.nimiq.com/?demo=
const DEMO_PARAM = 'demo';
+// No additional import needed, Config is already imported above
+
const DemoFallbackModal = () =>
import(
/* webpackChunkName: 'demo-hub-fallback-modal' */
@@ -39,7 +64,7 @@ const DemoFallbackModal = () =>
const DemoPurchaseModal = () =>
import(
- /* webpackChunkName: 'account-menu-modal' */
+ /* webpackChunkName: 'demo-modal-buy' */
'@/components/modals/demos/DemoModalBuy.vue'
);
@@ -73,12 +98,17 @@ let demoRouter: VueRouter;
* Initializes the demo environment and sets up various routes, data, and watchers.
*/
export function dangerouslyInitializeDemo(router: VueRouter) {
+ // Check if demo is active according to the configuration
+ if (!checkIfDemoIsActive()) {
+ console.info('[Demo] Demo mode not enabled in configuration. Skipping initialization.');
+ return;
+ }
+
console.warn('[Demo] Initializing demo environment...');
demoRouter = router;
insertCustomDemoStyles();
- rewriteDemoRoutes();
setupSingleMutationObserver();
addDemoModalRoutes();
interceptFetchRequest();
@@ -106,10 +136,28 @@ export function dangerouslyInitializeDemo(router: VueRouter) {
// #region App setup
/**
- * Checks if the 'demo' query param is present in the URL.
+ * Checks if the demo mode should be active based on the URL param and configuration.
+ * Demo mode is active if:
+ * 1. The demo query param is present in the URL, OR
+ * 2. The Config.demo.enabled is true, OR
+ * 3. The Config.demo.enabled is a string that matches the current hostname
*/
export function checkIfDemoIsActive() {
- return window.location.search.includes(DEMO_PARAM);
+ // Always check URL param first - this allows demo mode to be forced on any instance
+ if (window.location.search.includes(DEMO_PARAM)) return true;
+
+ // Check configuration - can be boolean or string (hostname)
+ const demoConfig = Config.demo?.enabled;
+
+ if (typeof demoConfig === 'boolean') {
+ return demoConfig;
+ }
+
+ if (typeof demoConfig === 'string') {
+ return window.location.hostname === demoConfig;
+ }
+
+ return false;
}
/**
@@ -121,30 +169,6 @@ function insertCustomDemoStyles() {
document.head.appendChild(styleElement);
}
-/**
- * Sets up a router guard to handle redirects for the demo environment.
- */
-function rewriteDemoRoutes() {
- demoRouter.beforeResolve(async (to, from, next) => {
- // Avoid displaying receive modal
- if (to.path.startsWith('/receive/') && !to.path.startsWith('/receive/nim')) {
- return next({ path: `/${DemoModal.Fallback}`, query: { ...to.query, [DEMO_PARAM]: '' } });
- }
-
- // Redirect certain known paths to the Buy demo modal
- if (to.path === '/buy') {
- return next({ path: `/${DemoModal.Buy}`, query: { ...to.query, [DEMO_PARAM]: '' } });
- }
-
- // FIXME: When clicking the hamburger menu, nothing opens
- if (to.query[DEMO_PARAM] === undefined) {
- return next({ path: to.path, query: { ...to.query, [DEMO_PARAM]: '' }, replace: true });
- }
-
- next();
- });
-}
-
/**
* Sets up a single mutation observer to handle all DOM-based demo features
*/
@@ -320,7 +344,16 @@ function attachIframeListeners() {
const { kind, data } = event.data as DemoFlowMessage;
if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
- demoRouter.push(demoRoutes[data]);
+
+ // Only include demo parameter in query if it's present in the current URL
+ const query = window.location.search.includes(DEMO_PARAM)
+ ? { [DEMO_PARAM]: '' }
+ : undefined;
+
+ demoRouter.push({
+ path: demoRoutes[data],
+ query,
+ });
}
});
@@ -971,7 +1004,14 @@ function replaceBuyNimFlow() {
btn1.className = 'nq-button-s inverse';
btn1.style.flex = '1';
btn1.addEventListener('click', () => {
- demoRouter.push('/buy');
+ // Only include demo parameter in query if it's present in the current URL
+ const query = window.location.search.includes(DEMO_PARAM)
+ ? { [DEMO_PARAM]: '' }
+ : undefined;
+ demoRouter.push({
+ path: '/buy',
+ query,
+ });
});
btn1.innerHTML = 'Buy';
@@ -1021,7 +1061,17 @@ function replaceStakingFlow() {
const amount = Number.parseFloat(amountInput.value.replaceAll(/[^\d]/g, '')) * 1e5;
const { address: validatorAddress } = activeValidator.value!;
- demoRouter.push('/');
+
+ // Only include demo parameter in query if it's present in the current URL
+ const query = window.location.search.includes(DEMO_PARAM)
+ ? { [DEMO_PARAM]: '' }
+ : undefined;
+
+ demoRouter.push({
+ path: '/',
+ query,
+ });
+
await new Promise((resolve) => { window.setTimeout(resolve, 100); });
setStake({
activeBalance: 0,
@@ -1856,7 +1906,14 @@ export class DemoHubApi extends HubApi {
});
console.log('[Demo] Redirecting to fallback modal');
- demoRouter.push(`/${DemoModal.Fallback}`);
+ // Only include demo parameter in query if it's present in the current URL
+ const query = window.location.search.includes(DEMO_PARAM)
+ ? { [DEMO_PARAM]: '' }
+ : undefined;
+ demoRouter.push({
+ path: `/${DemoModal.Fallback}`,
+ query,
+ });
});
},
});
@@ -1962,10 +2019,15 @@ function observeReceiveModal(processedElements: WeakSet) {
event.preventDefault();
event.stopPropagation();
+ // Only include demo parameter in query if it's present in the current URL
+ const query = window.location.search.includes(DEMO_PARAM)
+ ? { [DEMO_PARAM]: '' }
+ : undefined;
+
// Redirect to the fallback modal
demoRouter.replace({
path: `/${DemoModal.Fallback}`,
- query: { [DEMO_PARAM]: '' },
+ query,
});
console.log('[Demo] Redirected receive modal button click to fallback modal');
From 264e250d0aad6c9d201e5d68775cfa6ec0c21733 Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 15 May 2025 08:42:53 +0200
Subject: [PATCH 28/31] lint
---
src/components/layouts/Sidebar.vue | 4 +--
src/lib/Demo.ts | 40 +++++++++++++++---------------
src/main.ts | 10 ++++----
3 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue
index 0679cb996..7ae234d01 100644
--- a/src/components/layouts/Sidebar.vue
+++ b/src/components/layouts/Sidebar.vue
@@ -380,7 +380,7 @@ export default defineComponent({
: null;
});
- const isDemoActive = checkIfDemoIsActive();
+ const isDemoActive = checkIfDemoIsActive();
return {
CryptoCurrency,
@@ -408,7 +408,7 @@ export default defineComponent({
openSellModal,
nimSellOptions,
activeCurrency,
- isDemoActive ,
+ isDemoActive,
};
},
components: {
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index bffbacfc9..4ef27d922 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -3,22 +3,22 @@
/**
* Demo Mode for the Nimiq Wallet
* ------------------------------
- *
+ *
* This module provides a complete demo environment for the Nimiq Wallet, allowing users to
* try out wallet features without connecting to real blockchain networks.
- *
+ *
* Demo mode can be activated in three ways:
* 1. By adding the '?demo=' URL parameter to any wallet URL
* 2. Setting config.demo.enabled = true in the configuration (always enabled)
* 3. Setting config.demo.enabled = 'domain.com' to enable only on a specific domain
- *
+ *
* When active, demo mode:
* - Creates fake accounts with demo balances
* - Generates fake transaction history
* - Simulates blockchain interactions like sending, receiving, and swapping
* - Obfuscates addresses and disables certain features that wouldn't work in demo mode
* - Redirects certain actions to explainer modals
- *
+ *
* This is intended for demonstration, educational, and testing purposes only.
*/
@@ -103,7 +103,7 @@ export function dangerouslyInitializeDemo(router: VueRouter) {
console.info('[Demo] Demo mode not enabled in configuration. Skipping initialization.');
return;
}
-
+
console.warn('[Demo] Initializing demo environment...');
demoRouter = router;
@@ -145,18 +145,18 @@ export function dangerouslyInitializeDemo(router: VueRouter) {
export function checkIfDemoIsActive() {
// Always check URL param first - this allows demo mode to be forced on any instance
if (window.location.search.includes(DEMO_PARAM)) return true;
-
+
// Check configuration - can be boolean or string (hostname)
const demoConfig = Config.demo?.enabled;
-
+
if (typeof demoConfig === 'boolean') {
return demoConfig;
}
-
+
if (typeof demoConfig === 'string') {
return window.location.hostname === demoConfig;
}
-
+
return false;
}
@@ -344,12 +344,12 @@ function attachIframeListeners() {
const { kind, data } = event.data as DemoFlowMessage;
if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
-
+
// Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
+ const query = window.location.search.includes(DEMO_PARAM)
? { [DEMO_PARAM]: '' }
: undefined;
-
+
demoRouter.push({
path: demoRoutes[data],
query,
@@ -1005,7 +1005,7 @@ function replaceBuyNimFlow() {
btn1.style.flex = '1';
btn1.addEventListener('click', () => {
// Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
+ const query = window.location.search.includes(DEMO_PARAM)
? { [DEMO_PARAM]: '' }
: undefined;
demoRouter.push({
@@ -1061,17 +1061,17 @@ function replaceStakingFlow() {
const amount = Number.parseFloat(amountInput.value.replaceAll(/[^\d]/g, '')) * 1e5;
const { address: validatorAddress } = activeValidator.value!;
-
+
// Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
+ const query = window.location.search.includes(DEMO_PARAM)
? { [DEMO_PARAM]: '' }
: undefined;
-
+
demoRouter.push({
path: '/',
query,
});
-
+
await new Promise((resolve) => { window.setTimeout(resolve, 100); });
setStake({
activeBalance: 0,
@@ -1907,7 +1907,7 @@ export class DemoHubApi extends HubApi {
console.log('[Demo] Redirecting to fallback modal');
// Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
+ const query = window.location.search.includes(DEMO_PARAM)
? { [DEMO_PARAM]: '' }
: undefined;
demoRouter.push({
@@ -2020,7 +2020,7 @@ function observeReceiveModal(processedElements: WeakSet) {
event.stopPropagation();
// Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
+ const query = window.location.search.includes(DEMO_PARAM)
? { [DEMO_PARAM]: '' }
: undefined;
@@ -2069,7 +2069,7 @@ function observeTransactionList(processedElements: WeakSet) {
if (message && message.innerText === 'NIM Bank purchase') {
const iconDiv = transactionModal.querySelector('.identicon > .identicon');
if (iconDiv) {
- iconDiv.innerHTML= bankSvg.replaceAll('="48"', '="72"');
+ iconDiv.innerHTML = bankSvg.replaceAll('="48"', '="72"');
}
}
}
diff --git a/src/main.ts b/src/main.ts
index 7a9eff898..51bf08065 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -51,9 +51,9 @@ Vue.use(VuePortal, { name: 'Portal' });
async function start() {
initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
- const isDemoActive = checkIfDemoIsActive();
+ const isDemoActive = checkIfDemoIsActive();
- if (!isDemoActive ) {
+ if (!isDemoActive) {
await initStorage(); // Must be awaited before starting Vue
initTrials(); // Must be called after storage was initialized, can affect Config
// Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
@@ -104,13 +104,13 @@ async function start() {
const { language } = useSettingsStore();
loadLanguage(language.value);
- if (!isDemoActive ) {
+ if (!isDemoActive) {
startSentry();
}
const { config } = useConfig();
- if (isDemoActive ) {
+ if (isDemoActive) {
document.title = 'Nimiq Wallet Demo';
} else if (config.environment !== ENV_MAIN) {
document.title = 'Nimiq Testnet Wallet';
@@ -121,7 +121,7 @@ async function start() {
initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
});
- if (!isDemoActive ) {
+ if (!isDemoActive) {
watch(() => {
if (!config.oasis.apiEndpoint) return;
initOasisApi(config.oasis.apiEndpoint);
From a25e044037f127a48bb1d5dfea16df7cd121b10c Mon Sep 17 00:00:00 2001
From: onmax
Date: Thu, 15 May 2025 09:06:00 +0200
Subject: [PATCH 29/31] fix(config): disable polygon network by default
---
src/config/config.local.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/config/config.local.ts b/src/config/config.local.ts
index ff571b789..c732b8234 100644
--- a/src/config/config.local.ts
+++ b/src/config/config.local.ts
@@ -43,7 +43,7 @@ export default {
},
polygon: {
- enabled: true,
+ enabled: false,
networkId: 80002,
rpcEndpoint: 'wss://polygon-amoy.g.alchemy.com/v2/#ALCHEMY_API_KEY#',
rpcMaxBlockRange: 1_296_000, // 30 days - Range not limited, only limited by number of logs returned
From f1852597d26aee97839499792e4f49cbc1471b68 Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 8 Jul 2025 08:27:35 +0200
Subject: [PATCH 30/31] feat(demo): enhance demo build configuration and
streamline initialization logic
---
package.json | 3 ++
src/lib/Demo.ts | 54 ++------------------
src/main-demo.ts | 128 +++++++++++++++++++++++++++++++++++++++++++++++
src/main.ts | 49 +++++++-----------
vue.config.js | 61 +++++++++++++++++++---
5 files changed, 205 insertions(+), 90 deletions(-)
create mode 100644 src/main-demo.ts
diff --git a/package.json b/package.json
index 88e6965e2..2ba8c27c2 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,10 @@
"packageManager": "yarn@1.22.22",
"scripts": {
"serve": "yarn pre && yarn vue-cli-service serve --port 8081",
+ "serve:demo": "yarn pre && build=demo yarn vue-cli-service serve --port 8081",
"build": "yarn pre && yarn vue-cli-service build",
+ "build:demo": "yarn pre && build=demo NODE_ENV=production yarn vue-cli-service build",
+ "build:demo-production": "yarn pre && build=demo-production NODE_ENV=production yarn vue-cli-service build",
"lint": "vue-cli-service lint --no-fix",
"lint:fix": "vue-cli-service lint",
"build:bitcoinjs": "yarn --silent browserify bitcoinjs-parts.js -p common-shakeify -s BitcoinJS | yarn terser --compress --mangle --source-map --output public/bitcoin/BitcoinJS.min.js",
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index 4ef27d922..ac09f80ac 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -51,9 +51,6 @@ export type DemoState = {
active: boolean,
};
-// The query param that activates the demo. e.g. https://wallet.nimiq.com/?demo=
-const DEMO_PARAM = 'demo';
-
// No additional import needed, Config is already imported above
const DemoFallbackModal = () =>
@@ -136,28 +133,11 @@ export function dangerouslyInitializeDemo(router: VueRouter) {
// #region App setup
/**
- * Checks if the demo mode should be active based on the URL param and configuration.
- * Demo mode is active if:
- * 1. The demo query param is present in the URL, OR
- * 2. The Config.demo.enabled is true, OR
- * 3. The Config.demo.enabled is a string that matches the current hostname
+ * Checks if the demo mode should be active.
+ * In demo builds, this always returns true since demo builds are separate deployments.
*/
export function checkIfDemoIsActive() {
- // Always check URL param first - this allows demo mode to be forced on any instance
- if (window.location.search.includes(DEMO_PARAM)) return true;
-
- // Check configuration - can be boolean or string (hostname)
- const demoConfig = Config.demo?.enabled;
-
- if (typeof demoConfig === 'boolean') {
- return demoConfig;
- }
-
- if (typeof demoConfig === 'string') {
- return window.location.hostname === demoConfig;
- }
-
- return false;
+ return true;
}
/**
@@ -345,14 +325,8 @@ function attachIframeListeners() {
if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
- // Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
- ? { [DEMO_PARAM]: '' }
- : undefined;
-
demoRouter.push({
path: demoRoutes[data],
- query,
});
}
});
@@ -1004,13 +978,8 @@ function replaceBuyNimFlow() {
btn1.className = 'nq-button-s inverse';
btn1.style.flex = '1';
btn1.addEventListener('click', () => {
- // Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
- ? { [DEMO_PARAM]: '' }
- : undefined;
demoRouter.push({
path: '/buy',
- query,
});
});
btn1.innerHTML = 'Buy';
@@ -1062,14 +1031,8 @@ function replaceStakingFlow() {
const { address: validatorAddress } = activeValidator.value!;
- // Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
- ? { [DEMO_PARAM]: '' }
- : undefined;
-
demoRouter.push({
path: '/',
- query,
});
await new Promise((resolve) => { window.setTimeout(resolve, 100); });
@@ -1906,13 +1869,8 @@ export class DemoHubApi extends HubApi {
});
console.log('[Demo] Redirecting to fallback modal');
- // Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
- ? { [DEMO_PARAM]: '' }
- : undefined;
demoRouter.push({
path: `/${DemoModal.Fallback}`,
- query,
});
});
},
@@ -2019,15 +1977,9 @@ function observeReceiveModal(processedElements: WeakSet) {
event.preventDefault();
event.stopPropagation();
- // Only include demo parameter in query if it's present in the current URL
- const query = window.location.search.includes(DEMO_PARAM)
- ? { [DEMO_PARAM]: '' }
- : undefined;
-
// Redirect to the fallback modal
demoRouter.replace({
path: `/${DemoModal.Fallback}`,
- query,
});
console.log('[Demo] Redirected receive modal button click to fallback modal');
diff --git a/src/main-demo.ts b/src/main-demo.ts
new file mode 100644
index 000000000..5a9319daa
--- /dev/null
+++ b/src/main-demo.ts
@@ -0,0 +1,128 @@
+import Vue from 'vue';
+import VueCompositionApi, { watch } from '@vue/composition-api';
+// @ts-expect-error Could not find a declaration file for module 'vue-virtual-scroller'.
+import VueVirtualScroller from 'vue-virtual-scroller';
+import { setAssetPublicPath as setVueComponentsAssetPath } from '@nimiq/vue-components';
+import { init as initFastspotApi } from '@nimiq/fastspot-api';
+// @ts-expect-error missing types for this package
+import VuePortal from '@linusborg/vue-simple-portal';
+
+import App from './App.vue';
+import { dangerouslyInitializeDemo } from './lib/Demo';
+import { useAccountStore } from './stores/Account';
+import { useFiatStore } from './stores/Fiat';
+import { useSettingsStore } from './stores/Settings';
+import router from './router';
+import { i18n, loadLanguage } from './i18n/i18n-setup';
+import { CryptoCurrency } from './lib/Constants';
+import { useConfig } from './composables/useConfig';
+import { initPwa } from './composables/usePwaInstallPrompt';
+import { useInactivityDetection } from './composables/useInactivityDetection';
+
+// Side-effects
+import './lib/AddressBook';
+
+import '@nimiq/style/nimiq-style.min.css';
+import '@nimiq/vue-components/dist/NimiqVueComponents.css';
+import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
+import '@/scss/themes.scss';
+
+// Set asset path relative to the public path defined in vue.config.json,
+// see https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code
+setVueComponentsAssetPath(`${process.env.BASE_URL}js/`, `${process.env.BASE_URL}img/`);
+
+Vue.config.productionTip = false;
+
+Vue.use(VueCompositionApi);
+Vue.use(VueVirtualScroller);
+Vue.use(VuePortal, { name: 'Portal' });
+
+async function startDemo() {
+ // eslint-disable-next-line no-console
+ console.warn('[Demo] Starting Nimiq Wallet in demo mode...');
+
+ initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
+
+ // Initialize demo environment - this replaces the normal storage/hub initialization
+ dangerouslyInitializeDemo(router);
+
+ // Update exchange rates every 2 minutes or every 10 minutes, depending on whether the Wallet is currently actively
+ // used. If an update takes longer than that time due to a provider's rate limit, wait until the update succeeds
+ // before queueing the next update. If the last update before page load was less than 2 minutes ago, wait the
+ // remaining time first.
+ const { timestamp: lastSuccessfulExchangeRateUpdate, updateExchangeRates } = useFiatStore();
+ const { isUserInactive } = useInactivityDetection();
+ let lastTriedExchangeRateUpdate = lastSuccessfulExchangeRateUpdate.value;
+ const TWO_MINUTES = 2 * 60 * 1000;
+ const TEN_MINUTES = 5 * TWO_MINUTES;
+ let exchangeRateUpdateTimer = -1;
+ function queueExchangeRateUpdate() {
+ const interval = isUserInactive.value ? TEN_MINUTES : TWO_MINUTES;
+ // Update lastTriedExchangeRateUpdate as there might have been other exchange rate updates in the meantime, for
+ // example on currency change.
+ lastTriedExchangeRateUpdate = Math.max(lastTriedExchangeRateUpdate, lastSuccessfulExchangeRateUpdate.value);
+ // Also set interval as upper bound to be immune to the user's system clock being wrong.
+ const remainingTime = Math.max(0, Math.min(lastTriedExchangeRateUpdate + interval - Date.now(), interval));
+ clearTimeout(exchangeRateUpdateTimer);
+ exchangeRateUpdateTimer = window.setTimeout(async () => {
+ // Silently ignore errors. If successful, this updates fiatStore.timestamp, which then also triggers price
+ // chart updates in PriceChart.vue.
+ await updateExchangeRates(/* failGracefully */ true);
+ // In contrast to lastSuccessfulExchangeRateUpdate also update lastTriedExchangeRateUpdate on failed
+ // attempts, to avoid repeated rescheduling on failure. Instead, simply skip the failed attempt and try
+ // again at the regular interval. We update the time after the update attempt, instead of before it, because
+ // exchange rates are up-to-date at the time an update successfully finishes, and get old from that point,
+ // and not from the time the update was started.
+ lastTriedExchangeRateUpdate = Date.now();
+ queueExchangeRateUpdate();
+ }, remainingTime);
+ }
+ watch(isUserInactive, queueExchangeRateUpdate); // (Re)schedule exchange rate updates at the desired interval.
+
+ // Fetch language file
+ const { language } = useSettingsStore();
+ loadLanguage(language.value);
+
+ const { config } = useConfig();
+
+ // Set demo-specific document title
+ document.title = 'Nimiq Wallet Demo';
+
+ // Initialize Fastspot API for demo
+ watch(() => {
+ if (!config.fastspot.apiEndpoint || !config.fastspot.apiKey) return;
+ initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
+ });
+
+ // Make reactive config accessible in components
+ Vue.prototype.$config = config;
+
+ new Vue({
+ router,
+ i18n,
+ render: (h) => h(App),
+ }).$mount('#app');
+
+ // Note: We don't launch network, electrum, or polygon connections in demo mode
+ // as the demo uses simulated data instead of real blockchain connections
+
+ // Set active currency to NIM by default for demo
+ const { state: { activeCurrency } } = useAccountStore();
+ if (activeCurrency !== CryptoCurrency.NIM) {
+ useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
+ }
+}
+
+startDemo();
+
+declare module 'vue/types/vue' {
+ interface Vue {
+ $config: ReturnType['config'];
+ }
+}
+
+declare module '@vue/composition-api/dist/component/component' {
+ interface SetupContext {
+ readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] };
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 51bf08065..b5f926fb5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -18,7 +18,6 @@ import { launchPolygon } from './ethers';
import { initMatomo } from './matomo';
import { useAccountStore } from './stores/Account';
import { useFiatStore } from './stores/Fiat';
-import { checkIfDemoIsActive, dangerouslyInitializeDemo } from './lib/Demo';
import { useSettingsStore } from './stores/Settings';
import router from './router';
import { i18n, loadLanguage } from './i18n/i18n-setup';
@@ -51,21 +50,15 @@ Vue.use(VuePortal, { name: 'Portal' });
async function start() {
initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
- const isDemoActive = checkIfDemoIsActive();
+ await initStorage(); // Must be awaited before starting Vue
+ initTrials(); // Must be called after storage was initialized, can affect Config
+ // Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
+ // background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code
+ // anyways.
+ await initHubApi();
+ syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.
- if (!isDemoActive) {
- await initStorage(); // Must be awaited before starting Vue
- initTrials(); // Must be called after storage was initialized, can affect Config
- // Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
- // background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code
- // anyways.
- await initHubApi();
- syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.
-
- serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
- } else {
- dangerouslyInitializeDemo(router);
- }
+ serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
// Update exchange rates every 2 minutes or every 10 minutes, depending on whether the Wallet is currently actively
// used. If an update takes longer than that time due to a provider's rate limit, wait until the update succeeds
@@ -104,15 +97,11 @@ async function start() {
const { language } = useSettingsStore();
loadLanguage(language.value);
- if (!isDemoActive) {
- startSentry();
- }
+ startSentry();
const { config } = useConfig();
- if (isDemoActive) {
- document.title = 'Nimiq Wallet Demo';
- } else if (config.environment !== ENV_MAIN) {
+ if (config.environment !== ENV_MAIN) {
document.title = 'Nimiq Testnet Wallet';
}
@@ -121,17 +110,15 @@ async function start() {
initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
});
- if (!isDemoActive) {
- watch(() => {
- if (!config.oasis.apiEndpoint) return;
- initOasisApi(config.oasis.apiEndpoint);
- });
+ watch(() => {
+ if (!config.oasis.apiEndpoint) return;
+ initOasisApi(config.oasis.apiEndpoint);
+ });
- watch(() => {
- if (!config.ten31Pass.enabled) return;
- initKycConnection();
- });
- }
+ watch(() => {
+ if (!config.ten31Pass.enabled) return;
+ initKycConnection();
+ });
watch(() => {
if (!config.matomo.enabled) return;
diff --git a/vue.config.js b/vue.config.js
index 436a6df2a..b4ca6d0d7 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -13,17 +13,31 @@ crypto.createHash = (alg, opts) => {
return origCreateHash(alg === 'md4' ? 'md5' : alg, opts);
};
-const buildName = process.env.NODE_ENV === 'production' ? process.env.build : 'local';
+let buildName;
+if (process.env.NODE_ENV === 'production') {
+ buildName = process.env.build;
+} else if (process.env.build?.startsWith('demo')) {
+ buildName = 'demo';
+} else {
+ buildName = 'local';
+}
+
if (!buildName) {
throw new Error('Please specify the build config with the `build` environment variable');
}
+// Log the buildName value to help debugging
+console.log('Build name:', buildName);
+
let release;
if (process.env.NODE_ENV !== 'production') {
release = 'dev';
} else if (process.env.CI) {
// CI environment variables are documented at https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
release = `${process.env.CI_COMMIT_BRANCH}-${process.env.CI_PIPELINE_ID}-${process.env.CI_COMMIT_SHORT_SHA}`;
+} else if (buildName.startsWith('demo')) {
+ // For demo builds, use a special release tag format
+ release = `demo-${new Date().toISOString().split('T')[0]}`;
} else {
release = child_process.execSync("git tag --points-at HEAD").toString().split('\n')[0];
}
@@ -46,11 +60,38 @@ function sri(asset) {
process.env.VUE_APP_BITCOIN_JS_INTEGRITY_HASH = sri(fs.readFileSync(path.join(__dirname, 'public/bitcoin/BitcoinJS.min.js')));
process.env.VUE_APP_COPYRIGHT_YEAR = new Date().getUTCFullYear().toString(); // year at build time
-console.log('Building for:', buildName, ', release:', `"wallet-${release}"`);
+console.log('Building for:', buildName, ', release:', `"wallet-${release}"`, buildName === 'demo' ? '(DEMO MODE)' : '');
+
+const configFileMap = {
+ 'local': 'config.local.ts',
+ 'testnet': 'config.testnet.ts',
+ 'mainnet': 'config.mainnet.ts',
+ 'demo': 'config.local.ts',
+ 'demo-production': 'config.mainnet.ts',
+};
+
+const tsConfigFileMap = {
+ 'local': 'tsconfig.local.json',
+ 'testnet': 'tsconfig.testnet.json',
+ 'mainnet': 'tsconfig.mainnet.json',
+ 'demo': 'tsconfig.local.json',
+ 'demo-production': 'tsconfig.mainnet.json',
+};
+
+const configFile = configFileMap[buildName] || 'config.local.ts';
+const tsConfigFile = tsConfigFileMap[buildName] || 'tsconfig.json';
+
+const specificConfigPath = path.join(__dirname, 'src/config', configFile);
+const configPath = fs.existsSync(specificConfigPath) ? specificConfigPath : path.join(__dirname, 'src/config/config.local.ts');
+
+const specificTsConfigPath = path.join(__dirname, tsConfigFile);
+const tsConfigPath = fs.existsSync(specificTsConfigPath) ? specificTsConfigPath : path.join(__dirname, 'tsconfig.json');
+
+console.log(`Using config: ${configPath}, tsconfig: ${tsConfigPath}`);
module.exports = {
pages: {
- index: 'src/main.ts',
+ index: buildName === 'demo' ? 'src/main-demo.ts' : 'src/main.ts',
'swap-kyc-handler': {
// Unfortunately this includes the complete chunk-vendors and chunk-common, and even their css. Can we
// improve this? The `chunks` option doesn't seem to be too useful here. At least the chunks should be
@@ -66,6 +107,7 @@ module.exports = {
new webpack.DefinePlugin({
'process.env.SENTRY_RELEASE': `"wallet-${release}"`,
'process.env.VERSION': `"${release}"`,
+ 'process.env.IS_DEMO_BUILD': JSON.stringify(buildName === 'demo'),
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
@@ -75,7 +117,7 @@ module.exports = {
// Resolve config for yarn build
resolve: {
alias: {
- config: path.join(__dirname, `src/config/config.${buildName}.ts`)
+ config: configPath
},
// In Webpack 5, NodeJS polyfills have to be explicitly configured
fallback: {
@@ -152,8 +194,8 @@ module.exports = {
.use('ts-loader')
.loader('ts-loader')
.tap(options => {
- options.configFile = `tsconfig.${buildName}.json`
- return options
+ options.configFile = tsConfigPath;
+ return options;
});
},
// Note: the pwa is only setup on production builds, thus to test this config, a production build has to be built
@@ -163,7 +205,8 @@ module.exports = {
mainnet: 'Nimiq Wallet',
testnet: 'Nimiq Testnet Wallet',
local: 'Nimiq Local Wallet',
- }[buildName],
+ demo: 'Nimiq Wallet Demo',
+ }[buildName] || 'Nimiq Wallet',
msTileColor: '#1F2348',
manifestOptions: {
start_url: '/',
@@ -186,7 +229,9 @@ module.exports = {
type: "image/png"
},
],
- description: "Securely manage your Nimiq and Bitcoin accounts. Send and receive NIM and BTC, view balances, swap between NIM and BTC, and buy and sell with Nimiq OASIS.",
+ description: buildName === 'demo'
+ ? "Experience the Nimiq Wallet in demo mode. Try out all features with simulated accounts and transactions."
+ : "Securely manage your Nimiq and Bitcoin accounts. Send and receive NIM and BTC, view balances, swap between NIM and BTC, and buy and sell with Nimiq OASIS.",
screenshots: [
{
src: "./img/screenshots/01-Send-and-receive.png",
From 90a6fa2994f251e980eddea2ee4cf9473a0e2cce Mon Sep 17 00:00:00 2001
From: onmax
Date: Tue, 8 Jul 2025 10:01:59 +0200
Subject: [PATCH 31/31] chore(demo): refactor demo module structure and enhance
functionality with new demo accounts and transactions
---
README.md | 6 +-
src/lib/Demo.ts | 2035 +-----------------------------
src/lib/demo/DemoAccounts.ts | 65 +
src/lib/demo/DemoConstants.ts | 169 +++
src/lib/demo/DemoDom.ts | 275 ++++
src/lib/demo/DemoFlows.ts | 105 ++
src/lib/demo/DemoHubApi.ts | 122 ++
src/lib/demo/DemoSwaps.ts | 386 ++++++
src/lib/demo/DemoTransactions.ts | 1130 +++++++++++++++++
src/lib/demo/DemoUtils.ts | 37 +
src/lib/demo/index.ts | 171 +++
11 files changed, 2482 insertions(+), 2019 deletions(-)
create mode 100644 src/lib/demo/DemoAccounts.ts
create mode 100644 src/lib/demo/DemoConstants.ts
create mode 100644 src/lib/demo/DemoDom.ts
create mode 100644 src/lib/demo/DemoFlows.ts
create mode 100644 src/lib/demo/DemoHubApi.ts
create mode 100644 src/lib/demo/DemoSwaps.ts
create mode 100644 src/lib/demo/DemoTransactions.ts
create mode 100644 src/lib/demo/DemoUtils.ts
create mode 100644 src/lib/demo/index.ts
diff --git a/README.md b/README.md
index 1f13e9d5d..694937e3a 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,11 @@ yarn serve
If you need to interact with a real account and data, then you will also need to run the [Nimiq Hub](https://github.com/nimiq/hub#contribute) and [Nimiq Keyguard](https://github.com/nimiq/keyguard/#development) in development, too.
-Otherwise, you can activate demo mode by appending `?demo` to the URL: `http://localhost:8081/?demo`.
+For demo mode with simulated accounts and transactions, use:
+```
+yarn serve:demo
+```
+
### Compiles and minifies for production
```
yarn build
diff --git a/src/lib/Demo.ts b/src/lib/Demo.ts
index ac09f80ac..2de515659 100644
--- a/src/lib/Demo.ts
+++ b/src/lib/Demo.ts
@@ -7,10 +7,10 @@
* This module provides a complete demo environment for the Nimiq Wallet, allowing users to
* try out wallet features without connecting to real blockchain networks.
*
- * Demo mode can be activated in three ways:
- * 1. By adding the '?demo=' URL parameter to any wallet URL
- * 2. Setting config.demo.enabled = true in the configuration (always enabled)
- * 3. Setting config.demo.enabled = 'domain.com' to enable only on a specific domain
+ * Demo mode can only be activated through build commands:
+ * - yarn serve:demo (development)
+ * - yarn build:demo (production build)
+ * - yarn build:demo-production (production build with mainnet config)
*
* When active, demo mode:
* - Creates fake accounts with demo balances
@@ -22,2017 +22,16 @@
* This is intended for demonstration, educational, and testing purposes only.
*/
-import VueRouter from 'vue-router';
-import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
-import { CryptoCurrency, Utf8Tools } from '@nimiq/utils';
-import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
-import { AccountType, useAccountStore } from '@/stores/Account';
-import { AddressType, useAddressStore } from '@/stores/Address';
-import { toSecs, type Transaction as NimTransaction, useTransactionsStore } from '@/stores/Transactions';
-import { useBtcTransactionsStore, type Transaction as BtcTransaction } from '@/stores/BtcTransactions';
-import { useUsdtTransactionsStore, TransactionState as UsdtTransactionState, type Transaction as UsdtTransaction } from '@/stores/UsdtTransactions';
-import { useUsdcTransactionsStore, TransactionState as UsdcTransactionState, type Transaction as UsdcTransaction } from '@/stores/UsdcTransactions';
-import { useStakingStore } from '@/stores/Staking';
-import { useAccountSettingsStore } from '@/stores/AccountSettings';
-import { usePolygonAddressStore } from '@/stores/PolygonAddress';
-import Config from 'config';
-import { FastspotAsset, FastspotLimits, FastspotUserLimits, ReferenceAsset, SwapAsset, SwapStatus } from '@nimiq/fastspot-api';
-import HubApi, { SetupSwapResult } from '@nimiq/hub-api';
-import { useConfig } from '@/composables/useConfig';
-import { useBtcAddressStore } from '@/stores/BtcAddress';
-import { useContactsStore } from '@/stores/Contacts';
-import { useBtcLabelsStore } from '@/stores/BtcLabels';
-import { useUsdcContactsStore } from '@/stores/UsdcContacts';
-import { useUsdtContactsStore } from '@/stores/UsdtContacts';
-import { useFiatStore } from '@/stores/Fiat';
-import { SwapState, useSwapsStore } from '@/stores/Swaps';
-
-export type DemoState = {
- active: boolean,
-};
-
-// No additional import needed, Config is already imported above
-
-const DemoFallbackModal = () =>
- import(
- /* webpackChunkName: 'demo-hub-fallback-modal' */
- '@/components/modals/demos/DemoModalFallback.vue'
- );
-
-const DemoPurchaseModal = () =>
- import(
- /* webpackChunkName: 'demo-modal-buy' */
- '@/components/modals/demos/DemoModalBuy.vue'
- );
-
-// Replacing the enum with a simple object to avoid backticks
-const DemoModal = {
- Fallback: 'demo-fallback',
- Buy: 'demo-buy',
-};
-
-// Addresses for demo:
-const demoNimAddress = 'NQ57 2814 7L5B NBBD 0EU7 EL71 HXP8 M7H8 MHKD';
-const demoBtcAddress = '1XYZDemoAddress';
-const demoPolygonAddress = '0xabc123DemoPolygonAddress';
-const buyFromAddress = 'NQ04 JG63 HYXL H3QF PPNA 7ED7 426M 3FQE FHE5';
-
-// We keep this as our global/final balance, which should result from the transactions
-const nimInitialBalance = 140_418 * 1e5; // 14,041,800,000 - 14 april, 2018. 5 decimals.
-const btcInitialBalance = 0.0025 * 1e8; // 1 BTC (8 decimals)
-const usdtInitialBalance = 514.83 * 1e6; // 5000 USDT (6 decimals)
-const usdcInitialBalance = 357.38 * 1e6; // 3000 USDC (6 decimals)
-
-// Swaps
-const onGoingSwaps = new Map();
-
-// We keep a reference to the router here.
-let demoRouter: VueRouter;
-
-// #region Main
-
-/**
- * Initializes the demo environment and sets up various routes, data, and watchers.
- */
-export function dangerouslyInitializeDemo(router: VueRouter) {
- // Check if demo is active according to the configuration
- if (!checkIfDemoIsActive()) {
- console.info('[Demo] Demo mode not enabled in configuration. Skipping initialization.');
- return;
- }
-
- console.warn('[Demo] Initializing demo environment...');
-
- demoRouter = router;
-
- insertCustomDemoStyles();
- setupSingleMutationObserver();
- addDemoModalRoutes();
- interceptFetchRequest();
-
- setupDemoAddresses();
- setupDemoAccount();
-
- insertFakeNimTransactions();
- insertFakeBtcTransactions();
-
- if (useConfig().config.polygon.enabled) {
- insertFakeUsdcTransactions();
- insertFakeUsdtTransactions();
- }
-
- attachIframeListeners();
- replaceStakingFlow();
- replaceBuyNimFlow();
-
- listenForSwapChanges();
-}
-
-// #endregion
-
-// #region App setup
-
-/**
- * Checks if the demo mode should be active.
- * In demo builds, this always returns true since demo builds are separate deployments.
- */
-export function checkIfDemoIsActive() {
- return true;
-}
-
-/**
- * Creates a style tag to add demo-specific CSS.
- */
-function insertCustomDemoStyles() {
- const styleElement = document.createElement('style');
- styleElement.innerHTML = demoCSS;
- document.head.appendChild(styleElement);
-}
-
-/**
- * Sets up a single mutation observer to handle all DOM-based demo features
- */
-function setupSingleMutationObserver() {
- // Track processed elements to avoid duplicate processing
- const processedElements = new WeakSet();
-
- // Handler function to process all DOM mutations
- const processDomChanges = () => {
- // Call each handler with the processed elements set
- setupVisualCues(processedElements);
- disableSwapTriggers(processedElements);
- enableSellAndSwapModals(processedElements);
- obfuscateAddresses(processedElements);
- observeTransactionList(processedElements);
- observeReceiveModal(processedElements);
- };
-
- // Create one mutation observer for all DOM modifications
- const mutationObserver = new MutationObserver(processDomChanges);
-
- // Observe the entire document with a single observer
- mutationObserver.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['disabled'],
- });
-
- processDomChanges();
- // Also run checks when routes change to catch elements that appear after navigation
- demoRouter.afterEach(() => {
- // Wait for Vue to update the DOM after route change
- setTimeout(processDomChanges, 100);
- });
-}
-
-/**
- * Observes the home view to attach a highlight to some buttons for demonstration purposes.
- */
-function setupVisualCues(processedElements: WeakSet) {
- const highlightTargets = [
- ['.sidebar .trade-actions button', { top: '-18px', right: '-4px' }],
- ['.sidebar .swap-tooltip button', { top: '-18px', right: '-8px' }],
- ['.actions .staking-button', { top: '-2px', right: '-2px' }],
- ] as const;
-
- highlightTargets.forEach(([selector, position]) => {
- const target = document.querySelector(selector);
- if (!target || processedElements.has(target) || target.querySelector('.demo-highlight-badge')) return;
-
- const wrapper = document.createElement('div');
- wrapper.classList.add('demo-highlight-badge');
- wrapper.style.top = position.top;
- wrapper.style.right = position.right;
- const circle = document.createElement('div');
-
- wrapper.appendChild(circle);
- target.appendChild(wrapper);
- processedElements.add(target);
- });
-}
-
-/**
- * The only swap allowed is NIM-BTC.
- * Removes the swap triggers from the account grid so the user does not the
- * option to swap or sell assets in the demo environment.
- * We also remove the pair selection in the SwapModal.
- */
-function disableSwapTriggers(processedElements: WeakSet) {
- const swapTriggers = document.querySelectorAll(
- '.account-grid > :where(.nim-usdc-swap-button, .nim-btc-swap-button, .btc-usdc-swap-button, .account-backgrounds)',
- ) as NodeListOf;
-
- swapTriggers.forEach((trigger) => {
- if (!processedElements.has(trigger)) {
- trigger.remove();
- processedElements.add(trigger);
- }
- });
-
- const pairSelection = document.querySelector('.pair-selection');
- if (pairSelection && !processedElements.has(pairSelection)) {
- pairSelection.remove();
- processedElements.add(pairSelection);
- }
-}
-
-/**
- * Adds routes pointing to our demo modals.
- */
-function addDemoModalRoutes() {
- demoRouter.addRoute('root', {
- name: DemoModal.Fallback,
- path: `/${DemoModal.Fallback}`,
- components: { modal: DemoFallbackModal },
- props: { modal: true },
- });
- demoRouter.addRoute('root', {
- name: DemoModal.Buy,
- path: `/${DemoModal.Buy}`,
- components: { modal: DemoPurchaseModal },
- props: { modal: true },
- });
-}
-
-// #endregion
-
-// #region Addresses Setup
-
-/**
- * Setup and initialize the demo data for all currencies.
- */
-function setupDemoAddresses() {
- const { setAddressInfos } = useAddressStore();
- setAddressInfos([
- {
- label: 'Demo Account',
- type: AddressType.BASIC,
- address: demoNimAddress,
- balance: nimInitialBalance,
- },
- ]);
-
- // Setup Polygon addresses and balances
- const { setAddressInfos: setPolygonAddressInfos } = usePolygonAddressStore();
- setPolygonAddressInfos([{
- address: demoPolygonAddress,
- balanceUsdc: usdcInitialBalance,
- balanceUsdcBridged: 0,
- balanceUsdtBridged: usdtInitialBalance,
- pol: 1,
- }]);
-}
-
-/**
- * Creates a fake main account referencing our demo addresses.
- */
-function setupDemoAccount() {
- const { addAccountInfo, setActiveCurrency } = useAccountStore();
- const { setStablecoin, setKnowsAboutUsdt } = useAccountSettingsStore();
-
- // Setup account info with both USDC and USDT addresses
- addAccountInfo({
- id: 'demo-account-1',
- type: AccountType.BIP39,
- label: 'Demo Main Account',
- fileExported: true,
- wordsExported: true,
- addresses: [demoNimAddress],
- btcAddresses: { internal: [demoBtcAddress], external: [demoBtcAddress] },
- polygonAddresses: [demoPolygonAddress, demoPolygonAddress],
- uid: 'demo-uid-1',
- });
-
- // Pre-select USDC as the default stablecoin and mark USDT as known
- setStablecoin(CryptoCurrency.USDC);
- setKnowsAboutUsdt(true);
-
- setActiveCurrency(CryptoCurrency.NIM);
-}
-
-enum MessageEventName {
- FlowChange = 'FlowChange'
-}
-
-/**
- * Listens for messages from iframes (or parent frames) about changes in the user flow.
- */
-function attachIframeListeners() {
- window.addEventListener('message', (event) => {
- if (!event.data || typeof event.data !== 'object') return;
- const { kind, data } = event.data as DemoFlowMessage;
- if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
- useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
-
- demoRouter.push({
- path: demoRoutes[data],
- });
- }
- });
-
- demoRouter.afterEach((to) => {
- const match = Object.entries(demoRoutes).find(([, route]) => route === to.path);
- if (!match) return;
- window.parent.postMessage({ kind: MessageEventName.FlowChange, data: match[0] as DemoFlowType }, '*');
- });
-}
-
-// #endregion
-
-// #region NIM txs
-
-interface NimTransactionDefinition {
- fraction: number;
- daysAgo: number;
- description: string;
- recipientLabel?: string;
-}
-
-/**
- * Defines transaction definitions for demo NIM transactions
- */
-function defineNimFakeTransactions(): Partial[] {
- const txDefinitions: NimTransactionDefinition[] = [
- { fraction: -0.05, daysAgo: 0.4, description: 'Local cafe coffee' },
- { fraction: 0.1, daysAgo: 0.6, description: 'Red envelope from neighbor', recipientLabel: 'Neighbor' },
- { fraction: -0.03, daysAgo: 1, description: 'Food truck snack' },
- { fraction: -0.06, daysAgo: 2, description: 'Local store book' },
- { fraction: -0.01, daysAgo: 2, description: 'Chicken waffle', recipientLabel: 'Roberto\'s Waffle' },
- { fraction: -0.04, daysAgo: 3, description: 'Corner shop chai & snack' },
- { fraction: -0.12, daysAgo: 3, description: 'Thai massage session' },
- { fraction: -0.15, daysAgo: 6, description: 'Swedish flat-pack chair', recipientLabel: 'Furniture Mart' },
- { fraction: 0.1, daysAgo: 6, description: 'Red envelope from family', recipientLabel: 'Family' },
- { fraction: -0.08, daysAgo: 7, description: 'Cozy diner dinner', recipientLabel: 'Melissa' },
- { fraction: -0.02, daysAgo: 7, description: 'Coworker snack', recipientLabel: 'Coworker' },
- { fraction: -0.03, daysAgo: 8, description: 'Morning bus fare' },
- { fraction: -0.05, daysAgo: 8, description: 'Local fruit pack' },
- { fraction: 0.02, daysAgo: 10, description: 'Neighbor bill', recipientLabel: 'Neighbor' },
- { fraction: -0.07, daysAgo: 12, description: 'Movie ticket night' },
- { fraction: -0.04, daysAgo: 14, description: 'Trendy cafe coffee', recipientLabel: 'Cafe' },
- { fraction: -0.03, daysAgo: 15, description: 'Market street food snack' },
- { fraction: -0.1, daysAgo: 18, description: '' },
- { fraction: -0.05, daysAgo: 20, description: 'Street vendor souvenir' },
- { fraction: -0.08, daysAgo: 22, description: 'Quick haircut', recipientLabel: 'Barber' },
- { fraction: -0.04, daysAgo: 25, description: 'Local dessert', recipientLabel: 'Jose' },
- { fraction: -0.02, daysAgo: 27, description: 'Mall parking' },
- { fraction: -0.1, daysAgo: 30, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
- { fraction: 0.03, daysAgo: 32, description: 'Mistaken charge refund', recipientLabel: 'Store' },
- { fraction: -0.06, daysAgo: 35, description: 'Taxi fare', recipientLabel: 'Crazy Taxi' },
- { fraction: -0.04, daysAgo: 38, description: 'Local shop mug' },
- { fraction: -0.01, daysAgo: 40, description: 'Local newspaper' },
- { fraction: -0.05, daysAgo: 42, description: 'Coworker ride', recipientLabel: 'Coworker' },
- { fraction: -0.07, daysAgo: 45, description: 'Bistro lunch' },
- { fraction: -0.12, daysAgo: 45, description: 'Weekly market shopping', recipientLabel: 'Market' },
- { fraction: -0.1, daysAgo: 50, description: 'Utility bill', recipientLabel: 'Utility Co.' },
- { fraction: -0.03, daysAgo: 55, description: 'Corner snack pack' },
- { fraction: -0.1, daysAgo: 60, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
- { fraction: 0.05, daysAgo: 62, description: 'Client tip', recipientLabel: 'Client' },
- { fraction: -0.06, daysAgo: 65, description: 'Hair trim', recipientLabel: 'Barber' },
- { fraction: -0.09, daysAgo: 68, description: 'Takeaway meal' },
- { fraction: -0.04, daysAgo: 70, description: 'Stall fresh juice' },
- { fraction: -0.05, daysAgo: 72, description: 'Park picnic' },
- { fraction: -0.03, daysAgo: 75, description: 'Local event fee', recipientLabel: 'Event Org' },
- { fraction: -0.1, daysAgo: 78, description: 'Neighbors dinner', recipientLabel: 'Neighbors' },
- { fraction: -0.12, daysAgo: 80, description: 'New shoes sale' },
- { fraction: 0.1, daysAgo: 85, description: 'Festive cash gift', recipientLabel: 'Family' },
- { fraction: -0.1, daysAgo: 90, description: 'Streaming subscription', recipientLabel: 'StreamCo' },
- { fraction: -0.05, daysAgo: 95, description: 'Bakery fresh bread', recipientLabel: 'Bakery' },
- { fraction: -0.04, daysAgo: 100, description: 'Ice cream treat', recipientLabel: 'Ice Cream' },
- { fraction: -0.08, daysAgo: 110, description: 'Fitness class fee', recipientLabel: 'Gym' },
- { fraction: -0.03, daysAgo: 115, description: 'Meal discount' },
- { fraction: 0.04, daysAgo: 120, description: 'Double charge refund', recipientLabel: 'Store' },
- { fraction: -0.07, daysAgo: 125, description: 'Boutique trendy hat' },
- { fraction: -0.02, daysAgo: 130, description: 'Local cause donation', recipientLabel: 'Charity' },
- { fraction: -0.09, daysAgo: 140, description: 'Neighborhood dinner', recipientLabel: 'Food Joint' },
- { fraction: -0.05, daysAgo: 150, description: 'Gadget repair fee', recipientLabel: 'Repair Shop' },
- { fraction: -0.08, daysAgo: 200, description: 'Local play ticket', recipientLabel: 'Theater' },
- { fraction: -0.1, daysAgo: 250, description: 'Community event bill', recipientLabel: 'Community' },
- { fraction: 0.07, daysAgo: 300, description: 'Work bonus', recipientLabel: 'Employer' },
- { fraction: -0.04, daysAgo: 400, description: 'Local art fair entry' },
- { fraction: -0.06, daysAgo: 500, description: 'Online shop gadget', recipientLabel: 'Online Shop' },
- { fraction: -0.1, daysAgo: 600, description: 'Popular local dinner', recipientLabel: 'Diner' },
- { fraction: -0.12, daysAgo: 800, description: 'Home repair bill', recipientLabel: 'Repair Co.' },
- { fraction: 0.15, daysAgo: 1000, description: 'Freelance project check', recipientLabel: 'Client' },
- { fraction: -0.09, daysAgo: 1200, description: 'Local kitchen gear', recipientLabel: 'Kitchen Shop' },
- { fraction: -0.1, daysAgo: 1442, description: 'Family reunion dinner', recipientLabel: 'Family' },
-
- ];
-
- // Calculate sum of existing transactions to ensure they add up to exactly 1
- const existingSum = txDefinitions.reduce((sum, def) => sum + def.fraction, 0);
- const remainingFraction = 1 - existingSum;
-
- // Add the final balancing transaction with appropriate description
- if (Math.abs(remainingFraction) > 0.001) { // Only add if there's a meaningful amount to balance
- txDefinitions.push({
- fraction: remainingFraction,
- daysAgo: 1450,
- description: remainingFraction > 0
- ? 'Blockchain Hackathon Prize!'
- : 'Annual Software Subscription Renewal',
- recipientLabel: remainingFraction > 0 ? 'Crypto Innovation Fund' : undefined,
- });
- }
-
- const { setContact } = useContactsStore();
- const txs: Partial[] = [];
-
- for (const def of txDefinitions) {
- let txValue = Math.floor(nimInitialBalance * def.fraction);
- // Adjust so it doesn't end in a 0 digit
- while (txValue > 0 && txValue % 10 === 0) {
- txValue -= 1;
- }
-
- const hex = encodeTextToHex(def.description);
- const to32Bytes = (h: string) => h.padStart(64, '0').slice(-64);
- const address = KeyPair.derive(PrivateKey.fromHex(to32Bytes(hex))).toAddress().toUserFriendlyAddress();
- const recipient = def.fraction > 0 ? demoNimAddress : address;
- const sender = def.fraction > 0 ? address : demoNimAddress;
- const data = { type: 'raw', raw: hex } as const;
- const value = Math.abs(txValue);
- const timestamp = calculateDaysAgo(def.daysAgo);
- const tx: Partial = { value, recipient, sender, timestamp, data };
- txs.push(tx);
- // Add contact if a recipientLabel is provided
- if (def.recipientLabel && def.fraction < 0) {
- setContact(address, def.recipientLabel);
- }
- }
-
- return txs.sort((a, b) => a.timestamp! - b.timestamp!);
-}
-
-let head = 0;
-let nonce = 0;
-
-function transformNimTransaction(txs: Partial[]): NimTransaction[] {
- return txs.map((tx) => {
- head++;
- nonce++;
-
- return {
- network: 'mainnet',
- state: 'confirmed',
- transactionHash: `0x${nonce.toString(16)}`,
- sender: '',
- senderType: 'basic',
- recipient: '',
- recipientType: 'basic',
- value: 50000000,
- fee: 0,
- feePerByte: 0,
- format: 'basic',
- validityStartHeight: head,
- blockHeight: head,
- flags: 0,
- timestamp: Date.now(),
- size: 0,
- valid: true,
- proof: { raw: '', type: 'raw' },
- data: tx.data || { type: 'raw', raw: '' },
- ...tx,
- };
- });
-}
-
-/**
- * Inserts NIM transactions into the store. If no definitions provided, uses default demo transactions.
- */
-function insertFakeNimTransactions(txs = defineNimFakeTransactions()) {
- const { addTransactions } = useTransactionsStore();
- addTransactions(transformNimTransaction(txs));
-}
-
-/**
- * Updates the NIM balance after a transaction
- */
-function updateNimBalance(amount: number): void {
- const addressStore = useAddressStore();
- const currentAddressInfo = addressStore.addressInfos.value.find((info) => info.address === demoNimAddress);
-
- if (currentAddressInfo) {
- const newBalance = (currentAddressInfo.balance || 0) + amount;
- addressStore.patchAddress(demoNimAddress, { balance: newBalance });
- } else {
- console.error('[Demo] Failed to update NIM balance: Address not found');
- }
-}
-
-export function dangerouslyInsertFakeBuyNimTransaction(amount: number) {
- const tx: Partial = {
- value: amount,
- recipient: demoNimAddress,
- sender: buyFromAddress,
- data: {
- type: 'raw',
- raw: encodeTextToHex('NIM Bank purchase'),
- },
- };
-
- setTimeout(() => {
- const { addTransactions } = useTransactionsStore();
- addTransactions(transformNimTransaction([tx]));
- updateNimBalance(amount);
- }, 1_500);
-}
-
-// #region BTC txs
-
-interface BtcTransactionDefinition {
- fraction: number;
- daysAgo: number;
- description: string;
- recipientLabel?: string;
- incoming: boolean;
- address: string;
-}
-
-/**
- * Defines transaction definitions for demo BTC transactions.
- * Note: We add the "address" field so that incoming transactions use the correct sender address.
- */
-function defineBtcFakeTransactions(): BtcTransaction[] {
- const txDefinitions: BtcTransactionDefinition[] = [
- {
- fraction: 0.2,
- daysAgo: 2000,
- description: 'Initial BTC purchase from exchange',
- incoming: true,
- address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
- recipientLabel: 'Satoshi Exchange',
- },
- {
- fraction: 0.15,
- daysAgo: 1600,
- description: 'Mining pool payout',
- incoming: true,
- address: '1Hz7vQrRjnu3z9k7gxDYhKjEmABqChDvJr',
- recipientLabel: 'Genesis Mining Pool',
- },
- {
- fraction: -0.19,
- daysAgo: 1200,
- description: 'Purchase from online marketplace',
- incoming: false,
- address: '1LxKe5kKdgGVwXukEgqFxh6DrCXF2Pturc',
- recipientLabel: 'Digital Bazaar Shop',
- },
- {
- fraction: 0.3,
- daysAgo: 800,
- description: 'Company payment for consulting',
- incoming: true,
- address: '1N7aecJuKGDXzYK8CgpnNRYxdhZvXPxp3B',
- recipientLabel: 'Corporate Treasury',
- },
- {
- fraction: -0.15,
- daysAgo: 365,
- description: 'Auto-DCA investment program',
- incoming: false,
- address: '12vxjmKJkfL9s5JwqUzEVVJGvKYJgALbsz',
- },
- {
- fraction: 0.075,
- daysAgo: 180,
- description: 'P2P sale of digital goods',
- incoming: true,
- address: '1MZYS9nvVmFvSK7em5zzAsnvRq82RUcypS',
- },
- {
- fraction: 0.05,
- daysAgo: 60,
- description: 'Recent purchase from exchange',
- incoming: true,
- address: '1Kj4SNWFCxqvtP8nkJxeBwkXxgY9LW9rGg',
- },
- ].sort((a, b) => b.daysAgo - a.daysAgo);
-
- // If the sum of fractions does not add up to 1, add a balancing transaction.
- const existingSum = txDefinitions.reduce((sum, def) =>
- sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
- const remainingFraction = 1 - existingSum;
- if (Math.abs(remainingFraction) > 0.001) {
- txDefinitions.push({
- fraction: remainingFraction,
- daysAgo: 0,
- description: remainingFraction > 0 ? 'Initial purchase' : 'Investment allocation',
- incoming: remainingFraction > 0,
- address: remainingFraction > 0 ? '1ExampleAddressForInitialPurchase' : '1ExampleAddressForInvestment',
- recipientLabel: remainingFraction > 0 ? 'Prime Exchange' : 'Investment Fund',
- });
- }
-
- return transformBtcTransaction(txDefinitions);
-}
-
-/**
- * Transforms BTC transaction definitions into actual transactions.
- */
-function transformBtcTransaction(txDefinitions: BtcTransactionDefinition[]): BtcTransaction[] {
- const transactions = [];
- const knownUtxos = new Map();
- let txCounter = 1;
-
- for (const def of txDefinitions) {
- const txHash = `btc-tx-${txCounter++}`;
- // Compute the value using the initial BTC balance and the fraction.
- const value = Math.floor(btcInitialBalance * Math.abs(def.fraction));
-
- const tx: BtcTransaction = {
- addresses: [demoBtcAddress],
- isCoinbase: false,
- inputs: [
- {
- // For incoming tx, use the external address; for outgoing, use our demo address.
- address: def.incoming ? def.address : demoBtcAddress,
- outputIndex: 0,
- index: 0,
- script: 'script',
- sequence: 4294967295,
- transactionHash: def.incoming ? txHash : (getUTXOToSpend(knownUtxos)?.txHash || txHash),
- witness: ['witness'],
- },
- ],
- outputs: def.incoming
- ? [
- {
- value,
- address: demoBtcAddress,
- script: 'script',
- index: 0,
- },
- ]
- : [
- {
- value,
- address: def.address,
- script: 'script',
- index: 0,
- },
- {
- // Change output.
- value: 1000000,
- address: demoBtcAddress,
- script: 'script',
- index: 1,
- },
- ],
- transactionHash: txHash,
- version: 1,
- vsize: 200,
- weight: 800,
- locktime: 0,
- confirmations: Math.max(1, Math.floor(10 - def.daysAgo / 200)),
- replaceByFee: false,
- timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
- state: ElectrumTransactionState.CONFIRMED,
- };
-
- updateUTXOs(knownUtxos, tx);
- transactions.push(tx);
-
- // Set labels if a recipient label is provided.
- if (def.recipientLabel) {
- const { setSenderLabel } = useBtcLabelsStore();
- setSenderLabel(def.incoming ? def.address : demoBtcAddress, def.recipientLabel);
- }
- }
-
- // Update the address store with the current UTXOs.
- const { addAddressInfos } = useBtcAddressStore();
- addAddressInfos([{
- address: demoBtcAddress,
- txoCount: transactions.length + 2, // Total number of outputs received.
- utxos: Array.from(knownUtxos.values()),
- }]);
-
- return transactions;
-}
-
-/**
- * Tracks UTXO changes for BTC transactions.
- */
-function updateUTXOs(knownUtxos: Map, tx: any) {
- // Remove spent inputs.
- for (const input of tx.inputs) {
- if (input.address === demoBtcAddress) {
- const utxoKey = `${input.transactionHash}:${input.outputIndex}`;
- knownUtxos.delete(utxoKey);
- }
- }
-
- // Add new outputs for our address.
- for (const output of tx.outputs) {
- if (output.address === demoBtcAddress) {
- const utxoKey = `${tx.transactionHash}:${output.index}`;
- knownUtxos.set(utxoKey, {
- transactionHash: tx.transactionHash,
- index: output.index,
- witness: {
- script: output.script,
- value: output.value,
- },
- });
- }
- }
-}
-
-/**
- * Helper to get a UTXO to spend.
- */
-function getUTXOToSpend(knownUtxos: Map) {
- if (knownUtxos.size === 0) return null;
- const utxo = knownUtxos.values().next().value;
- return {
- txHash: utxo.transactionHash,
- index: utxo.index,
- value: utxo.witness.value,
- };
-}
-
-/**
- * Insert fake BTC transactions into the store.
- */
-function insertFakeBtcTransactions(txs = defineBtcFakeTransactions()) {
- const { addTransactions } = useBtcTransactionsStore();
- addTransactions(txs);
-}
-
-/**
- * Updates the BTC address balance by adding a new UTXO
- */
-function updateBtcBalance(amount: number): void {
- const btcAddressStore = useBtcAddressStore();
- const addressInfo = btcAddressStore.state.addressInfos[demoBtcAddress];
-
- if (!addressInfo) {
- console.error('[Demo] Failed to update BTC balance: Address not found');
- return;
- }
-
- // Create a unique transaction hash
- const txHash = `btc-tx-swap-${Date.now().toString(16)}`;
-
- // Create a proper UTXO with the correct format
- const newUtxo = {
- transactionHash: txHash,
- index: 0,
- witness: {
- script: 'script',
- value: amount * 1e-6,
- },
- txoValue: amount * 1e-6,
- };
-
- btcAddressStore.addAddressInfos([{
- address: demoBtcAddress,
- utxos: [...(addressInfo.utxos || []), newUtxo],
- }]);
-
- console.log(`[Demo] Updated BTC balance, added UTXO with value: ${amount * 1e-6}`);
-}
-
-// #endregion
-
-// #region Polygon txs
-
-const getRandomPolygonHash = () => `0x${Math.random().toString(16).slice(2, 66)}`;
-const getRandomPolygonAddress = () => `0x${Math.random().toString(16).slice(2, 42)}`;
-
-interface UsdcTransactionDefinition {
- fraction: number;
- daysAgo: number;
- description: string;
- recipientLabel?: string;
- incoming: boolean;
-}
-
-/**
- * Defines transaction definitions for demo USDC transactions
- */
-function defineUsdcFakeTransactions(): UsdcTransaction[] {
- const txDefinitions: UsdcTransactionDefinition[] = [
- { fraction: 0.3, daysAgo: 0, description: 'DeFi yield harvest', incoming: true, recipientLabel: 'Yield Protocol' },
- { fraction: -0.1, daysAgo: 2, description: 'NFT marketplace purchase', incoming: false, recipientLabel: 'NFT Market' },
- { fraction: 0.2, daysAgo: 5, description: 'Bridge transfer', incoming: true, recipientLabel: 'Polygon Bridge' },
- { fraction: -0.15, daysAgo: 10, description: 'DEX liquidity provision', incoming: false },
- { fraction: 0.25, daysAgo: 20, description: 'Staking rewards', incoming: true, recipientLabel: 'Staking Pool' },
- { fraction: -0.12, daysAgo: 30, description: 'GameFi item purchase', incoming: false },
- { fraction: 0.3, daysAgo: 45, description: 'DAO compensation', incoming: true, recipientLabel: 'Treasury DAO' },
- { fraction: -0.2, daysAgo: 60, description: 'Lending deposit', incoming: false, recipientLabel: 'Lending Protocol' },
- ];
-
- const existingSum = txDefinitions.reduce((sum, def) =>
- sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
- const remainingFraction = 1 - existingSum;
-
- if (Math.abs(remainingFraction) > 0.001) {
- txDefinitions.push({
- fraction: remainingFraction,
- daysAgo: remainingFraction > 0 ? 90 : 100,
- description: remainingFraction > 0 ? 'Initial bridge deposit' : 'Protocol investment',
- incoming: remainingFraction > 0,
- recipientLabel: remainingFraction > 0 ? 'Bridge Service' : 'DeFi Protocol',
- });
- }
-
- return transformUsdcTransaction(txDefinitions);
-}
-
-/**
- * Transform USDC transaction definitions into actual transactions
- */
-function transformUsdcTransaction(txDefinitions: UsdcTransactionDefinition[]): UsdcTransaction[] {
- return txDefinitions.map((def, index) => {
- const value = Math.floor(usdcInitialBalance * Math.abs(def.fraction));
- const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
- const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
-
- if (def.recipientLabel) {
- const { setContact } = useUsdcContactsStore();
- setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
- }
-
- return {
- token: Config.polygon.usdc.tokenContract,
- transactionHash: getRandomPolygonHash(),
- logIndex: index,
- sender: randomAddress,
- recipient: recipientAddress,
- value,
- state: UsdcTransactionState.CONFIRMED,
- blockHeight: 1000000 + index,
- timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
- };
- });
-}
-
-/**
- * Insert fake USDC transactions into the store
- */
-function insertFakeUsdcTransactions(txs = defineUsdcFakeTransactions()) {
- const { addTransactions } = useUsdcTransactionsStore();
- addTransactions(txs);
-}
-
-interface UsdtTransactionDefinition {
- fraction: number;
- daysAgo: number;
- description: string;
- recipientLabel?: string;
- incoming: boolean;
-}
-
-/**
- * Defines transaction definitions for demo USDT transactions
- */
-function defineUsdtFakeTransactions(): UsdtTransaction[] {
- const txDefinitions: UsdtTransactionDefinition[] = [
- { fraction: 0.25, daysAgo: 0, description: 'Trading profit', incoming: true, recipientLabel: 'DEX Trading' },
- { fraction: -0.08, daysAgo: 3, description: 'Merchandise payment', incoming: false },
- { fraction: 0.15, daysAgo: 7, description: 'Freelance payment', incoming: true, recipientLabel: 'Client Pay' },
- { fraction: -0.12, daysAgo: 15, description: 'Service subscription', incoming: false, recipientLabel: 'Web3 Service' },
- { fraction: 0.3, daysAgo: 25, description: 'P2P exchange', incoming: true },
- { fraction: -0.18, daysAgo: 35, description: 'Platform fees', incoming: false },
- { fraction: 0.2, daysAgo: 50, description: 'Revenue share', incoming: true, recipientLabel: 'Revenue Pool' },
- { fraction: -0.15, daysAgo: 70, description: 'Marketing campaign', incoming: false, recipientLabel: 'Marketing DAO' },
- ];
-
- const existingSum = txDefinitions.reduce((sum, def) =>
- sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
- const remainingFraction = 1 - existingSum;
-
- if (Math.abs(remainingFraction) > 0.001) {
- txDefinitions.push({
- fraction: remainingFraction,
- daysAgo: remainingFraction > 0 ? 85 : 95,
- description: remainingFraction > 0 ? 'Initial USDT deposit' : 'Portfolio rebalance',
- incoming: remainingFraction > 0,
- recipientLabel: remainingFraction > 0 ? 'Bridge Protocol' : 'Portfolio Manager',
- });
- }
-
- return transformUsdtTransaction(txDefinitions);
-}
-
-/**
- * Transform USDT transaction definitions into actual transactions
- */
-function transformUsdtTransaction(txDefinitions: UsdtTransactionDefinition[]): UsdtTransaction[] {
- return txDefinitions.map((def, index) => {
- const value = Math.floor(usdtInitialBalance * Math.abs(def.fraction));
- const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
- const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
-
- if (def.recipientLabel) {
- const { setContact } = useUsdtContactsStore();
- setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
- }
-
- return {
- token: Config.polygon.usdt_bridged.tokenContract,
- transactionHash: getRandomPolygonHash(),
- logIndex: index,
- sender: randomAddress,
- recipient: recipientAddress,
- value,
- state: UsdtTransactionState.CONFIRMED,
- blockHeight: 1000000 + index,
- timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
- };
- });
-}
-
-/**
- * Insert fake USDT transactions into the store
- */
-function insertFakeUsdtTransactions(txs = defineUsdtFakeTransactions()) {
- const { addTransactions } = useUsdtTransactionsStore();
- addTransactions(txs);
-}
-
-// #endregion
-
-// #region Flows
-
-/**
- * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
- */
-function replaceBuyNimFlow() {
- let observedTarget: HTMLDivElement | undefined;
-
- demoRouter.afterEach((to) => {
- if (to.path.startsWith('/')) {
- const targetSelector = '.sidebar .trade-actions';
- const checkForTradeActions = setInterval(() => {
- const target = document.querySelector(targetSelector) as HTMLDivElement;
- if (!target || target === observedTarget) return;
- observedTarget = target;
-
- target.innerHTML = '';
-
- const btn1 = document.createElement('button');
- btn1.className = 'nq-button-s inverse';
- btn1.style.flex = '1';
- btn1.addEventListener('click', () => {
- demoRouter.push({
- path: '/buy',
- });
- });
- btn1.innerHTML = 'Buy';
-
- const btn2 = document.createElement('button');
- btn2.className = 'nq-button-s inverse';
- btn2.style.flex = '1';
- btn2.disabled = true;
- btn2.innerHTML = 'Sell';
-
- target.appendChild(btn1);
- target.appendChild(btn2);
- }, 500);
-
- // Clear interval when navigating away
- demoRouter.afterEach((nextTo) => {
- if (!nextTo.path.startsWith('/')) {
- clearInterval(checkForTradeActions);
- observedTarget = undefined;
- }
- });
- }
- });
-}
-
-/**
- * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
- */
-function replaceStakingFlow() {
- let lastProcessedButton: HTMLButtonElement;
-
- demoRouter.afterEach((to) => {
- if (to.path === '/staking') {
- const checkForStakeButton = setInterval(() => {
- const target = document.querySelector('.stake-graph-page .stake-button');
- if (!target || target === lastProcessedButton) return;
-
- // remove previous listeners by cloning the element and replacing the original
- const newElement = target.cloneNode(true) as HTMLButtonElement;
- target.parentNode!.replaceChild(newElement, target);
- newElement.removeAttribute('disabled');
- lastProcessedButton = newElement;
-
- newElement.addEventListener('click', async () => {
- const { setStake } = useStakingStore();
- const { activeValidator } = useStakingStore();
- const amountInput = document.querySelector('.nq-input') as HTMLInputElement;
- const amount = Number.parseFloat(amountInput.value.replaceAll(/[^\d]/g, '')) * 1e5;
-
- const { address: validatorAddress } = activeValidator.value!;
-
- demoRouter.push({
- path: '/',
- });
-
- await new Promise((resolve) => { window.setTimeout(resolve, 100); });
- setStake({
- activeBalance: 0,
- inactiveBalance: amount,
- address: demoNimAddress,
- retiredBalance: 0,
- validator: validatorAddress,
- });
- const stakeTx: Partial = {
- value: amount,
- recipient: validatorAddress,
- sender: demoNimAddress,
- timestamp: Date.now(),
- data: { type: 'add-stake', raw: '', staker: demoNimAddress },
- };
- insertFakeNimTransactions([stakeTx]);
- });
- }, 500);
-
- // Clear interval when navigating away
- demoRouter.afterEach((nextTo) => {
- if (nextTo.path !== '/staking') {
- clearInterval(checkForStakeButton);
- }
- });
- }
- });
-}
-
-/**
- * Returns the hex encoding of a UTF-8 string.
- */
-function encodeTextToHex(text: string): string {
- const utf8Array = Utf8Tools.stringToUtf8ByteArray(text);
- return Array.from(utf8Array)
- .map((byte) => byte.toString(16).padStart(2, '0'))
- .join('');
-}
-
-// We pick a random but fixed time-of-day offset for each day.
-const baseDate = new Date();
-baseDate.setHours(0, 0, 0, 0);
-const baseDateMs = baseDate.getTime();
-const oneDayMs = 24 * 60 * 60 * 1000;
-
-/**
- * Generates a past timestamp for a given number of days ago, adding a predictable random offset.
- */
-function calculateDaysAgo(days: number): number {
- const x = Math.sin(days) * 10000;
- const fractionalPart = x - Math.floor(x);
- const randomPart = Math.floor(fractionalPart * oneDayMs);
- return baseDateMs - days * oneDayMs - randomPart;
-}
-
-/**
- * Flow type for our demo environment, e.g. buy, swap, stake.
- */
-type DemoFlowType = 'buy' | 'swap' | 'stake';
-
-/**
- * The expected message structure for flow-change events between frames.
- */
-type DemoFlowMessage = { kind: 'FlowChange', data: DemoFlowType };
-
-/**
- * Maps each flow type to a specific route path in our app.
- */
-const demoRoutes: Record = {
- buy: '/buy',
- swap: '/swap/NIM-BTC',
- stake: '/staking',
-};
-
-/**
- * CSS for the special demo elements, stored in a normal string (no backticks).
- */
-const demoCSS = `
-.transaction-list .month-label > :where(.fetching, .failed-to-fetch) {
- display: none;
-}
-
-/* Hide address */
-.active-address .meta .copyable {
- display: none !important;
-}
-
-#app > div > .wallet-status-button.nq-button-pill {
- display: none;
-}
-
-.staking-button .tooltip.staking-feature-tip {
- display: none;
-}
-
-.modal.transaction-modal .confirmed .tooltip.info-tooltip {
- display: none;
-}
-
-.send-modal-footer .footer-notice {
- display: none;
-}
-
-/* Demo address tooltip styling */
-.tooltip.demo-tooltip {
- width: max-content;
- background: var(--nimiq-orange-bg);
- margin-left: -7rem;
-}
-
-.tooltip.demo-tooltip::after {
- background: #fc750c; /* Match the red theme for the demo warning */
-}
-
-.demo-highlight-badge {
- position: absolute;
- width: 34px;
- height: 34px;
- z-index: 5;
- pointer-events: none;
-}
-
-.demo-highlight-badge > div {
- position: relative;
- width: 100%;
- height: 100%;
- background: rgba(31, 35, 72, 0.1);
- border: 1.5px solid rgba(255, 255, 255, 0.5);
- border-radius: 50%;
- backdrop-filter: blur(3px);
-}
-
-.demo-highlight-badge > div::before {
- content: "";
- position: absolute;
- inset: 5px;
- background: rgba(31, 35, 72, 0.3);
- border: 2px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(3px);
- border-radius: 12px;
-}
-
-.demo-highlight-badge > div::after {
- content: "";
- position: absolute;
- inset: 11.6px;
- background: rgba(255, 255, 255);
- border-radius: 50%;
-}
-
-.send-modal-footer .footer-notice {
- display: none;
-}
-
-.account-grid > button.reset,
-.account-grid > .nimiq-account {
- background: #e7e8ea;
- border-radius: 8px;
- transition: background 0.2s var(--nimiq-ease);
-}
-
-.account-grid > button:where(:hover, :focus-visible) {
- background: #dedee2 !important;
-}
-
-/* Hide links and addresses to block explorer in the swap animation */
-.swap-animation :where(.short-address, .blue-link.nq-link) {
- display: none !important;
-}
-`;
-
-/**
- * Intercepts fetch request for swaps
- */
-function interceptFetchRequest() {
- const originalFetch = window.fetch;
- window.fetch = async (...args: Parameters) => {
- if (typeof args[0] !== 'string') return originalFetch(...args);
- if (args[0].startsWith('/')) return originalFetch(...args);
-
- const url = new URL(args[0] as string);
- const isFastspotRequest = url.host === (new URL(Config.fastspot.apiEndpoint).host);
- const isLimitsRequest = url.pathname.includes('/limits');
- const isAssetsRequest = url.pathname.includes('/assets');
- const isSwapRequest = url.pathname.includes('/swaps');
-
- // return originalFetch(...args);
- if (!isFastspotRequest) {
- return originalFetch(...args);
- }
-
- console.log('[Demo] Intercepted fetch request:', url.pathname);
-
- if (isLimitsRequest) {
- const constants = {
- current: '9800',
- daily: '50000',
- dailyRemaining: '49000',
- monthly: '100000',
- monthlyRemaining: '98000',
- swap: '10000',
- } as const;
-
- const [assetOrLimit] = url.pathname.split('/').slice(-2) as [SwapAsset | 'limits', string];
-
- if (assetOrLimit === 'limits') {
- const limits: FastspotUserLimits = {
- asset: ReferenceAsset.USD,
- ...constants,
- };
- return new Response(JSON.stringify(limits));
- }
-
- const asset = assetOrLimit as SwapAsset;
-
- const { exchangeRates, currency } = useFiatStore();
- const rate: number = exchangeRates.value[asset.toLocaleLowerCase().split('_')[0]][currency.value]!;
-
- const json: FastspotLimits = {
- asset,
- referenceAsset: ReferenceAsset.USD,
- referenceCurrent: constants.current,
- referenceDaily: constants.daily,
- referenceDailyRemaining: constants.dailyRemaining,
- referenceMonthly: constants.monthly,
- referenceMonthlyRemaining: constants.monthlyRemaining,
- referenceSwap: `${10000}`,
- current: `${Number(constants.current) / rate}`,
- daily: `${Number(constants.daily) / rate}`,
- dailyRemaining: `${Number(constants.dailyRemaining) / rate}`,
- monthly: `${Number(constants.monthly) / rate}`,
- monthlyRemaining: `${Number(constants.monthlyRemaining) / rate}`,
- swap: `${Number(constants.swap) / rate}`,
- };
-
- return new Response(JSON.stringify(json));
- }
-
- if (isAssetsRequest) {
- // Return mock assets data with fees for all supported assets
- const json: FastspotAsset[] = [
- {
- symbol: SwapAsset.BTC,
- name: 'Bitcoin',
- feePerUnit: `${getNetworkFeePerUnit(SwapAsset.BTC)}`,
- limits: { minimum: '0.0001', maximum: '1' },
- },
- {
- symbol: SwapAsset.NIM,
- name: 'Nimiq',
- feePerUnit: `${getNetworkFeePerUnit(SwapAsset.NIM)}`,
- limits: { minimum: '1', maximum: '100000' },
- },
- {
- symbol: SwapAsset.USDC_MATIC,
- name: 'USDC (Polygon)',
- feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDC_MATIC)}`,
- limits: { minimum: '1', maximum: '100000' },
- },
- {
- symbol: SwapAsset.USDT_MATIC,
- name: 'USDT (Polygon)',
- feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDT_MATIC)}`,
- limits: { minimum: '1', maximum: '100000' },
- },
- ];
-
- return new Response(JSON.stringify(json));
- }
-
- if (isSwapRequest) {
- const swapId = url.pathname.split('/').slice(-1)[0];
-
- if (swapId === 'swaps') {
- const { patchAccount, activeAccountInfo } = useAccountStore();
- const newBtcAddress = demoBtcAddress + Math.random().toString(36).slice(2, 9);
-
- patchAccount(activeAccountInfo.value?.id, {
- ...activeAccountInfo,
- btcAddresses: {
- external: [...(activeAccountInfo.value?.btcAddresses.external || []), newBtcAddress],
- internal: [...(activeAccountInfo.value?.btcAddresses.internal || []), newBtcAddress],
- },
- });
-
- const { addAddressInfos } = useBtcAddressStore();
- addAddressInfos([{
- address: newBtcAddress,
- txoCount: 0, // Total number of outputs received
- utxos: [],
- }]);
- } else {
- listenForSwapChanges();
-
- console.log('[Demo] Swap request:', swapId);
- const swap = onGoingSwaps.get(swapId);
- if (!swap) {
- return new Response(JSON.stringify({
- error: 'Swap not found',
- status: 404,
- }), { status: 404 });
- }
-
- console.log('[Demo] Swap:', swap);
- const expirationTimestamp = Math.floor(Date.now() / 1000) + 3600;
-
- return new Response(JSON.stringify({
- id: swapId,
- status: SwapStatus.WAITING_FOR_CONFIRMATION,
- expires: expirationTimestamp,
- info: {
- from: [
- {
- symbol: swap.fund.type,
- amount: swap.fund.output.value / 1e8,
- fundingNetworkFee: {
- total: '0.000037',
- perUnit: '0.000000240254',
- totalIsIncluded: true,
- },
- operatingNetworkFee: {
- total: '0',
- perUnit: '0.000000240254',
- totalIsIncluded: false,
- },
- finalizeNetworkFee: {
- total: '0.0000346',
- perUnit: '0.000000240254',
- totalIsIncluded: false,
- },
- },
- ],
- to: [
- {
- symbol: swap.redeem.type,
- amount: swap.redeem.value / 1e5,
- fundingNetworkFee: {
- total: '0',
- perUnit: '0',
- totalIsIncluded: false,
- },
- operatingNetworkFee: {
- total: '0',
- perUnit: '0',
- totalIsIncluded: false,
- },
- finalizeNetworkFee: {
- total: '0',
- perUnit: '0',
- totalIsIncluded: false,
- },
- },
- ],
- serviceFeePercentage: 0.0025,
- direction: 'reverse',
- },
- hash: '946dc06baf94ee49a1bd026eff8eb4f30d34c9e162211667dbebd5a5282e6294',
- contracts: [
- {
- asset: swap.fund.type,
- refund: {
- address: swap.fund.refundAddress,
- },
- recipient: {
- address: swap.fund.refundAddress,
- },
- amount: swap.fund.output.value,
- timeout: expirationTimestamp,
- direction: 'send',
- status: 'pending',
- id: '2MzQo4ehDrSEsxX7RnysLL6VePD3tuNyx4M',
- intermediary: {},
- },
- {
- asset: swap.redeem.type,
- refund: {
- address: swap.redeem.recipient,
- },
- recipient: {
- address: swap.redeem.recipient,
- },
- amount: swap.redeem.value,
- timeout: expirationTimestamp,
- direction: 'receive',
- status: 'pending',
- id: 'eff8a1a5-4f4e-3895-b95c-fd5a40c99001',
- intermediary: {},
- },
- ],
- }));
- }
- }
-
- return originalFetch(...args);
- };
-}
-
-/**
- * Fee per unit helper function
- */
-function getNetworkFeePerUnit(asset: string): number {
- switch (asset) {
- case SwapAsset.BTC:
- return Math.floor(Math.random() * 100) / 1e8; // 1 - 100 sats/vbyte
- case SwapAsset.NIM:
- return 0; // luna per byte
- case SwapAsset.USDC_MATIC:
- case SwapAsset.USDT_MATIC:
- return 1000000000; // 1 Gwei
- default:
- return 0;
- }
-}
-
-/**
- * Ensures the Send button in modals is always enabled in demo mode, regardless of network state.
- * This allows users to interact with the send functionality without waiting for network sync.
- */
-function enableSellAndSwapModals(processedElements: WeakSet) {
- // Target the send modal and swap footer button
- const bottomButton = document.querySelector('.send-modal-footer .nq-button');
- if (!bottomButton || processedElements.has(bottomButton)) return;
-
- if (bottomButton.hasAttribute('disabled')) {
- bottomButton.removeAttribute('disabled');
- bottomButton.classList.remove('disabled');
- processedElements.add(bottomButton);
-
- // Also find and hide any sync message if shown
- const footer = document.querySelector('.send-modal-footer');
- if (footer) {
- const footerNotice = footer.querySelector('.footer-notice') as HTMLDivElement;
- if (footerNotice && footerNotice.textContent
- && (footerNotice.textContent.includes('Connecting to Bitcoin')
- || footerNotice.textContent.includes('Syncing with Bitcoin'))) {
- footerNotice.style.display = 'none';
- processedElements.add(footerNotice);
- }
- }
- }
-}
-
-let swapInterval: NodeJS.Timeout | null = null;
-function listenForSwapChanges() {
- if (swapInterval) return;
- swapInterval = setInterval(() => {
- // Check if there are any active swaps that need to be processed
- const swap = useSwapsStore().activeSwap.value;
- if (!swap) return;
- console.log('[Demo] Active swap:', { swap, state: swap.state });
- switch (swap.state) {
- case SwapState.AWAIT_INCOMING:
- console.log('[Demo] Swap is in AWAIT_INCOMING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.CREATE_OUTGOING,
- });
- break;
- case SwapState.CREATE_OUTGOING:
- console.log('[Demo] Swap is in CREATE_OUTGOING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.AWAIT_SECRET,
- });
- break;
- case SwapState.AWAIT_SECRET:
- console.log('[Demo] Swap is in AWAIT_SECRET state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.SETTLE_INCOMING,
- });
- break;
- case SwapState.SETTLE_INCOMING:
- console.log('[Demo] Swap is in SETTLE_INCOMING state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.COMPLETE,
- });
- completeSwap(swap);
- break;
- case SwapState.COMPLETE:
- console.log('[Demo] Swap is in COMPLETE state');
- if (swapInterval) clearInterval(swapInterval);
- swapInterval = null;
- break;
- default:
- console.log('[Demo] Swap is in unknown state');
- useSwapsStore().setActiveSwap({
- ...swap,
- state: SwapState.AWAIT_INCOMING,
- });
- break;
- }
- }, 1_800);
-}
-
-/**
- * Completes an active swap by creating transactions for both sides of the swap
- */
-function completeSwap(activeSwap: any) {
- // Generate a unique hash for this swap to connect both sides
- const swapHash = `swap-${Math.random().toString(36).slice(2, 10)}`;
-
- // Add transactions for both sides of the swap
- const fromAsset = activeSwap.from.asset;
- const toAsset = activeSwap.to.asset;
- const fromAmount = activeSwap.from.amount;
- const toAmount = activeSwap.to.amount;
-
- const { setSwap } = useSwapsStore();
- const now = Date.now();
- const nowSecs = Math.floor(now / 1000);
-
- // Create outgoing transaction (from asset)
- switch (fromAsset) {
- case 'NIM': {
- // Create a unique transaction hash for the NIM transaction
- const nimTxHash = `nim-swap-${Math.random().toString(16).slice(2, 10)}`;
-
- // Create HTLC data that would be needed for a real swap
- const nimHtlcAddress = `NQ${Math.random().toString(36).slice(2, 34)}`;
- const btcAddress = `1${Math.random().toString(36).slice(2, 34)}`;
-
- // Register the swap with the Swaps store first
- setSwap(swapHash, {
- id: swapHash,
- in: {
- asset: SwapAsset.NIM,
- transactionHash: nimTxHash,
- htlc: {
- address: nimHtlcAddress,
- refundAddress: demoNimAddress,
- redeemAddress: btcAddress,
- timeoutMs: now + 3600000,
- },
- },
- out: {
- asset: SwapAsset.BTC,
- transactionHash: `btc-swap-${Math.random().toString(16).slice(2, 10)}`,
- outputIndex: 0,
- htlc: {
- address: `1HTLC${Math.random().toString(36).slice(2, 30)}`,
- script: 'btc-htlc-script',
- refundAddress: btcAddress,
- redeemAddress: demoBtcAddress,
- timeoutTimestamp: nowSecs + 7200,
- },
- },
- });
-
- // Create the NIM transaction with proper HTLC data
- const tx: Partial = {
- value: fromAmount,
- recipient: nimHtlcAddress,
- sender: demoNimAddress,
- timestamp: now,
- transactionHash: nimTxHash,
- data: {
- type: 'htlc',
- hashAlgorithm: 'sha256',
- hashCount: 1,
- hashRoot: swapHash,
- raw: encodeTextToHex(`Swap ${fromAsset}-${toAsset}`),
- recipient: nimHtlcAddress,
- sender: demoNimAddress,
- timeout: nowSecs + 3600,
- },
- };
-
- insertFakeNimTransactions(transformNimTransaction([tx]));
- updateNimBalance(-fromAmount);
- break;
- }
- case 'BTC': {
- // Create a unique transaction hash for the BTC transaction
- const btcTxHash = `btc-swap-${Math.random().toString(16).slice(2, 10)}`;
-
- // Create HTLC data structures
- const btcHtlcAddress = `1HTLC${Math.random().toString(36).slice(2, 30)}`;
- const nimAddress = `NQ${Math.random().toString(36).slice(2, 34)}`;
-
- // Register the swap with the Swaps store
- setSwap(swapHash, {
- id: swapHash,
- in: {
- asset: SwapAsset.BTC,
- transactionHash: btcTxHash,
- outputIndex: 0,
- htlc: {
- address: btcHtlcAddress,
- script: 'btc-htlc-script',
- refundAddress: demoBtcAddress,
- redeemAddress: nimAddress,
- timeoutTimestamp: nowSecs + 7200,
- },
- },
- out: {
- asset: SwapAsset.NIM,
- transactionHash: `nim-swap-${Math.random().toString(16).slice(2, 10)}`,
- htlc: {
- address: nimAddress,
- refundAddress: nimAddress,
- redeemAddress: demoNimAddress,
- timeoutMs: now + 3600000,
- },
- },
- });
-
- // Create the BTC transaction definition
- const tx: BtcTransactionDefinition = {
- address: btcHtlcAddress,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- fraction: fromAmount / btcInitialBalance,
- incoming: false,
- recipientLabel: 'Bitcoin HTLC',
- };
-
- insertFakeBtcTransactions(transformBtcTransaction([tx]));
- updateBtcBalance(-fromAmount);
- break;
- }
- default: {
- console.warn(`Unsupported asset type for swap: ${fromAsset}`);
- }
- }
-
- // Create incoming transaction (to asset)
- switch (toAsset) {
- case 'NIM': {
- // Create a unique transaction hash for the NIM settlement transaction
- const nimSettlementTxHash = `nim-settle-${Math.random().toString(16).slice(2, 10)}`;
-
- // Get the existing swap data to ensure we're using the same addresses
- const existingSwap = useSwapsStore().getSwap.value(swapHash);
- if (!existingSwap || !existingSwap.out || existingSwap.out.asset !== SwapAsset.NIM) {
- console.error('[Demo] Existing swap not found or incorrect structure');
- return;
- }
-
- // Update the swap to include the settlement transaction hash
- setSwap(swapHash, {
- ...existingSwap,
- out: {
- ...existingSwap.out,
- transactionHash: nimSettlementTxHash,
- },
- });
-
- // Create the NIM settlement transaction
- const tx: Partial = {
- value: toAmount,
- recipient: demoNimAddress,
- sender: existingSwap.out.htlc?.address || 'HTLC-ADDRESS',
- timestamp: now + 1000, // Slightly after the funding tx
- transactionHash: nimSettlementTxHash,
- data: {
- type: 'htlc',
- hashAlgorithm: 'sha256',
- hashCount: 1,
- hashRoot: swapHash,
- raw: encodeTextToHex(`Swap ${fromAsset}-${toAsset}`),
- recipient: demoNimAddress,
- sender: existingSwap.out.htlc?.address || 'HTLC-ADDRESS',
- timeout: nowSecs + 3600,
- },
- };
-
- insertFakeNimTransactions(transformNimTransaction([tx]));
- updateNimBalance(toAmount);
- break;
- }
- case 'BTC': {
- // Create a unique transaction hash for the BTC settlement transaction
- const btcSettlementTxHash = `btc-settle-${Math.random().toString(16).slice(2, 10)}`;
-
- // Get the existing swap data
- const existingSwap = useSwapsStore().getSwap.value(swapHash);
- if (!existingSwap || !existingSwap.out || existingSwap.out.asset !== SwapAsset.BTC) {
- console.error('[Demo] Existing swap not found or incorrect structure');
- return;
- }
-
- // Update the swap to include the settlement transaction hash
- setSwap(swapHash, {
- ...existingSwap,
- out: {
- ...existingSwap.out,
- transactionHash: btcSettlementTxHash,
- },
- });
-
- // Create the BTC settlement transaction
- const tx: BtcTransactionDefinition = {
- address: demoBtcAddress,
- daysAgo: 0,
- description: `Swap ${fromAsset} to ${toAsset}`,
- fraction: toAmount / btcInitialBalance,
- incoming: true,
- recipientLabel: 'BTC Settlement',
- };
-
- insertFakeBtcTransactions(transformBtcTransaction([tx]));
- updateBtcBalance(toAmount);
- break;
- }
- default: {
- console.warn(`Unsupported asset type for swap: ${toAsset}`);
- }
- }
-
- console.log('[Demo] Swap completed:', { swapHash, fromAsset, toAsset, fromAmount, toAmount });
-}
-
-const ignoreHubRequests = [
- 'addBtcAddresses',
- 'on',
-];
-
-interface SetupSwapArgs {
- accountId: string;
- swapId: string;
- fund: {
- type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
- inputs: {
- address: string,
- transactionHash: string,
- outputIndex: number,
- outputScript: string,
- value: number,
- }[],
- output: {
- value: number,
- },
- changeOutput: {
- address: string,
- value: number,
- },
- refundAddress: string,
- };
- redeem: {
- type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
- recipient: string,
- value: number,
- fee: number,
- validityStartHeight: number,
- };
- fundingFiatRate: number;
- redeemingFiatRate: number;
- fundFees: {
- processing: number,
- redeeming: number,
- };
- redeemFees: {
- funding: number,
- processing: number,
- };
- serviceSwapFee: number;
- nimiqAddresses: {
- address: string,
- balance: number,
- }[];
- polygonAddresses: {
- address: string,
- usdcBalance: number,
- usdtBalance: number,
- }[];
-}
-
-/**
- * Replacement of the Hub API class to capture and redirect calls to our demo modals instead.
- */
-export class DemoHubApi extends HubApi {
- static create(): DemoHubApi {
- const instance = new DemoHubApi();
- return new Proxy(instance, {
- get(target, prop: keyof HubApi) {
- if (typeof target[prop] !== 'function') {
- return target[prop];
- }
-
- return async (...args: Parameters) => new Promise(async (resolveInterceptedAction) => {
- const requestName = String(prop);
- const [firstArg] = args;
- console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
-
- if (ignoreHubRequests.includes(requestName)) {
- return;
- }
-
- if (requestName === 'setupSwap') {
- const swap = await firstArg as SetupSwapArgs;
- const signerTransaction: SetupSwapResult = {
- nim: {
- transaction: new Uint8Array(),
- serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
- hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
- raw: {
- signerPublicKey: new Uint8Array(),
- signature: new Uint8Array(),
- sender: 'NQ86 D3M0 SW4P NB59 U3F8 NDLX CE0P AFMX Y52S',
- senderType: 2,
- recipient: swap.redeem.recipient,
- recipientType: 0,
- value: swap.redeem.value,
- fee: 0,
- validityStartHeight: swap.redeem.validityStartHeight,
- extraData: new Uint8Array(),
- flags: 0,
- networkId: 5,
- proof: new Uint8Array(),
- },
- },
- btc: {
- serializedTx: '0200000000010168c8952af998f2c68412a848a72d1f9b0b7ff27417df1cb85514c97474b51ba40000000000ffffffff026515000000000000220020bf0ffdd2ffb9a579973455cfe9b56515538b79361d5ae8a4d255dea2519ef77864c501000000000016001428257447efe2d254ce850ea2760274d233d86e5c024730440220792fa932d9d0591e3c5eb03f47d05912a1e21f3e76d169e383af66e47896ac8c02205947df5523490e4138f2da0fc5c9da3039750fe43bd217b68d26730fdcae7fbe012102ef8d4b51d1a075e67d62baa78991d5fc36a658fec28d8b978826058168ed2a1a00000000',
- hash: '3090808993a796c26a614f5a4a36a48e0b4af6cd3e28e39f3f006e9a447da2b3',
- },
- refundTx: '02000000000101b3a27d449a6e003f9fe3283ecdf64a0b8ea4364a5a4f616ac296a793898090300000000000feffffff011e020000000000001600146d2146bb49f6d1de6b4f14e0a8074c79b887cef50447304402202a7dce2e39cf86ee1d7c1e9cc55f1e0fb26932fd22e5437e5e5804a9e5d220b1022031aa177ea085c10c4d54b2f5aa528aac0013b67f9ee674070aa2fb51894de80e0121025b4d40682bbcb5456a9d658971b725666a3cccaa2fb45d269d2f1486bf85b3c000636382012088a820be8719b9427f1551c4234f8b02d8f8aa055ae282b2e9eef6c155326ae951061f8876a914e546b01d8c9d9bf35f9f115132ce8eab7191a68d88ac67046716ca67b17576a9146d2146bb49f6d1de6b4f14e0a8074c79b887cef588ac686816ca67',
- };
-
- // Add to onGoingSwaps map
- onGoingSwaps.set(swap.swapId, swap);
-
- resolveInterceptedAction(signerTransaction);
- return;
- }
-
- // Wait for router readiness
- await new Promise((resolve) => {
- demoRouter.onReady(resolve);
- });
-
- console.log('[Demo] Redirecting to fallback modal');
- demoRouter.push({
- path: `/${DemoModal.Fallback}`,
- });
- });
- },
- });
- }
-}
-
-/**
- * Obfuscates addresses in the UI by:
- * - Showing only first 3 chunks of addresses (rest are XXXX) for NIM addresses
- * - Showing only the first few characters for BTC and polygon addresses
- * - Changing the copy tooltip message
- * - Changing the copy functionality to provide a demo disclaimer
- */
-function obfuscateAddresses(processedElements: WeakSet) {
- // Adds the common clipboard click handler to an element.
- function addDemoClickHandler(el: HTMLElement) {
- el.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- el.classList.add('copied');
- setTimeout(() => el.classList.remove('copied'), 1500);
- navigator.clipboard.writeText('This is a demo address - not for actual use');
- }, true);
- }
-
- // Updates the tooltip for an element.
- function updateTooltip(el: HTMLElement) {
- const tooltip = el.querySelector('.tooltip') as HTMLElement;
- if (tooltip && !processedElements.has(tooltip)) {
- processedElements.add(tooltip);
- tooltip.textContent = 'Demo address';
- tooltip.classList.add('demo-tooltip');
- addDemoClickHandler(tooltip);
- }
- }
-
- // Processes an element: marks it as processed, applies any extra changes, updates tooltip, and adds a click handler.
- function processElement(el: HTMLElement, extraProcess: ((el: HTMLElement) => void) | null = null) {
- if (processedElements.has(el)) return;
- processedElements.add(el);
- if (extraProcess) extraProcess(el);
- updateTooltip(el);
- addDemoClickHandler(el);
- }
-
- // Process NIM address displays: obfuscate address chunks beyond the first three.
- const nimAddressElements = document.querySelectorAll('.copyable.address-display') as NodeListOf;
- nimAddressElements.forEach((el) =>
- processElement(el, (element) => {
- const chunks = element.querySelectorAll('.chunk');
- for (let i = 3; i < chunks.length; i++) {
- const chunk = chunks[i];
- const space = chunk.querySelector('.space');
- chunk.textContent = 'XXXX';
- if (space) chunk.appendChild(space);
- }
- }),
- );
-
- // Process short address displays: change the last chunk of the short address.
- const shortAddressElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable') as NodeListOf;
- shortAddressElements.forEach((el) =>
- processElement(el, (element) => {
- const lastChunk = element.querySelector('.short-address .address:last-child');
- if (lastChunk) {
- lastChunk.textContent = 'xxxx';
- }
- }),
- );
-
- // Process tooltip boxes inside short address displays.
- const tooltipBoxElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable .tooltip-box') as NodeListOf;
- tooltipBoxElements.forEach((el) => {
- if (processedElements.has(el)) return;
- processedElements.add(el);
- el.textContent = 'Demo address';
- el.classList.add('demo-tooltip');
- addDemoClickHandler(el);
- });
-}
-
-/**
- * Observes the receive modal and redirects relevant button clicks to the fallback modal
- */
-function observeReceiveModal(processedElements: WeakSet) {
- // Find the receive modal
- const receiveModal = document.querySelector('.receive-modal');
- if (!receiveModal) return;
-
- // Look for buttons that should redirect to the fallback modal
- const buttons = receiveModal.querySelectorAll('.nq-button-s, .qr-button');
-
- buttons.forEach((button) => {
- // Skip if we've already processed this button
- if (processedElements.has(button)) return;
-
- // Mark as processed to avoid adding multiple listeners
- processedElements.add(button);
-
- // Replace the original click handler with our redirect
- button.addEventListener('click', (event) => {
- // Prevent the default action and stop propagation
- event.preventDefault();
- event.stopPropagation();
-
- // Redirect to the fallback modal
- demoRouter.replace({
- path: `/${DemoModal.Fallback}`,
- });
-
- console.log('[Demo] Redirected receive modal button click to fallback modal');
- }, true); // Use capture to intercept the event before other handlers
- });
-}
-
-/**
- * Observes the transaction list items to replace the identicon and address.
- */
-function observeTransactionList(processedElements: WeakSet) {
- const buttons = document.querySelectorAll('.transaction-list button.reset.transaction.confirmed');
- buttons.forEach((button) => {
- if (processedElements.has(button)) return;
- processedElements.add(button);
-
- const message = button.querySelector('.message') as HTMLDivElement;
- if (!message || message.innerText !== '·NIM Bank purchase') return;
-
- // Replace identicon with bankSvg
- const iconDiv = button.querySelector(':scope > .identicon');
- if (iconDiv) {
- iconDiv.innerHTML = bankSvg;
- }
-
- // Replace address text
- const addressDiv = button.querySelector(':scope > .data > .address');
- if (addressDiv) {
- addressDiv.textContent = 'Demo Bank';
- }
- });
-
- // Replace the identicon in the transaction modal for the bank
- const transactionModal = document.querySelector('.transaction-modal') as HTMLDivElement;
- if (!transactionModal) return;
- if (processedElements.has(transactionModal)) return;
- processedElements.add(transactionModal);
- const message = transactionModal.querySelector('.message') as HTMLDivElement;
- if (message && message.innerText === 'NIM Bank purchase') {
- const iconDiv = transactionModal.querySelector('.identicon > .identicon');
- if (iconDiv) {
- iconDiv.innerHTML = bankSvg.replaceAll('="48"', '="72"');
- }
- }
-}
-
-const bankSvg = ``;
+// Re-export everything from the refactored demo modules for backward compatibility
+export {
+ dangerouslyInitializeDemo,
+ checkIfDemoIsActive,
+ dangerouslyInsertFakeBuyNimTransaction,
+ DemoHubApi,
+ type DemoState,
+ type DemoFlowType,
+ type DemoFlowMessage,
+ DemoModal,
+ MessageEventName,
+ demoRoutes,
+} from './demo/index';
diff --git a/src/lib/demo/DemoAccounts.ts b/src/lib/demo/DemoAccounts.ts
new file mode 100644
index 000000000..2abc1e0b4
--- /dev/null
+++ b/src/lib/demo/DemoAccounts.ts
@@ -0,0 +1,65 @@
+import { CryptoCurrency } from '@nimiq/utils';
+import { AccountType, useAccountStore } from '@/stores/Account';
+import { AddressType, useAddressStore } from '@/stores/Address';
+import { useAccountSettingsStore } from '@/stores/AccountSettings';
+import { usePolygonAddressStore } from '@/stores/PolygonAddress';
+import {
+ demoNimAddress,
+ demoBtcAddress,
+ demoPolygonAddress,
+ nimInitialBalance,
+ usdcInitialBalance,
+ usdtInitialBalance,
+} from './DemoConstants';
+
+/**
+ * Setup and initialize the demo data for all currencies.
+ */
+export function setupDemoAddresses(): void {
+ const { setAddressInfos } = useAddressStore();
+ setAddressInfos([
+ {
+ label: 'Demo Account',
+ type: AddressType.BASIC,
+ address: demoNimAddress,
+ balance: nimInitialBalance,
+ },
+ ]);
+
+ // Setup Polygon addresses and balances
+ const { setAddressInfos: setPolygonAddressInfos } = usePolygonAddressStore();
+ setPolygonAddressInfos([{
+ address: demoPolygonAddress,
+ balanceUsdc: usdcInitialBalance,
+ balanceUsdcBridged: 0,
+ balanceUsdtBridged: usdtInitialBalance,
+ pol: 1,
+ }]);
+}
+
+/**
+ * Creates a fake main account referencing our demo addresses.
+ */
+export function setupDemoAccount(): void {
+ const { addAccountInfo, setActiveCurrency } = useAccountStore();
+ const { setStablecoin, setKnowsAboutUsdt } = useAccountSettingsStore();
+
+ // Setup account info with both USDC and USDT addresses
+ addAccountInfo({
+ id: 'demo-account-1',
+ type: AccountType.BIP39,
+ label: 'Demo Main Account',
+ fileExported: true,
+ wordsExported: true,
+ addresses: [demoNimAddress],
+ btcAddresses: { internal: [demoBtcAddress], external: [demoBtcAddress] },
+ polygonAddresses: [demoPolygonAddress, demoPolygonAddress],
+ uid: 'demo-uid-1',
+ });
+
+ // Pre-select USDC as the default stablecoin and mark USDT as known
+ setStablecoin(CryptoCurrency.USDC);
+ setKnowsAboutUsdt(true);
+
+ setActiveCurrency(CryptoCurrency.NIM);
+}
diff --git a/src/lib/demo/DemoConstants.ts b/src/lib/demo/DemoConstants.ts
new file mode 100644
index 000000000..8c53bfcfe
--- /dev/null
+++ b/src/lib/demo/DemoConstants.ts
@@ -0,0 +1,169 @@
+// Demo state type
+export type DemoState = {
+ active: boolean,
+};
+
+// Demo modal types
+export const DemoModal = {
+ Fallback: 'demo-fallback',
+ Buy: 'demo-buy',
+} as const;
+
+// Demo addresses
+export const demoNimAddress = 'NQ57 2814 7L5B NBBD 0EU7 EL71 HXP8 M7H8 MHKD';
+export const demoBtcAddress = '1XYZDemoAddress';
+export const demoPolygonAddress = '0xabc123DemoPolygonAddress';
+export const buyFromAddress = 'NQ04 JG63 HYXL H3QF PPNA 7ED7 426M 3FQE FHE5';
+
+// Initial balances
+export const nimInitialBalance = 140_418 * 1e5; // 14,041,800,000 - 14 april, 2018. 5 decimals.
+export const btcInitialBalance = 0.0025 * 1e8; // 1 BTC (8 decimals)
+export const usdtInitialBalance = 514.83 * 1e6; // 5000 USDT (6 decimals)
+export const usdcInitialBalance = 357.38 * 1e6; // 3000 USDC (6 decimals)
+
+// Message event types
+export enum MessageEventName {
+ FlowChange = 'FlowChange'
+}
+
+// Flow types
+export type DemoFlowType = 'buy' | 'swap' | 'stake';
+
+// Flow message structure
+export type DemoFlowMessage = { kind: 'FlowChange', data: DemoFlowType };
+
+// Demo routes mapping
+export const demoRoutes: Record = {
+ buy: '/buy',
+ swap: '/swap/NIM-BTC',
+ stake: '/staking',
+};
+
+// Hub API requests to ignore
+export const ignoreHubRequests = [
+ 'addBtcAddresses',
+ 'on',
+];
+
+// Demo CSS styles
+export const demoCSS = `
+.transaction-list .month-label > :where(.fetching, .failed-to-fetch) {
+ display: none;
+}
+
+/* Hide address */
+.active-address .meta .copyable {
+ display: none !important;
+}
+
+#app > div > .wallet-status-button.nq-button-pill {
+ display: none;
+}
+
+.staking-button .tooltip.staking-feature-tip {
+ display: none;
+}
+
+.modal.transaction-modal .confirmed .tooltip.info-tooltip {
+ display: none;
+}
+
+.send-modal-footer .footer-notice {
+ display: none;
+}
+
+/* Demo address tooltip styling */
+.tooltip.demo-tooltip {
+ width: max-content;
+ background: var(--nimiq-orange-bg);
+ margin-left: -7rem;
+}
+
+.tooltip.demo-tooltip::after {
+ background: #fc750c; /* Match the red theme for the demo warning */
+}
+
+.demo-highlight-badge {
+ position: absolute;
+ width: 34px;
+ height: 34px;
+ z-index: 5;
+ pointer-events: none;
+}
+
+.demo-highlight-badge > div {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background: rgba(31, 35, 72, 0.1);
+ border: 1.5px solid rgba(255, 255, 255, 0.5);
+ border-radius: 50%;
+ backdrop-filter: blur(3px);
+}
+
+.demo-highlight-badge > div::before {
+ content: "";
+ position: absolute;
+ inset: 5px;
+ background: rgba(31, 35, 72, 0.3);
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(3px);
+ border-radius: 12px;
+}
+
+.demo-highlight-badge > div::after {
+ content: "";
+ position: absolute;
+ inset: 11.6px;
+ background: rgba(255, 255, 255);
+ border-radius: 50%;
+}
+
+.send-modal-footer .footer-notice {
+ display: none;
+}
+
+.account-grid > button.reset,
+.account-grid > .nimiq-account {
+ background: #e7e8ea;
+ border-radius: 8px;
+ transition: background 0.2s var(--nimiq-ease);
+}
+
+.account-grid > button:where(:hover, :focus-visible) {
+ background: #dedee2 !important;
+}
+
+/* Hide links and addresses to block explorer in the swap animation */
+.swap-animation :where(.short-address, .blue-link.nq-link) {
+ display: none !important;
+}
+`;
+
+// Bank SVG icon
+export const bankSvg = ``;
+
+/**
+ * Checks if the demo mode should be active.
+ * Demo mode is only activated through build commands:
+ * - yarn serve:demo
+ * - yarn build:demo
+ * - yarn build:demo-production
+ * This ensures demo builds are separate deployments with no runtime activation.
+ */
+export function checkIfDemoIsActive(): boolean {
+ return true;
+}
diff --git a/src/lib/demo/DemoDom.ts b/src/lib/demo/DemoDom.ts
new file mode 100644
index 000000000..cc99f97bd
--- /dev/null
+++ b/src/lib/demo/DemoDom.ts
@@ -0,0 +1,275 @@
+import VueRouter from 'vue-router';
+import { demoCSS, DemoModal, bankSvg } from './DemoConstants';
+
+/**
+ * Creates a style tag to add demo-specific CSS.
+ */
+export function insertCustomDemoStyles(): void {
+ const styleElement = document.createElement('style');
+ styleElement.innerHTML = demoCSS;
+ document.head.appendChild(styleElement);
+}
+
+/**
+ * Sets up a single mutation observer to handle all DOM-based demo features
+ */
+export function setupSingleMutationObserver(demoRouter: VueRouter): void {
+ // Track processed elements to avoid duplicate processing
+ const processedElements = new WeakSet();
+
+ // Handler function to process all DOM mutations
+ const processDomChanges = () => {
+ // Call each handler with the processed elements set
+ setupVisualCues(processedElements);
+ disableSwapTriggers(processedElements);
+ enableSellAndSwapModals(processedElements);
+ obfuscateAddresses(processedElements);
+ observeTransactionList(processedElements);
+ observeReceiveModal(processedElements, demoRouter);
+ };
+
+ // Create one mutation observer for all DOM modifications
+ const mutationObserver = new MutationObserver(processDomChanges);
+
+ // Observe the entire document with a single observer
+ mutationObserver.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['disabled'],
+ });
+
+ processDomChanges();
+ // Also run checks when routes change to catch elements that appear after navigation
+ demoRouter.afterEach(() => {
+ // Wait for Vue to update the DOM after route change
+ setTimeout(processDomChanges, 100);
+ });
+}
+
+/**
+ * Observes the home view to attach a highlight to some buttons for demonstration purposes.
+ */
+function setupVisualCues(processedElements: WeakSet): void {
+ const highlightTargets = [
+ ['.sidebar .trade-actions button', { top: '-18px', right: '-4px' }],
+ ['.sidebar .swap-tooltip button', { top: '-18px', right: '-8px' }],
+ ['.actions .staking-button', { top: '-2px', right: '-2px' }],
+ ] as const;
+
+ highlightTargets.forEach(([selector, position]) => {
+ const target = document.querySelector(selector);
+ if (!target || processedElements.has(target) || target.querySelector('.demo-highlight-badge')) return;
+
+ const wrapper = document.createElement('div');
+ wrapper.classList.add('demo-highlight-badge');
+ wrapper.style.top = position.top;
+ wrapper.style.right = position.right;
+ const circle = document.createElement('div');
+
+ wrapper.appendChild(circle);
+ target.appendChild(wrapper);
+ processedElements.add(target);
+ });
+}
+
+/**
+ * The only swap allowed is NIM-BTC.
+ * Removes the swap triggers from the account grid so the user does not the
+ * option to swap or sell assets in the demo environment.
+ * We also remove the pair selection in the SwapModal.
+ */
+function disableSwapTriggers(processedElements: WeakSet): void {
+ const swapTriggers = document.querySelectorAll(
+ '.account-grid > :where(.nim-usdc-swap-button, .nim-btc-swap-button, .btc-usdc-swap-button, .account-backgrounds)',
+ ) as NodeListOf;
+
+ swapTriggers.forEach((trigger) => {
+ if (!processedElements.has(trigger)) {
+ trigger.remove();
+ processedElements.add(trigger);
+ }
+ });
+
+ const pairSelection = document.querySelector('.pair-selection');
+ if (pairSelection && !processedElements.has(pairSelection)) {
+ pairSelection.remove();
+ processedElements.add(pairSelection);
+ }
+}
+
+/**
+ * Ensures the Send button in modals is always enabled in demo mode, regardless of network state.
+ * This allows users to interact with the send functionality without waiting for network sync.
+ */
+function enableSellAndSwapModals(processedElements: WeakSet): void {
+ // Target the send modal and swap footer button
+ const bottomButton = document.querySelector('.send-modal-footer .nq-button');
+ if (!bottomButton || processedElements.has(bottomButton)) return;
+
+ if (bottomButton.hasAttribute('disabled')) {
+ bottomButton.removeAttribute('disabled');
+ bottomButton.classList.remove('disabled');
+ processedElements.add(bottomButton);
+
+ // Also find and hide any sync message if shown
+ const footer = document.querySelector('.send-modal-footer');
+ if (footer) {
+ const footerNotice = footer.querySelector('.footer-notice') as HTMLDivElement;
+ if (footerNotice && footerNotice.textContent
+ && (footerNotice.textContent.includes('Connecting to Bitcoin')
+ || footerNotice.textContent.includes('Syncing with Bitcoin'))) {
+ footerNotice.style.display = 'none';
+ processedElements.add(footerNotice);
+ }
+ }
+ }
+}
+
+/**
+ * Obfuscates addresses in the UI by:
+ * - Showing only first 3 chunks of addresses (rest are XXXX) for NIM addresses
+ * - Showing only the first few characters for BTC and polygon addresses
+ * - Changing the copy tooltip message
+ * - Changing the copy functionality to provide a demo disclaimer
+ */
+function obfuscateAddresses(processedElements: WeakSet): void {
+ // Adds the common clipboard click handler to an element.
+ function addDemoClickHandler(el: HTMLElement) {
+ el.addEventListener('click', (e: MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ el.classList.add('copied');
+ setTimeout(() => el.classList.remove('copied'), 1500);
+ navigator.clipboard.writeText('This is a demo address - not for actual use');
+ }, true);
+ }
+
+ // Updates the tooltip for an element.
+ function updateTooltip(el: HTMLElement) {
+ const tooltip = el.querySelector('.tooltip') as HTMLElement;
+ if (tooltip && !processedElements.has(tooltip)) {
+ processedElements.add(tooltip);
+ tooltip.textContent = 'Demo address';
+ tooltip.classList.add('demo-tooltip');
+ addDemoClickHandler(tooltip);
+ }
+ }
+
+ // Processes an element: marks it as processed, applies any extra changes, updates tooltip, and adds a click handler.
+ function processElement(el: HTMLElement, extraProcess: ((el: HTMLElement) => void) | null = null) {
+ if (processedElements.has(el)) return;
+ processedElements.add(el);
+ if (extraProcess) extraProcess(el);
+ updateTooltip(el);
+ addDemoClickHandler(el);
+ }
+
+ // Process NIM address displays: obfuscate address chunks beyond the first three.
+ const nimAddressElements = document.querySelectorAll('.copyable.address-display') as NodeListOf;
+ nimAddressElements.forEach((el) =>
+ processElement(el, (element) => {
+ const chunks = element.querySelectorAll('.chunk');
+ for (let i = 3; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const space = chunk.querySelector('.space');
+ chunk.textContent = 'XXXX';
+ if (space) chunk.appendChild(space);
+ }
+ }),
+ );
+
+ // Process short address displays: change the last chunk of the short address.
+ const shortAddressElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable') as NodeListOf;
+ shortAddressElements.forEach((el) =>
+ processElement(el, (element) => {
+ const lastChunk = element.querySelector('.short-address .address:last-child');
+ if (lastChunk) {
+ lastChunk.textContent = 'xxxx';
+ }
+ }),
+ );
+
+ // Process tooltip boxes inside short address displays.
+ const tooltipBoxElements = document.querySelectorAll('.tooltip.interactive-short-address.is-copyable .tooltip-box') as NodeListOf;
+ tooltipBoxElements.forEach((el) => {
+ if (processedElements.has(el)) return;
+ processedElements.add(el);
+ el.textContent = 'Demo address';
+ el.classList.add('demo-tooltip');
+ addDemoClickHandler(el);
+ });
+}
+
+/**
+ * Observes the receive modal and redirects relevant button clicks to the fallback modal
+ */
+function observeReceiveModal(processedElements: WeakSet, demoRouter: VueRouter): void {
+ // Find the receive modal
+ const receiveModal = document.querySelector('.receive-modal');
+ if (!receiveModal) return;
+
+ // Look for buttons that should redirect to the fallback modal
+ const buttons = receiveModal.querySelectorAll('.nq-button-s, .qr-button');
+
+ buttons.forEach((button) => {
+ // Skip if we've already processed this button
+ if (processedElements.has(button)) return;
+
+ // Mark as processed to avoid adding multiple listeners
+ processedElements.add(button);
+
+ // Replace the original click handler with our redirect
+ button.addEventListener('click', (event) => {
+ // Prevent the default action and stop propagation
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Redirect to the fallback modal
+ demoRouter.replace({
+ path: `/${DemoModal.Fallback}`,
+ });
+
+ console.log('[Demo] Redirected receive modal button click to fallback modal');
+ }, true); // Use capture to intercept the event before other handlers
+ });
+}
+
+/**
+ * Observes the transaction list items to replace the identicon and address.
+ */
+function observeTransactionList(processedElements: WeakSet): void {
+ const buttons = document.querySelectorAll('.transaction-list button.reset.transaction.confirmed');
+ buttons.forEach((button) => {
+ if (processedElements.has(button)) return;
+ processedElements.add(button);
+
+ const message = button.querySelector('.message') as HTMLDivElement;
+ if (!message || message.innerText !== '·NIM Bank purchase') return;
+
+ // Replace identicon with bankSvg
+ const iconDiv = button.querySelector(':scope > .identicon');
+ if (iconDiv) {
+ iconDiv.innerHTML = bankSvg;
+ }
+
+ // Replace address text
+ const addressDiv = button.querySelector(':scope > .data > .address');
+ if (addressDiv) {
+ addressDiv.textContent = 'Demo Bank';
+ }
+ });
+
+ // Replace the identicon in the transaction modal for the bank
+ const transactionModal = document.querySelector('.transaction-modal') as HTMLDivElement;
+ if (!transactionModal) return;
+ if (processedElements.has(transactionModal)) return;
+ processedElements.add(transactionModal);
+ const message = transactionModal.querySelector('.message') as HTMLDivElement;
+ if (message && message.innerText === 'NIM Bank purchase') {
+ const iconDiv = transactionModal.querySelector('.identicon > .identicon');
+ if (iconDiv) {
+ iconDiv.innerHTML = bankSvg.replaceAll('="48"', '="72"');
+ }
+ }
+}
diff --git a/src/lib/demo/DemoFlows.ts b/src/lib/demo/DemoFlows.ts
new file mode 100644
index 000000000..a5fbba4b6
--- /dev/null
+++ b/src/lib/demo/DemoFlows.ts
@@ -0,0 +1,105 @@
+import VueRouter from 'vue-router';
+import { useStakingStore } from '@/stores/Staking';
+import { demoNimAddress } from './DemoConstants';
+import { createStakeTransaction, transformNimTransaction, insertFakeNimTransactions } from './DemoTransactions';
+
+/**
+ * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
+ */
+export function replaceBuyNimFlow(demoRouter: VueRouter): void {
+ let observedTarget: HTMLDivElement | undefined;
+
+ demoRouter.afterEach((to) => {
+ if (to.path.startsWith('/')) {
+ const targetSelector = '.sidebar .trade-actions';
+ const checkForTradeActions = setInterval(() => {
+ const target = document.querySelector(targetSelector) as HTMLDivElement;
+ if (!target || target === observedTarget) return;
+ observedTarget = target;
+
+ target.innerHTML = '';
+
+ const btn1 = document.createElement('button');
+ btn1.className = 'nq-button-s inverse';
+ btn1.style.flex = '1';
+ btn1.addEventListener('click', () => {
+ demoRouter.push({
+ path: '/buy',
+ });
+ });
+ btn1.innerHTML = 'Buy';
+
+ const btn2 = document.createElement('button');
+ btn2.className = 'nq-button-s inverse';
+ btn2.style.flex = '1';
+ btn2.disabled = true;
+ btn2.innerHTML = 'Sell';
+
+ target.appendChild(btn1);
+ target.appendChild(btn2);
+ }, 500);
+
+ // Clear interval when navigating away
+ demoRouter.afterEach((nextTo) => {
+ if (!nextTo.path.startsWith('/')) {
+ clearInterval(checkForTradeActions);
+ observedTarget = undefined;
+ }
+ });
+ }
+ });
+}
+
+/**
+ * Observes the staking modal and prevents from validating the info and instead fakes the staking process.
+ */
+export function replaceStakingFlow(demoRouter: VueRouter): void {
+ let lastProcessedButton: HTMLButtonElement;
+
+ demoRouter.afterEach((to) => {
+ if (to.path === '/staking') {
+ const checkForStakeButton = setInterval(() => {
+ const target = document.querySelector('.stake-graph-page .stake-button');
+ if (!target || target === lastProcessedButton) return;
+
+ // remove previous listeners by cloning the element and replacing the original
+ const newElement = target.cloneNode(true) as HTMLButtonElement;
+ target.parentNode!.replaceChild(newElement, target);
+ newElement.removeAttribute('disabled');
+ lastProcessedButton = newElement;
+
+ newElement.addEventListener('click', async () => {
+ const { setStake } = useStakingStore();
+ const { activeValidator } = useStakingStore();
+ const amountInput = document.querySelector('.nq-input') as HTMLInputElement;
+ const amount = Number.parseFloat(amountInput.value.replaceAll(/[^\d]/g, '')) * 1e5;
+
+ const { address: validatorAddress } = activeValidator.value!;
+
+ demoRouter.push({
+ path: '/',
+ });
+
+ await new Promise((resolve) => { window.setTimeout(resolve, 100); });
+ setStake({
+ activeBalance: 0,
+ inactiveBalance: amount,
+ address: demoNimAddress,
+ retiredBalance: 0,
+ validator: validatorAddress,
+ });
+
+ const stakeTx = createStakeTransaction(amount, validatorAddress);
+ insertFakeNimTransactions(transformNimTransaction([stakeTx]));
+ });
+ }, 500);
+
+ // Clear interval when navigating away
+ demoRouter.afterEach((nextTo) => {
+ if (nextTo.path !== '/staking') {
+ clearInterval(checkForStakeButton);
+ }
+ });
+ }
+ });
+}
diff --git a/src/lib/demo/DemoHubApi.ts b/src/lib/demo/DemoHubApi.ts
new file mode 100644
index 000000000..be22ee873
--- /dev/null
+++ b/src/lib/demo/DemoHubApi.ts
@@ -0,0 +1,122 @@
+import HubApi, { SetupSwapResult } from '@nimiq/hub-api';
+import { ignoreHubRequests } from './DemoConstants';
+import { addOngoingSwap } from './DemoSwaps';
+
+interface SetupSwapArgs {
+ accountId: string;
+ swapId: string;
+ fund: {
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
+ inputs: {
+ address: string,
+ transactionHash: string,
+ outputIndex: number,
+ outputScript: string,
+ value: number,
+ }[],
+ output: {
+ value: number,
+ },
+ changeOutput: {
+ address: string,
+ value: number,
+ },
+ refundAddress: string,
+ };
+ redeem: {
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
+ recipient: string,
+ value: number,
+ fee: number,
+ validityStartHeight: number,
+ };
+ fundingFiatRate: number;
+ redeemingFiatRate: number;
+ fundFees: {
+ processing: number,
+ redeeming: number,
+ };
+ redeemFees: {
+ funding: number,
+ processing: number,
+ };
+ serviceSwapFee: number;
+ nimiqAddresses: {
+ address: string,
+ balance: number,
+ }[];
+ polygonAddresses: {
+ address: string,
+ usdcBalance: number,
+ usdtBalance: number,
+ }[];
+}
+
+/**
+ * Replacement of the Hub API class to capture and redirect calls to our demo modals instead.
+ */
+export class DemoHubApi extends HubApi {
+ static create(): DemoHubApi {
+ const instance = new DemoHubApi();
+ return new Proxy(instance, {
+ get(target, prop: keyof HubApi) {
+ if (typeof target[prop] !== 'function') {
+ return target[prop];
+ }
+
+ return async (...args: Parameters) => new Promise(async (resolveInterceptedAction) => {
+ const requestName = String(prop);
+ const [firstArg] = args;
+ console.warn(`[Demo] Mocking Hub call: ${requestName}("${firstArg}")`);
+
+ if (ignoreHubRequests.includes(requestName)) {
+ return;
+ }
+
+ if (requestName === 'setupSwap') {
+ const swap = await firstArg as SetupSwapArgs;
+ const signerTransaction: SetupSwapResult = {
+ nim: {
+ transaction: new Uint8Array(),
+ serializedTx: '0172720036a3b2ca9e0de8b369e6381753ebef945a020091fa7bbddf959616767c50c50962c9e056ade9c400000000000000989680000000000000000000c3e23d0500a60100010366687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292520000000000000000000000000000000000000000000000000000000000000000000200demoSerializedTx',
+ hash: '6c58b337a907fe000demoTxHash8a1f4ab4fdc0f69b1e582f',
+ raw: {
+ signerPublicKey: new Uint8Array(),
+ signature: new Uint8Array(),
+ sender: 'NQ86 D3M0 SW4P NB59 U3F8 NDLX CE0P AFMX Y52S',
+ senderType: 2,
+ recipient: swap.redeem.recipient,
+ recipientType: 0,
+ value: swap.redeem.value,
+ fee: 0,
+ validityStartHeight: swap.redeem.validityStartHeight,
+ extraData: new Uint8Array(),
+ flags: 0,
+ networkId: 5,
+ proof: new Uint8Array(),
+ },
+ },
+ btc: {
+ serializedTx: '0200000000010168c8952af998f2c68412a848a72d1f9b0b7ff27417df1cb85514c97474b51ba40000000000ffffffff026515000000000000220020bf0ffdd2ffb9a579973455cfe9b56515538b79361d5ae8a4d255dea2519ef77864c501000000000016001428257447efe2d254ce850ea2760274d233d86e5c024730440220792fa932d9d0591e3c5eb03f47d05912a1e21f3e76d169e383af66e47896ac8c02205947df5523490e4138f2da0fc5c9da3039750fe43bd217b68d26730fdcae7fbe012102ef8d4b51d1a075e67d62baa78991d5fc36a658fec28d8b978826058168ed2a1a00000000',
+ hash: '3090808993a796c26a614f5a4a36a48e0b4af6cd3e28e39f3f006e9a447da2b3',
+ },
+ refundTx: '02000000000101b3a27d449a6e003f9fe3283ecdf64a0b8ea4364a5a4f616ac296a793898090300000000000feffffff011e020000000000001600146d2146bb49f6d1de6b4f14e0a8074c79b887cef50447304402202a7dce2e39cf86ee1d7c1e9cc55f1e0fb26932fd22e5437e5e5804a9e5d220b1022031aa177ea085c10c4d54b2f5aa528aac0013b67f9ee674070aa2fb51894de80e0121025b4d40682bbcb5456a9d658971b725666a3cccaa2fb45d269d2f1486bf85b3c000636382012088a820be8719b9427f1551c4234f8b02d8f8aa055ae282b2e9eef6c155326ae951061f8876a914e546b01d8c9d9bf35f9f115132ce8eab7191a68d88ac67046716ca67b17576a9146d2146bb49f6d1de6b4f14e0a8074c79b887cef588ac686816ca67',
+ };
+
+ // Add to onGoingSwaps map
+ addOngoingSwap(swap.swapId, swap);
+
+ resolveInterceptedAction(signerTransaction);
+ return;
+ }
+
+ // For any other Hub API method, redirect to the demo fallback modal
+ console.log('[Demo] Redirecting to fallback modal');
+ // This would need access to the router, which would be passed in during initialization
+ // For now, we'll resolve with undefined and let the UI handle it
+ resolveInterceptedAction(undefined);
+ });
+ },
+ });
+ }
+}
diff --git a/src/lib/demo/DemoSwaps.ts b/src/lib/demo/DemoSwaps.ts
new file mode 100644
index 000000000..4eaf9700b
--- /dev/null
+++ b/src/lib/demo/DemoSwaps.ts
@@ -0,0 +1,386 @@
+import {
+ FastspotAsset,
+ FastspotLimits,
+ FastspotUserLimits,
+ ReferenceAsset,
+ SwapAsset,
+ SwapStatus,
+} from '@nimiq/fastspot-api';
+import Config from 'config';
+import { useAccountStore } from '@/stores/Account';
+import { SwapState, useSwapsStore } from '@/stores/Swaps';
+import { useFiatStore } from '@/stores/Fiat';
+import { useBtcAddressStore } from '@/stores/BtcAddress';
+import { completeSwapTransactions } from './DemoTransactions';
+import { demoBtcAddress } from './DemoConstants';
+
+// Track ongoing swaps
+const onGoingSwaps = new Map();
+
+interface SetupSwapArgs {
+ accountId: string;
+ swapId: string;
+ fund: {
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
+ inputs: {
+ address: string,
+ transactionHash: string,
+ outputIndex: number,
+ outputScript: string,
+ value: number,
+ }[],
+ output: {
+ value: number,
+ },
+ changeOutput: {
+ address: string,
+ value: number,
+ },
+ refundAddress: string,
+ };
+ redeem: {
+ type: 'BTC' | 'NIM' /* | 'USDC' | 'USDT' */,
+ recipient: string,
+ value: number,
+ fee: number,
+ validityStartHeight: number,
+ };
+ fundingFiatRate: number;
+ redeemingFiatRate: number;
+ fundFees: {
+ processing: number,
+ redeeming: number,
+ };
+ redeemFees: {
+ funding: number,
+ processing: number,
+ };
+ serviceSwapFee: number;
+ nimiqAddresses: {
+ address: string,
+ balance: number,
+ }[];
+ polygonAddresses: {
+ address: string,
+ usdcBalance: number,
+ usdtBalance: number,
+ }[];
+}
+
+/**
+ * Intercepts fetch request for swaps
+ */
+export function interceptFetchRequest(): void {
+ const originalFetch = window.fetch;
+ window.fetch = async (...args: Parameters) => {
+ if (typeof args[0] !== 'string') return originalFetch(...args);
+ if (args[0].startsWith('/')) return originalFetch(...args);
+
+ const url = new URL(args[0] as string);
+ const isFastspotRequest = url.host === (new URL(Config.fastspot.apiEndpoint).host);
+ const isLimitsRequest = url.pathname.includes('/limits');
+ const isAssetsRequest = url.pathname.includes('/assets');
+ const isSwapRequest = url.pathname.includes('/swaps');
+
+ // return originalFetch(...args);
+ if (!isFastspotRequest) {
+ return originalFetch(...args);
+ }
+
+ console.log('[Demo] Intercepted fetch request:', url.pathname);
+
+ if (isLimitsRequest) {
+ const constants = {
+ current: '9800',
+ daily: '50000',
+ dailyRemaining: '49000',
+ monthly: '100000',
+ monthlyRemaining: '98000',
+ swap: '10000',
+ } as const;
+
+ const [assetOrLimit] = url.pathname.split('/').slice(-2) as [SwapAsset | 'limits', string];
+
+ if (assetOrLimit === 'limits') {
+ const limits: FastspotUserLimits = {
+ asset: ReferenceAsset.USD,
+ ...constants,
+ };
+ return new Response(JSON.stringify(limits));
+ }
+
+ const asset = assetOrLimit as SwapAsset;
+
+ const { exchangeRates, currency } = useFiatStore();
+ const rate: number = exchangeRates.value[asset.toLocaleLowerCase().split('_')[0]][currency.value]!;
+
+ const json: FastspotLimits = {
+ asset,
+ referenceAsset: ReferenceAsset.USD,
+ referenceCurrent: constants.current,
+ referenceDaily: constants.daily,
+ referenceDailyRemaining: constants.dailyRemaining,
+ referenceMonthly: constants.monthly,
+ referenceMonthlyRemaining: constants.monthlyRemaining,
+ referenceSwap: `${10000}`,
+ current: `${Number(constants.current) / rate}`,
+ daily: `${Number(constants.daily) / rate}`,
+ dailyRemaining: `${Number(constants.dailyRemaining) / rate}`,
+ monthly: `${Number(constants.monthly) / rate}`,
+ monthlyRemaining: `${Number(constants.monthlyRemaining) / rate}`,
+ swap: `${Number(constants.swap) / rate}`,
+ };
+
+ return new Response(JSON.stringify(json));
+ }
+
+ if (isAssetsRequest) {
+ // Return mock assets data with fees for all supported assets
+ const json: FastspotAsset[] = [
+ {
+ symbol: SwapAsset.BTC,
+ name: 'Bitcoin',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.BTC)}`,
+ limits: { minimum: '0.0001', maximum: '1' },
+ },
+ {
+ symbol: SwapAsset.NIM,
+ name: 'Nimiq',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.NIM)}`,
+ limits: { minimum: '1', maximum: '100000' },
+ },
+ {
+ symbol: SwapAsset.USDC_MATIC,
+ name: 'USDC (Polygon)',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDC_MATIC)}`,
+ limits: { minimum: '1', maximum: '100000' },
+ },
+ {
+ symbol: SwapAsset.USDT_MATIC,
+ name: 'USDT (Polygon)',
+ feePerUnit: `${getNetworkFeePerUnit(SwapAsset.USDT_MATIC)}`,
+ limits: { minimum: '1', maximum: '100000' },
+ },
+ ];
+
+ return new Response(JSON.stringify(json));
+ }
+
+ if (isSwapRequest) {
+ const swapId = url.pathname.split('/').slice(-1)[0];
+
+ if (swapId === 'swaps') {
+ const { patchAccount, activeAccountInfo } = useAccountStore();
+ const newBtcAddress = demoBtcAddress + Math.random().toString(36).slice(2, 9);
+
+ patchAccount(activeAccountInfo.value?.id, {
+ ...activeAccountInfo,
+ btcAddresses: {
+ external: [...(activeAccountInfo.value?.btcAddresses.external || []), newBtcAddress],
+ internal: [...(activeAccountInfo.value?.btcAddresses.internal || []), newBtcAddress],
+ },
+ });
+
+ const { addAddressInfos } = useBtcAddressStore();
+ addAddressInfos([{
+ address: newBtcAddress,
+ txoCount: 0, // Total number of outputs received
+ utxos: [],
+ }]);
+ } else {
+ listenForSwapChanges();
+
+ console.log('[Demo] Swap request:', swapId);
+ const swap = onGoingSwaps.get(swapId);
+ if (!swap) {
+ return new Response(JSON.stringify({
+ error: 'Swap not found',
+ status: 404,
+ }), { status: 404 });
+ }
+
+ console.log('[Demo] Swap:', swap);
+ const expirationTimestamp = Math.floor(Date.now() / 1000) + 3600;
+
+ return new Response(JSON.stringify({
+ id: swapId,
+ status: SwapStatus.WAITING_FOR_CONFIRMATION,
+ expires: expirationTimestamp,
+ info: {
+ from: [
+ {
+ symbol: swap.fund.type,
+ amount: swap.fund.output.value / 1e8,
+ fundingNetworkFee: {
+ total: '0.000037',
+ perUnit: '0.000000240254',
+ totalIsIncluded: true,
+ },
+ operatingNetworkFee: {
+ total: '0',
+ perUnit: '0.000000240254',
+ totalIsIncluded: false,
+ },
+ finalizeNetworkFee: {
+ total: '0.0000346',
+ perUnit: '0.000000240254',
+ totalIsIncluded: false,
+ },
+ },
+ ],
+ to: [
+ {
+ symbol: swap.redeem.type,
+ amount: swap.redeem.value / 1e5,
+ fundingNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ operatingNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ finalizeNetworkFee: {
+ total: '0',
+ perUnit: '0',
+ totalIsIncluded: false,
+ },
+ },
+ ],
+ serviceFeePercentage: 0.0025,
+ direction: 'reverse',
+ },
+ hash: '946dc06baf94ee49a1bd026eff8eb4f30d34c9e162211667dbebd5a5282e6294',
+ contracts: [
+ {
+ asset: swap.fund.type,
+ refund: {
+ address: swap.fund.refundAddress,
+ },
+ recipient: {
+ address: swap.fund.refundAddress,
+ },
+ amount: swap.fund.output.value,
+ timeout: expirationTimestamp,
+ direction: 'send',
+ status: 'pending',
+ id: '2MzQo4ehDrSEsxX7RnysLL6VePD3tuNyx4M',
+ intermediary: {},
+ },
+ {
+ asset: swap.redeem.type,
+ refund: {
+ address: swap.redeem.recipient,
+ },
+ recipient: {
+ address: swap.redeem.recipient,
+ },
+ amount: swap.redeem.value,
+ timeout: expirationTimestamp,
+ direction: 'receive',
+ status: 'pending',
+ id: 'eff8a1a5-4f4e-3895-b95c-fd5a40c99001',
+ intermediary: {},
+ },
+ ],
+ }));
+ }
+ }
+
+ return originalFetch(...args);
+ };
+}
+
+/**
+ * Fee per unit helper function
+ */
+function getNetworkFeePerUnit(asset: string): number {
+ switch (asset) {
+ case SwapAsset.BTC:
+ return Math.floor(Math.random() * 100) / 1e8; // 1 - 100 sats/vbyte
+ case SwapAsset.NIM:
+ return 0; // luna per byte
+ case SwapAsset.USDC_MATIC:
+ case SwapAsset.USDT_MATIC:
+ return 1000000000; // 1 Gwei
+ default:
+ return 0;
+ }
+}
+
+let swapInterval: NodeJS.Timeout | null = null;
+
+export function listenForSwapChanges(): void {
+ if (swapInterval) return;
+ swapInterval = setInterval(() => {
+ // Check if there are any active swaps that need to be processed
+ const swap = useSwapsStore().activeSwap.value;
+ if (!swap) return;
+ console.log('[Demo] Active swap:', { swap, state: swap.state });
+ switch (swap.state) {
+ case SwapState.AWAIT_INCOMING:
+ console.log('[Demo] Swap is in AWAIT_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.CREATE_OUTGOING,
+ });
+ break;
+ case SwapState.CREATE_OUTGOING:
+ console.log('[Demo] Swap is in CREATE_OUTGOING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_SECRET,
+ });
+ break;
+ case SwapState.AWAIT_SECRET:
+ console.log('[Demo] Swap is in AWAIT_SECRET state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.SETTLE_INCOMING,
+ });
+ break;
+ case SwapState.SETTLE_INCOMING:
+ console.log('[Demo] Swap is in SETTLE_INCOMING state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.COMPLETE,
+ });
+ completeSwap(swap);
+ break;
+ case SwapState.COMPLETE:
+ console.log('[Demo] Swap is in COMPLETE state');
+ if (swapInterval) clearInterval(swapInterval);
+ swapInterval = null;
+ break;
+ default:
+ console.log('[Demo] Swap is in unknown state');
+ useSwapsStore().setActiveSwap({
+ ...swap,
+ state: SwapState.AWAIT_INCOMING,
+ });
+ break;
+ }
+ }, 1_800);
+}
+
+/**
+ * Completes an active swap by creating transactions for both sides of the swap
+ */
+function completeSwap(activeSwap: any): void {
+ // Add transactions for both sides of the swap
+ const fromAsset = activeSwap.from.asset;
+ const toAsset = activeSwap.to.asset;
+ const fromAmount = activeSwap.from.amount;
+ const toAmount = activeSwap.to.amount;
+
+ // Use the consolidated transaction creation function
+ completeSwapTransactions(fromAsset, fromAmount, toAsset, toAmount);
+
+ console.log('[Demo] Swap completed:', { fromAsset, toAsset, fromAmount, toAmount });
+}
+
+export function addOngoingSwap(swapId: string, swapArgs: SetupSwapArgs): void {
+ onGoingSwaps.set(swapId, swapArgs);
+}
diff --git a/src/lib/demo/DemoTransactions.ts b/src/lib/demo/DemoTransactions.ts
new file mode 100644
index 000000000..73e8518bf
--- /dev/null
+++ b/src/lib/demo/DemoTransactions.ts
@@ -0,0 +1,1130 @@
+import { KeyPair, PlainTransactionDetails, PrivateKey } from '@nimiq/core';
+import { TransactionState as ElectrumTransactionState } from '@nimiq/electrum-client';
+import {
+ type Transaction as NimTransaction,
+ useTransactionsStore,
+ toSecs,
+} from '@/stores/Transactions';
+import {
+ useBtcTransactionsStore,
+ type Transaction as BtcTransaction,
+} from '@/stores/BtcTransactions';
+import {
+ useUsdtTransactionsStore,
+ TransactionState as UsdtTransactionState,
+ type Transaction as UsdtTransaction,
+} from '@/stores/UsdtTransactions';
+import {
+ useUsdcTransactionsStore,
+ TransactionState as UsdcTransactionState,
+ type Transaction as UsdcTransaction,
+} from '@/stores/UsdcTransactions';
+import { useAddressStore } from '@/stores/Address';
+import { useContactsStore } from '@/stores/Contacts';
+import { useBtcAddressStore } from '@/stores/BtcAddress';
+import { useBtcLabelsStore } from '@/stores/BtcLabels';
+import { useUsdcContactsStore } from '@/stores/UsdcContacts';
+import { useUsdtContactsStore } from '@/stores/UsdtContacts';
+import Config from 'config';
+import {
+ nimInitialBalance,
+ btcInitialBalance,
+ usdcInitialBalance,
+ usdtInitialBalance,
+ demoNimAddress,
+ demoBtcAddress,
+ demoPolygonAddress,
+ buyFromAddress,
+} from './DemoConstants';
+import { encodeTextToHex, calculateDaysAgo, getRandomPolygonHash, getRandomPolygonAddress } from './DemoUtils';
+
+// #region NIM Transactions
+
+interface NimTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+}
+
+/**
+ * Defines transaction definitions for demo NIM transactions
+ * Story: A crypto enthusiast who made money with BTC, converted to NIM, and travels the world
+ */
+function defineNimFakeTransactions(): Partial[] {
+ const txDefinitions: NimTransactionDefinition[] = [
+ // Recent transactions - Currently in Europe
+ { fraction: -0.015, daysAgo: 0.2, description: 'Coffee in Saarsbrücken downtown', recipientLabel: 'Café am Markt' },
+ { fraction: -0.03, daysAgo: 0.8, description: 'Train ticket to Saarsbrücken', recipientLabel: 'Deutsche Bahn' },
+ { fraction: -0.025, daysAgo: 1.2, description: 'Hostel in Frankfurt', recipientLabel: 'Frankfurt Backpackers' },
+
+ // Europe - Western & Central
+ { fraction: -0.04, daysAgo: 3, description: 'Dinner in Berlin', recipientLabel: 'Local Bistro' },
+ { fraction: -0.02, daysAgo: 4, description: 'Museum entry in Berlin', recipientLabel: 'Museum Island' },
+ { fraction: -0.035, daysAgo: 6, description: 'Night train to Prague', recipientLabel: 'Czech Railways' },
+ { fraction: -0.03, daysAgo: 8, description: 'Traditional meal in Prague', recipientLabel: 'Old Town Restaurant' },
+ { fraction: -0.045, daysAgo: 12, description: 'Bus to Vienna', recipientLabel: 'FlixBus' },
+
+ // Europe - Mediterranean
+ { fraction: -0.06, daysAgo: 18, description: 'Ferry to Greek Islands', recipientLabel: 'Aegean Sea Lines' },
+ { fraction: -0.04, daysAgo: 22, description: 'Santorini sunset tour', recipientLabel: 'Island Tours' },
+ { fraction: -0.055, daysAgo: 28, description: 'Flight Athens to Istanbul', recipientLabel: 'Turkish Airlines' },
+
+ // Asia - Middle East & Central
+ { fraction: -0.03, daysAgo: 32, description: 'Turkish bath in Istanbul', recipientLabel: 'Galata Hammam' },
+ { fraction: -0.07, daysAgo: 38, description: 'Overland bus to Uzbekistan', recipientLabel: 'Central Asia Transit' },
+ { fraction: -0.035, daysAgo: 45, description: 'Silk Road tour in Samarkand', recipientLabel: 'Heritage Tours' },
+
+ // Asia - South & Southeast
+ { fraction: -0.08, daysAgo: 52, description: 'Flight to Delhi', recipientLabel: 'Air India' },
+ { fraction: -0.025, daysAgo: 58, description: 'Train ticket to Rajasthan', recipientLabel: 'Indian Railways' },
+ { fraction: -0.04, daysAgo: 65, description: 'Camel safari in Jaisalmer', recipientLabel: 'Desert Adventures' },
+ { fraction: -0.06, daysAgo: 72, description: 'Flight to Bangkok', recipientLabel: 'Thai Airways' },
+ { fraction: -0.02, daysAgo: 78, description: 'Bus to Bangkok city center', recipientLabel: 'Bangkok Bus' },
+ { fraction: -0.03, daysAgo: 82, description: 'Street food tour Bangkok', recipientLabel: 'Local Food Guide' },
+ { fraction: -0.045, daysAgo: 88, description: 'Train to Vietnam border', recipientLabel: 'Thai State Railways' },
+
+ // Asia - Vietnam (Nimiq community location)
+ { fraction: -0.035, daysAgo: 95, description: 'Motorbike rental in Ho Chi Minh', recipientLabel: 'Saigon Bikes' },
+ { fraction: -0.025, daysAgo: 98, description: 'Pho lunch in District 1', recipientLabel: 'Pho 2000' },
+ { fraction: -0.04, daysAgo: 105, description: 'Halong Bay cruise', recipientLabel: 'Heritage Cruises' },
+ { fraction: -0.03, daysAgo: 112, description: 'Night bus to Hanoi', recipientLabel: 'Vietnam Bus Lines' },
+
+ // Asia - East
+ { fraction: -0.08, daysAgo: 125, description: 'Flight Hanoi to Tokyo', recipientLabel: 'Vietnam Airlines' },
+ { fraction: -0.05, daysAgo: 132, description: 'JR Pass for bullet trains', recipientLabel: 'JR Central' },
+ { fraction: -0.035, daysAgo: 138, description: 'Sushi dinner in Shibuya', recipientLabel: 'Tokyo Sushi Bar' },
+
+ // Oceania
+ { fraction: -0.12, daysAgo: 148, description: 'Flight Tokyo to Sydney', recipientLabel: 'Qantas Airways' },
+ { fraction: -0.04, daysAgo: 155, description: 'Hostel in Bondi Beach', recipientLabel: 'Bondi Backpackers' },
+ { fraction: -0.045, daysAgo: 162, description: 'Great Barrier Reef tour', recipientLabel: 'Reef Tours Australia' },
+ { fraction: -0.06, daysAgo: 175, description: 'Flight Sydney to Auckland', recipientLabel: 'Air New Zealand' },
+ { fraction: -0.035, daysAgo: 182, description: 'Hobbiton movie set tour', recipientLabel: 'NZ Movie Tours' },
+
+ // Africa - East & South
+ { fraction: -0.15, daysAgo: 195, description: 'Flight Auckland to Nairobi', recipientLabel: 'Kenya Airways' },
+ { fraction: -0.055, daysAgo: 205, description: 'Safari in Masai Mara', recipientLabel: 'African Safaris' },
+ { fraction: -0.04, daysAgo: 215, description: 'Bus to Tanzania border', recipientLabel: 'East Africa Shuttle' },
+ { fraction: -0.06, daysAgo: 225, description: 'Kilimanjaro base camp trek', recipientLabel: 'Mountain Guides' },
+ { fraction: -0.08, daysAgo: 238, description: 'Flight to Cape Town', recipientLabel: 'South African Airways' },
+ { fraction: -0.035, daysAgo: 248, description: 'Table Mountain cable car', recipientLabel: 'SA Tourism' },
+
+ // Africa - West (The Gambia - Nimiq community location)
+ { fraction: -0.1, daysAgo: 265, description: 'Flight Cape Town to Banjul', recipientLabel: 'Brussels Airlines' },
+ { fraction: -0.03, daysAgo: 272, description: 'River cruise in The Gambia', recipientLabel: 'Gambia River Tours' },
+ { fraction: -0.025, daysAgo: 278, description: 'Local market in Banjul', recipientLabel: 'Banjul Market' },
+ { fraction: -0.04, daysAgo: 285, description: 'Cultural tour in Juffureh', recipientLabel: 'Heritage Gambia' },
+
+ // North America - Central America (Costa Rica - Nimiq community location)
+ { fraction: -0.11, daysAgo: 298, description: 'Flight Banjul to San José', recipientLabel: 'TAP Air Portugal' },
+ { fraction: -0.045, daysAgo: 308, description: 'Zip-lining in Monteverde', recipientLabel: 'Costa Rica Adventures' },
+ { fraction: -0.035, daysAgo: 315, description: 'Eco-lodge in Manuel Antonio', recipientLabel: 'Rainforest Lodge' },
+ { fraction: -0.055, daysAgo: 325, description: 'Volcano tour near San José', recipientLabel: 'Volcano Expeditions' },
+
+ // North America - USA & Canada
+ { fraction: -0.08, daysAgo: 345, description: 'Flight San José to Miami', recipientLabel: 'American Airlines' },
+ { fraction: -0.04, daysAgo: 358, description: 'Road trip car rental', recipientLabel: 'Enterprise Rent-A-Car' },
+ { fraction: -0.035, daysAgo: 368, description: 'Grand Canyon entrance fee', recipientLabel: 'National Park Service' },
+ { fraction: -0.06, daysAgo: 385, description: 'Train to Vancouver', recipientLabel: 'Via Rail Canada' },
+ { fraction: -0.04, daysAgo: 398, description: 'Whale watching in Victoria', recipientLabel: 'Pacific Marine Tours' },
+
+ // South America
+ { fraction: -0.12, daysAgo: 418, description: 'Flight Vancouver to Lima', recipientLabel: 'LATAM Airlines' },
+ { fraction: -0.045, daysAgo: 432, description: 'Inca Trail permit', recipientLabel: 'Peru National Parks' },
+ { fraction: -0.06, daysAgo: 445, description: 'Machu Picchu guided tour', recipientLabel: 'Ancient Paths Peru' },
+ { fraction: -0.08, daysAgo: 465, description: 'Bus to La Paz Bolivia', recipientLabel: 'Cruz del Sur' },
+ { fraction: -0.055, daysAgo: 485, description: 'Salt flats tour Uyuni', recipientLabel: 'Uyuni Expeditions' },
+ { fraction: -0.1, daysAgo: 505, description: 'Flight La Paz to Rio', recipientLabel: 'GOL Airlines' },
+ { fraction: -0.04, daysAgo: 518, description: 'Christ the Redeemer visit', recipientLabel: 'Rio Tourism' },
+ { fraction: -0.035, daysAgo: 535, description: 'Copacabana beach day', recipientLabel: 'Beach Vendors' },
+
+ // Antarctica (Research station visit)
+ { fraction: -0.2, daysAgo: 558, description: 'Antarctic expedition cruise', recipientLabel: 'Polar Expeditions' },
+ { fraction: -0.03, daysAgo: 578, description: 'Research station donation', recipientLabel: 'Antarctic Foundation' },
+
+ // Early journey setup - BTC profits and conversion
+ { fraction: 0.15, daysAgo: 600, description: 'Sold BTC at peak for travel fund', recipientLabel: 'Crypto Exchange' },
+ { fraction: 0.18, daysAgo: 650, description: 'Bitcoin mining rewards', recipientLabel: 'Mining Pool' },
+ { fraction: 0.12, daysAgo: 720, description: 'Early BTC investment profit', recipientLabel: 'Digital Assets' },
+ { fraction: 0.25, daysAgo: 800, description: 'BTC sale for world trip', recipientLabel: 'Binance Exchange' },
+
+ // Initial crypto journey
+ { fraction: 0.08, daysAgo: 900, description: 'First successful BTC trade', recipientLabel: 'Trading Platform' },
+ { fraction: 0.2, daysAgo: 1200, description: 'Early Bitcoin purchase gains', recipientLabel: 'Crypto Broker' },
+ { fraction: 0.1, daysAgo: 1400, description: 'Freelance payment in crypto', recipientLabel: 'Tech Client' },
+ ];
+
+ // Calculate sum of existing transactions to ensure they add up to exactly 1
+ const existingSum = txDefinitions.reduce((sum, def) => sum + def.fraction, 0);
+ const remainingFraction = 1 - existingSum;
+
+ // Add the final balancing transaction if needed
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: remainingFraction,
+ daysAgo: 1450,
+ description: remainingFraction > 0
+ ? 'Initial Bitcoin investment'
+ : 'Final trip preparation expense',
+ recipientLabel: remainingFraction > 0 ? 'Genesis Block' : 'Travel Prep',
+ });
+ }
+
+ const { setContact } = useContactsStore();
+ const txs: Partial[] = [];
+
+ for (const def of txDefinitions) {
+ let txValue = Math.floor(nimInitialBalance * def.fraction);
+ // Adjust so it doesn't end in a 0 digit
+ while (txValue > 0 && txValue % 10 === 0) {
+ txValue -= 1;
+ }
+
+ const hex = encodeTextToHex(def.description);
+ const to32Bytes = (h: string) => h.padStart(64, '0').slice(-64);
+ const address = KeyPair.derive(PrivateKey.fromHex(to32Bytes(hex))).toAddress().toUserFriendlyAddress();
+ const recipient = def.fraction > 0 ? demoNimAddress : address;
+ const sender = def.fraction > 0 ? address : demoNimAddress;
+ const data = { type: 'raw', raw: hex } as const;
+ const value = Math.abs(txValue);
+ const timestamp = calculateDaysAgo(def.daysAgo);
+ const tx: Partial = { value, recipient, sender, timestamp, data };
+ txs.push(tx);
+ // Add contact if a recipientLabel is provided
+ if (def.recipientLabel && def.fraction < 0) {
+ setContact(address, def.recipientLabel);
+ }
+ }
+
+ return txs.sort((a, b) => a.timestamp! - b.timestamp!);
+}
+
+let head = 0;
+let nonce = 0;
+
+export function transformNimTransaction(txs: Partial[]): NimTransaction[] {
+ return txs.map((tx) => {
+ head++;
+ nonce++;
+
+ return {
+ network: 'mainnet',
+ state: 'confirmed',
+ transactionHash: `0x${nonce.toString(16)}`,
+ sender: '',
+ senderType: 'basic',
+ recipient: '',
+ recipientType: 'basic',
+ value: 50000000,
+ fee: 0,
+ feePerByte: 0,
+ format: 'basic',
+ validityStartHeight: head,
+ blockHeight: head,
+ flags: 0,
+ timestamp: Date.now(),
+ size: 0,
+ valid: true,
+ proof: { raw: '', type: 'raw' },
+ data: tx.data || { type: 'raw', raw: '' },
+ ...tx,
+ };
+ });
+}
+
+/**
+ * Inserts NIM transactions into the store. If no definitions provided, uses default demo transactions.
+ */
+export function insertFakeNimTransactions(txs = defineNimFakeTransactions()): void {
+ const { addTransactions } = useTransactionsStore();
+ addTransactions(transformNimTransaction(txs));
+}
+
+/**
+ * Updates the NIM balance after a transaction
+ */
+export function updateNimBalance(amount: number): void {
+ const addressStore = useAddressStore();
+ const currentAddressInfo = addressStore.addressInfos.value.find((info) => info.address === demoNimAddress);
+
+ if (currentAddressInfo) {
+ const newBalance = (currentAddressInfo.balance || 0) + amount;
+ addressStore.patchAddress(demoNimAddress, { balance: newBalance });
+ } else {
+ console.error('[Demo] Failed to update NIM balance: Address not found');
+ }
+}
+
+export function dangerouslyInsertFakeBuyNimTransaction(amount: number): void {
+ const tx: Partial = {
+ value: amount,
+ recipient: demoNimAddress,
+ sender: buyFromAddress,
+ data: {
+ type: 'raw',
+ raw: encodeTextToHex('NIM Bank purchase'),
+ },
+ };
+
+ setTimeout(() => {
+ const { addTransactions } = useTransactionsStore();
+ addTransactions(transformNimTransaction([tx]));
+ updateNimBalance(amount);
+ }, 1_500);
+}
+
+// #endregion
+
+// #region BTC Transactions
+
+interface BtcTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
+ address: string;
+}
+
+/**
+ * Defines transaction definitions for demo BTC transactions.
+ * Story: The BTC journey that funded the world travel adventure
+ */
+function defineBtcFakeTransactions(): BtcTransaction[] {
+ const txDefinitions: BtcTransactionDefinition[] = [
+ // Recent BTC usage during travel
+ {
+ fraction: -0.05,
+ daysAgo: 50,
+ description: 'Emergency flight change in Bangkok',
+ incoming: false,
+ address: '1TravelEmergencyBangkok5KJH9x2mN8qP',
+ recipientLabel: 'Bangkok Airways',
+ },
+ {
+ fraction: -0.08,
+ daysAgo: 150,
+ description: 'Luxury hotel in Sydney with BTC',
+ incoming: false,
+ address: '1SydneyLuxuryHotel7P8qR3mK5nB2',
+ recipientLabel: 'Sydney Grand Hotel',
+ },
+ {
+ fraction: -0.1,
+ daysAgo: 280,
+ description: 'Private safari booking in Kenya',
+ incoming: false,
+ address: '1KenyaSafariLodge9M4tX6pL8cQ7',
+ recipientLabel: 'Mara Safari Lodge',
+ },
+
+ // Major BTC sales that funded the trip
+ {
+ fraction: -0.3,
+ daysAgo: 590,
+ description: 'BTC to NIM conversion for world trip',
+ incoming: false,
+ address: '1NimiqExchangeConvert12Kx8B9mP',
+ recipientLabel: 'NIM Exchange',
+ },
+ {
+ fraction: -0.25,
+ daysAgo: 620,
+ description: 'Sold BTC for travel emergency fund',
+ incoming: false,
+ address: '1TravelFundExchange45mQ7pK8Nx',
+ recipientLabel: 'Crypto Travel Fund',
+ },
+
+ // BTC accumulation period
+ {
+ fraction: 0.2,
+ daysAgo: 800,
+ description: 'Mining pool rewards from home setup',
+ incoming: true,
+ address: '1MiningPoolRewards78pL3nK9Mx',
+ recipientLabel: 'Home Mining Pool',
+ },
+ {
+ fraction: 0.15,
+ daysAgo: 1000,
+ description: 'DeFi yield farming profits',
+ incoming: true,
+ address: '1DeFiYieldPlatform56nB2mL8Kx',
+ recipientLabel: 'Compound Finance',
+ },
+ {
+ fraction: 0.12,
+ daysAgo: 1200,
+ description: 'Freelance web development payment',
+ incoming: true,
+ address: '1FreelanceClientBTC89pM3nK7',
+ recipientLabel: 'Tech Startup Client',
+ },
+ {
+ fraction: 0.18,
+ daysAgo: 1400,
+ description: 'Bitcoin bull run profits',
+ incoming: true,
+ address: '1BullRunGains2021mK8pL5nQ3',
+ recipientLabel: 'Trading Exchange',
+ },
+ {
+ fraction: 0.25,
+ daysAgo: 1600,
+ description: 'Early Bitcoin investment from 2017',
+ incoming: true,
+ address: '1EarlyBTCInvestment34mN9pK8',
+ recipientLabel: 'Coinbase Pro',
+ },
+ {
+ fraction: 0.08,
+ daysAgo: 1800,
+ description: 'First Bitcoin purchase',
+ incoming: true,
+ address: '1FirstBTCPurchase567pL2nM8',
+ recipientLabel: 'Local Bitcoin ATM',
+ },
+ ].sort((a, b) => b.daysAgo - a.daysAgo);
+
+ // If the sum of fractions does not add up to 1, add a balancing transaction.
+ const existingSum = txDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const remainingFraction = 1 - existingSum;
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: Math.abs(remainingFraction),
+ daysAgo: 2000,
+ description: remainingFraction > 0 ? 'Genesis block mining reward' : 'Final preparation',
+ incoming: remainingFraction > 0,
+ address: remainingFraction > 0 ? '1GenesisMining2009Satoshi12' : '1FinalPrep2024Ready567',
+ recipientLabel: remainingFraction > 0 ? 'Genesis Mining' : 'Preparation Fund',
+ });
+ }
+
+ return transformBtcTransaction(txDefinitions);
+}
+
+/**
+ * Transforms BTC transaction definitions into actual transactions.
+ */
+export function transformBtcTransaction(txDefinitions: BtcTransactionDefinition[]): BtcTransaction[] {
+ const transactions = [];
+ const knownUtxos = new Map();
+ let txCounter = 1;
+
+ for (const def of txDefinitions) {
+ const txHash = `btc-tx-${txCounter++}`;
+ // Compute the value using the initial BTC balance and the fraction.
+ const value = Math.floor(btcInitialBalance * Math.abs(def.fraction));
+
+ const tx: BtcTransaction = {
+ addresses: [demoBtcAddress],
+ isCoinbase: false,
+ inputs: [
+ {
+ // For incoming tx, use the external address; for outgoing, use our demo address.
+ address: def.incoming ? def.address : demoBtcAddress,
+ outputIndex: 0,
+ index: 0,
+ script: 'script',
+ sequence: 4294967295,
+ transactionHash: def.incoming ? txHash : (getUTXOToSpend(knownUtxos)?.txHash || txHash),
+ witness: ['witness'],
+ },
+ ],
+ outputs: def.incoming
+ ? [
+ {
+ value,
+ address: demoBtcAddress,
+ script: 'script',
+ index: 0,
+ },
+ ]
+ : [
+ {
+ value,
+ address: def.address,
+ script: 'script',
+ index: 0,
+ },
+ {
+ // Change output.
+ value: 1000000,
+ address: demoBtcAddress,
+ script: 'script',
+ index: 1,
+ },
+ ],
+ transactionHash: txHash,
+ version: 1,
+ vsize: 200,
+ weight: 800,
+ locktime: 0,
+ confirmations: Math.max(1, Math.floor(10 - def.daysAgo / 200)),
+ replaceByFee: false,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ state: ElectrumTransactionState.CONFIRMED,
+ };
+
+ updateUTXOs(knownUtxos, tx);
+ transactions.push(tx);
+
+ // Set labels if a recipient label is provided.
+ if (def.recipientLabel) {
+ const { setSenderLabel } = useBtcLabelsStore();
+ setSenderLabel(def.incoming ? def.address : demoBtcAddress, def.recipientLabel);
+ }
+ }
+
+ // Update the address store with the current UTXOs.
+ const { addAddressInfos } = useBtcAddressStore();
+ addAddressInfos([{
+ address: demoBtcAddress,
+ txoCount: transactions.length + 2, // Total number of outputs received.
+ utxos: Array.from(knownUtxos.values()),
+ }]);
+
+ return transactions;
+}
+
+/**
+ * Tracks UTXO changes for BTC transactions.
+ */
+function updateUTXOs(knownUtxos: Map, tx: any): void {
+ // Remove spent inputs.
+ for (const input of tx.inputs) {
+ if (input.address === demoBtcAddress) {
+ const utxoKey = `${input.transactionHash}:${input.outputIndex}`;
+ knownUtxos.delete(utxoKey);
+ }
+ }
+
+ // Add new outputs for our address.
+ for (const output of tx.outputs) {
+ if (output.address === demoBtcAddress) {
+ const utxoKey = `${tx.transactionHash}:${output.index}`;
+ knownUtxos.set(utxoKey, {
+ transactionHash: tx.transactionHash,
+ index: output.index,
+ witness: {
+ script: output.script,
+ value: output.value,
+ },
+ });
+ }
+ }
+}
+
+/**
+ * Helper to get a UTXO to spend.
+ */
+function getUTXOToSpend(knownUtxos: Map): any {
+ if (knownUtxos.size === 0) return null;
+ const utxo = knownUtxos.values().next().value;
+ return {
+ txHash: utxo.transactionHash,
+ index: utxo.index,
+ value: utxo.witness.value,
+ };
+}
+
+/**
+ * Insert fake BTC transactions into the store.
+ */
+export function insertFakeBtcTransactions(txs = defineBtcFakeTransactions()): void {
+ const { addTransactions } = useBtcTransactionsStore();
+ addTransactions(txs);
+}
+
+/**
+ * Updates the BTC address balance by adding a new UTXO
+ */
+export function updateBtcBalance(amount: number): void {
+ const btcAddressStore = useBtcAddressStore();
+ const addressInfo = btcAddressStore.state.addressInfos[demoBtcAddress];
+
+ if (!addressInfo) {
+ console.error('[Demo] Failed to update BTC balance: Address not found');
+ return;
+ }
+
+ // Create a unique transaction hash
+ const txHash = `btc-tx-swap-${Date.now().toString(16)}`;
+
+ // Create a proper UTXO with the correct format
+ const newUtxo = {
+ transactionHash: txHash,
+ index: 0,
+ witness: {
+ script: 'script',
+ value: amount * 1e-6,
+ },
+ txoValue: amount * 1e-6,
+ };
+
+ btcAddressStore.addAddressInfos([{
+ address: demoBtcAddress,
+ utxos: [...(addressInfo.utxos || []), newUtxo],
+ }]);
+
+ console.log(`[Demo] Updated BTC balance, added UTXO with value: ${amount * 1e-6}`);
+}
+
+// #endregion
+
+// #region Polygon Transactions
+
+interface UsdcTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
+}
+
+/**
+ * Defines transaction definitions for demo USDC transactions
+ * Story: Strategic stablecoin usage during world travel for specific situations
+ */
+function defineUsdcFakeTransactions(): UsdcTransaction[] {
+ const txDefinitions: UsdcTransactionDefinition[] = [
+ // Recent stablecoin usage for stability
+ {
+ fraction: -0.12,
+ daysAgo: 25,
+ description: 'Hotel deposit in Berlin (stable value needed)',
+ incoming: false,
+ recipientLabel: 'Berlin Luxury Hotel',
+ },
+ {
+ fraction: -0.08,
+ daysAgo: 85,
+ description: 'Travel insurance premium',
+ incoming: false,
+ recipientLabel: 'Global Travel Insurance',
+ },
+
+ // Asia - Used for larger expenses where stability matters
+ {
+ fraction: -0.15,
+ daysAgo: 120,
+ description: 'Long-term accommodation in Tokyo',
+ incoming: false,
+ recipientLabel: 'Tokyo Monthly Rental',
+ },
+ {
+ fraction: -0.1,
+ daysAgo: 180,
+ description: 'Medical check-up in Singapore',
+ incoming: false,
+ recipientLabel: 'Singapore General Hospital',
+ },
+
+ // Africa - Stable value for major bookings
+ {
+ fraction: -0.18,
+ daysAgo: 250,
+ description: 'Adventure tour package in Cape Town',
+ incoming: false,
+ recipientLabel: 'African Adventure Co',
+ },
+
+ // Americas - Large bookings and official payments
+ {
+ fraction: -0.14,
+ daysAgo: 320,
+ description: 'National park tour package Costa Rica',
+ incoming: false,
+ recipientLabel: 'Costa Rica Nature Tours',
+ },
+ {
+ fraction: -0.16,
+ daysAgo: 380,
+ description: 'Car rental deposit for US road trip',
+ incoming: false,
+ recipientLabel: 'US Car Rental Deposit',
+ },
+
+ // Initial funding and DeFi activities
+ {
+ fraction: 0.25,
+ daysAgo: 450,
+ description: 'Liquidity mining rewards',
+ incoming: true,
+ recipientLabel: 'Uniswap LP Rewards',
+ },
+ {
+ fraction: 0.2,
+ daysAgo: 550,
+ description: 'Stable income from yield farming',
+ incoming: true,
+ recipientLabel: 'Aave Lending Pool',
+ },
+ {
+ fraction: 0.18,
+ daysAgo: 650,
+ description: 'Converted BTC profits to stablecoin',
+ incoming: true,
+ recipientLabel: 'Crypto Conversion',
+ },
+ {
+ fraction: 0.3,
+ daysAgo: 800,
+ description: 'Initial stablecoin purchase for travel',
+ incoming: true,
+ recipientLabel: 'Coinbase USDC',
+ },
+ ];
+
+ const existingSum = txDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const remainingFraction = 1 - existingSum;
+
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: Math.abs(remainingFraction),
+ daysAgo: remainingFraction > 0 ? 900 : 950,
+ description: remainingFraction > 0 ? 'DeFi protocol rewards' : 'Travel preparation funds',
+ incoming: remainingFraction > 0,
+ recipientLabel: remainingFraction > 0 ? 'DeFi Platform' : 'Preparation Expense',
+ });
+ }
+
+ return transformUsdcTransaction(txDefinitions);
+}
+
+/**
+ * Transform USDC transaction definitions into actual transactions
+ */
+function transformUsdcTransaction(txDefinitions: UsdcTransactionDefinition[]): UsdcTransaction[] {
+ return txDefinitions.map((def, index) => {
+ const value = Math.floor(usdcInitialBalance * Math.abs(def.fraction));
+ const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
+ const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
+
+ if (def.recipientLabel) {
+ const { setContact } = useUsdcContactsStore();
+ setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
+ }
+
+ return {
+ token: Config.polygon.usdc.tokenContract,
+ transactionHash: getRandomPolygonHash(),
+ logIndex: index,
+ sender: randomAddress,
+ recipient: recipientAddress,
+ value,
+ state: UsdcTransactionState.CONFIRMED,
+ blockHeight: 1000000 + index,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ };
+ });
+}
+
+/**
+ * Insert fake USDC transactions into the store
+ */
+export function insertFakeUsdcTransactions(txs = defineUsdcFakeTransactions()): void {
+ const { addTransactions } = useUsdcTransactionsStore();
+ addTransactions(txs);
+}
+
+interface UsdtTransactionDefinition {
+ fraction: number;
+ daysAgo: number;
+ description: string;
+ recipientLabel?: string;
+ incoming: boolean;
+}
+
+/**
+ * Defines transaction definitions for demo USDT transactions
+ * Story: Additional stablecoin for trading and specific travel situations
+ */
+function defineUsdtFakeTransactions(): UsdtTransaction[] {
+ const txDefinitions: UsdtTransactionDefinition[] = [
+ // Recent trading and travel activities
+ {
+ fraction: 0.18,
+ daysAgo: 15,
+ description: 'Arbitrage trading profits Europe',
+ incoming: true,
+ recipientLabel: 'European DEX',
+ },
+ {
+ fraction: -0.09,
+ daysAgo: 45,
+ description: 'Emergency funds transfer to family',
+ incoming: false,
+ recipientLabel: 'Family Support',
+ },
+
+ // Asia - Trading and remittances
+ {
+ fraction: 0.15,
+ daysAgo: 110,
+ description: 'Crypto trading gains in Vietnam',
+ incoming: true,
+ recipientLabel: 'Vietnam Crypto Exchange',
+ },
+ {
+ fraction: -0.12,
+ daysAgo: 135,
+ description: 'Online course payment from Japan',
+ incoming: false,
+ recipientLabel: 'Japanese Language Course',
+ },
+
+ // Oceania - Stable payments for services
+ {
+ fraction: -0.08,
+ daysAgo: 170,
+ description: 'Work visa processing fee Australia',
+ incoming: false,
+ recipientLabel: 'Australian Immigration',
+ },
+
+ // Africa - Remittances and local payments
+ {
+ fraction: -0.06,
+ daysAgo: 290,
+ description: 'Local guide payment in The Gambia',
+ incoming: false,
+ recipientLabel: 'Local Gambian Guide',
+ },
+ {
+ fraction: 0.1,
+ daysAgo: 310,
+ description: 'Freelance consulting payment',
+ incoming: true,
+ recipientLabel: 'African Tech Startup',
+ },
+
+ // Americas - Cross-border payments
+ {
+ fraction: -0.14,
+ daysAgo: 350,
+ description: 'University course enrollment Costa Rica',
+ incoming: false,
+ recipientLabel: 'Universidad de Costa Rica',
+ },
+ {
+ fraction: 0.12,
+ daysAgo: 420,
+ description: 'Remote work payment from US client',
+ incoming: true,
+ recipientLabel: 'US Tech Company',
+ },
+
+ // South America - Local services
+ {
+ fraction: -0.07,
+ daysAgo: 480,
+ description: 'Altitude sickness treatment Bolivia',
+ incoming: false,
+ recipientLabel: 'Bolivian Medical Center',
+ },
+
+ // Initial funding and DeFi
+ {
+ fraction: 0.2,
+ daysAgo: 580,
+ description: 'Lending protocol rewards',
+ incoming: true,
+ recipientLabel: 'Compound Protocol',
+ },
+ {
+ fraction: 0.15,
+ daysAgo: 720,
+ description: 'Stable arbitrage trading profits',
+ incoming: true,
+ recipientLabel: 'Cross-DEX Arbitrage',
+ },
+ {
+ fraction: 0.16,
+ daysAgo: 850,
+ description: 'Initial USDT for trading capital',
+ incoming: true,
+ recipientLabel: 'Binance USDT',
+ },
+ ];
+
+ const existingSum = txDefinitions.reduce((sum, def) =>
+ sum + (def.incoming ? Math.abs(def.fraction) : -Math.abs(def.fraction)), 0);
+ const remainingFraction = 1 - existingSum;
+
+ if (Math.abs(remainingFraction) > 0.001) {
+ txDefinitions.push({
+ fraction: Math.abs(remainingFraction),
+ daysAgo: remainingFraction > 0 ? 950 : 1000,
+ description: remainingFraction > 0 ? 'Initial trading capital' : 'Trading preparation',
+ incoming: remainingFraction > 0,
+ recipientLabel: remainingFraction > 0 ? 'Trading Platform' : 'Trading Preparation',
+ });
+ }
+
+ return transformUsdtTransaction(txDefinitions);
+}
+
+/**
+ * Transform USDT transaction definitions into actual transactions
+ */
+function transformUsdtTransaction(txDefinitions: UsdtTransactionDefinition[]): UsdtTransaction[] {
+ return txDefinitions.map((def, index) => {
+ const value = Math.floor(usdtInitialBalance * Math.abs(def.fraction));
+ const randomAddress = def.incoming ? getRandomPolygonAddress() : demoPolygonAddress;
+ const recipientAddress = def.incoming ? demoPolygonAddress : getRandomPolygonAddress();
+
+ if (def.recipientLabel) {
+ const { setContact } = useUsdtContactsStore();
+ setContact(def.incoming ? randomAddress : recipientAddress, def.recipientLabel);
+ }
+
+ return {
+ token: Config.polygon.usdt_bridged.tokenContract,
+ transactionHash: getRandomPolygonHash(),
+ logIndex: index,
+ sender: randomAddress,
+ recipient: recipientAddress,
+ value,
+ state: UsdtTransactionState.CONFIRMED,
+ blockHeight: 1000000 + index,
+ timestamp: toSecs(calculateDaysAgo(def.daysAgo)),
+ };
+ });
+}
+
+/**
+ * Insert fake USDT transactions into the store
+ */
+export function insertFakeUsdtTransactions(txs = defineUsdtFakeTransactions()): void {
+ const { addTransactions } = useUsdtTransactionsStore();
+ addTransactions(txs);
+}
+
+// #endregion
+
+// #region Staking Transactions
+
+/**
+ * Creates a staking transaction for the demo environment
+ */
+export function createStakeTransaction(amount: number, validatorAddress: string): Partial {
+ return {
+ value: amount,
+ recipient: validatorAddress,
+ sender: demoNimAddress,
+ timestamp: Date.now(),
+ data: { type: 'add-stake', raw: '', staker: demoNimAddress },
+ };
+}
+
+// #endregion
+
+// #region Swap Transactions
+
+/**
+ * Creates the outgoing transaction for a swap operation
+ */
+export function createSwapOutgoingTransaction(
+ fromAsset: string,
+ fromAmount: number,
+ toAsset: string,
+ swapHash: string,
+): {
+ transaction: Partial | any,
+ updateBalance: () => void,
+} {
+ const now = Date.now();
+ const nowSecs = Math.floor(now / 1000);
+
+ switch (fromAsset) {
+ case 'NIM': {
+ // Create a unique transaction hash for the NIM transaction
+ const nimTxHash = `nim-swap-${Math.random().toString(16).slice(2, 10)}`;
+
+ // Create HTLC data that would be needed for a real swap
+ const nimHtlcAddress = `NQ${Math.random().toString(36).slice(2, 34)}`;
+
+ // Create the NIM transaction with proper HTLC data
+ const transaction: Partial = {
+ value: fromAmount,
+ recipient: nimHtlcAddress,
+ sender: demoNimAddress,
+ timestamp: now,
+ transactionHash: nimTxHash,
+ data: {
+ type: 'htlc',
+ hashAlgorithm: 'sha256',
+ hashCount: 1,
+ hashRoot: swapHash,
+ raw: encodeTextToHex(`Swap ${fromAsset}-${toAsset}`),
+ recipient: nimHtlcAddress,
+ sender: demoNimAddress,
+ timeout: nowSecs + 3600,
+ },
+ };
+
+ return {
+ transaction,
+ updateBalance: () => updateNimBalance(-fromAmount),
+ };
+ }
+ case 'BTC': {
+ // Create the BTC transaction definition
+ const transaction = {
+ address: `1HTLC${Math.random().toString(36).slice(2, 30)}`,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: fromAmount / btcInitialBalance,
+ incoming: false,
+ recipientLabel: 'Bitcoin HTLC',
+ };
+
+ return {
+ transaction,
+ updateBalance: () => updateBtcBalance(-fromAmount),
+ };
+ }
+ default: {
+ throw new Error(`Unsupported asset type for swap: ${fromAsset}`);
+ }
+ }
+}
+
+/**
+ * Creates the incoming (settlement) transaction for a swap operation
+ */
+export function createSwapIncomingTransaction(
+ toAsset: string,
+ toAmount: number,
+ fromAsset: string,
+ swapHash: string,
+ htlcAddress?: string,
+): {
+ transaction: Partial | any,
+ updateBalance: () => void,
+} {
+ const now = Date.now();
+ const nowSecs = Math.floor(now / 1000);
+
+ switch (toAsset) {
+ case 'NIM': {
+ // Create a unique transaction hash for the NIM settlement transaction
+ const nimSettlementTxHash = `nim-settle-${Math.random().toString(16).slice(2, 10)}`;
+
+ // Create the NIM settlement transaction
+ const transaction: Partial = {
+ value: toAmount,
+ recipient: demoNimAddress,
+ sender: htlcAddress || 'HTLC-ADDRESS',
+ timestamp: now + 1000, // Slightly after the funding tx
+ transactionHash: nimSettlementTxHash,
+ data: {
+ type: 'htlc',
+ hashAlgorithm: 'sha256',
+ hashCount: 1,
+ hashRoot: swapHash,
+ raw: encodeTextToHex(`Swap ${fromAsset}-${toAsset}`),
+ recipient: demoNimAddress,
+ sender: htlcAddress || 'HTLC-ADDRESS',
+ timeout: nowSecs + 3600,
+ },
+ };
+
+ return {
+ transaction,
+ updateBalance: () => updateNimBalance(toAmount),
+ };
+ }
+ case 'BTC': {
+ // Create the BTC settlement transaction
+ const transaction = {
+ address: demoBtcAddress,
+ daysAgo: 0,
+ description: `Swap ${fromAsset} to ${toAsset}`,
+ fraction: toAmount / btcInitialBalance,
+ incoming: true,
+ recipientLabel: 'BTC Settlement',
+ };
+
+ return {
+ transaction,
+ updateBalance: () => updateBtcBalance(toAmount),
+ };
+ }
+ default: {
+ throw new Error(`Unsupported asset type for swap: ${toAsset}`);
+ }
+ }
+}
+
+/**
+ * Completes a swap by creating transactions for both sides and updating balances
+ */
+export function completeSwapTransactions(
+ fromAsset: string,
+ fromAmount: number,
+ toAsset: string,
+ toAmount: number,
+): void {
+ // Generate a unique hash for this swap to connect both sides
+ const swapHash = `swap-${Math.random().toString(36).slice(2, 10)}`;
+
+ // Create outgoing transaction (from asset)
+ const outgoing = createSwapOutgoingTransaction(
+ fromAsset,
+ fromAmount,
+ toAsset,
+ swapHash,
+ );
+
+ if (fromAsset === 'NIM') {
+ insertFakeNimTransactions(
+ transformNimTransaction([
+ outgoing.transaction as Partial,
+ ]),
+ );
+ } else if (fromAsset === 'BTC') {
+ const transformedBtcTx = transformBtcTransaction([outgoing.transaction]);
+ const { addTransactions } = useBtcTransactionsStore();
+ addTransactions(transformedBtcTx);
+ }
+ outgoing.updateBalance();
+
+ // Create incoming transaction (to asset) - with a slight delay for realism
+ setTimeout(() => {
+ const incoming = createSwapIncomingTransaction(
+ toAsset,
+ toAmount,
+ fromAsset,
+ swapHash,
+ );
+
+ if (toAsset === 'NIM') {
+ insertFakeNimTransactions(
+ transformNimTransaction([
+ incoming.transaction as Partial,
+ ]),
+ );
+ } else if (toAsset === 'BTC') {
+ const transformedBtcTx = transformBtcTransaction([incoming.transaction]);
+ const { addTransactions } = useBtcTransactionsStore();
+ addTransactions(transformedBtcTx);
+ }
+ incoming.updateBalance();
+ }, 1000);
+
+ console.log('[Demo] Swap completed:', {
+ swapHash,
+ fromAsset,
+ toAsset,
+ fromAmount,
+ toAmount,
+ });
+}
+
+// #endregion
diff --git a/src/lib/demo/DemoUtils.ts b/src/lib/demo/DemoUtils.ts
new file mode 100644
index 000000000..51ff77325
--- /dev/null
+++ b/src/lib/demo/DemoUtils.ts
@@ -0,0 +1,37 @@
+import { Utf8Tools } from '@nimiq/utils';
+
+/**
+ * Returns the hex encoding of a UTF-8 string.
+ */
+export function encodeTextToHex(text: string): string {
+ const utf8Array = Utf8Tools.stringToUtf8ByteArray(text);
+ return Array.from(utf8Array)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('');
+}
+
+// We pick a random but fixed time-of-day offset for each day.
+const baseDate = new Date();
+baseDate.setHours(0, 0, 0, 0);
+const baseDateMs = baseDate.getTime();
+const oneDayMs = 24 * 60 * 60 * 1000;
+
+/**
+ * Generates a past timestamp for a given number of days ago, adding a predictable random offset.
+ */
+export function calculateDaysAgo(days: number): number {
+ const x = Math.sin(days) * 10000;
+ const fractionalPart = x - Math.floor(x);
+ const randomPart = Math.floor(fractionalPart * oneDayMs);
+ return baseDateMs - days * oneDayMs - randomPart;
+}
+
+/**
+ * Generates a random Polygon transaction hash
+ */
+export const getRandomPolygonHash = () => `0x${Math.random().toString(16).slice(2, 66)}`;
+
+/**
+ * Generates a random Polygon address
+ */
+export const getRandomPolygonAddress = () => `0x${Math.random().toString(16).slice(2, 42)}`;
diff --git a/src/lib/demo/index.ts b/src/lib/demo/index.ts
new file mode 100644
index 000000000..18c38b824
--- /dev/null
+++ b/src/lib/demo/index.ts
@@ -0,0 +1,171 @@
+/* eslint-disable max-len, consistent-return, no-console, no-async-promise-executor */
+
+/**
+ * Demo Mode for the Nimiq Wallet
+ * ------------------------------
+ *
+ * This module provides a complete demo environment for the Nimiq Wallet, allowing users to
+ * try out wallet features without connecting to real blockchain networks.
+ *
+ * Demo mode can only be activated through build commands:
+ * - yarn serve:demo (development)
+ * - yarn build:demo (production build)
+ * - yarn build:demo-production (production build with mainnet config)
+ *
+ * When active, demo mode:
+ * - Creates fake accounts with demo balances
+ * - Generates fake transaction history
+ * - Simulates blockchain interactions like sending, receiving, and swapping
+ * - Obfuscates addresses and disables certain features that wouldn't work in demo mode
+ * - Redirects certain actions to explainer modals
+ *
+ * This is intended for demonstration, educational, and testing purposes only.
+*/
+
+import VueRouter from 'vue-router';
+import { useConfig } from '@/composables/useConfig';
+import { checkIfDemoIsActive, DemoState, DemoModal, MessageEventName, demoRoutes, type DemoFlowMessage, type DemoFlowType } from './DemoConstants';
+import { insertCustomDemoStyles, setupSingleMutationObserver } from './DemoDom';
+import { setupDemoAddresses, setupDemoAccount } from './DemoAccounts';
+import {
+ insertFakeNimTransactions,
+ dangerouslyInsertFakeBuyNimTransaction,
+ insertFakeBtcTransactions,
+ insertFakeUsdcTransactions,
+ insertFakeUsdtTransactions,
+ createStakeTransaction,
+ createSwapOutgoingTransaction,
+ createSwapIncomingTransaction,
+ completeSwapTransactions,
+ transformNimTransaction,
+ transformBtcTransaction,
+ updateNimBalance,
+ updateBtcBalance,
+} from './DemoTransactions';
+import { replaceBuyNimFlow, replaceStakingFlow } from './DemoFlows';
+import { interceptFetchRequest, listenForSwapChanges } from './DemoSwaps';
+
+// Keep a reference to the router here
+let demoRouter: VueRouter;
+
+// Demo modal imports for dynamic loading
+const DemoFallbackModal = () =>
+ import(
+ /* webpackChunkName: 'demo-hub-fallback-modal' */
+ '@/components/modals/demos/DemoModalFallback.vue'
+ );
+
+const DemoPurchaseModal = () =>
+ import(
+ /* webpackChunkName: 'demo-modal-buy' */
+ '@/components/modals/demos/DemoModalBuy.vue'
+ );
+
+/**
+ * Initializes the demo environment and sets up various routes, data, and watchers.
+ */
+export function dangerouslyInitializeDemo(router: VueRouter): void {
+ // Check if demo is active according to the configuration
+ if (!checkIfDemoIsActive()) {
+ console.info('[Demo] Demo mode not enabled in configuration. Skipping initialization.');
+ return;
+ }
+
+ console.warn('[Demo] Initializing demo environment...');
+
+ demoRouter = router;
+
+ insertCustomDemoStyles();
+ setupSingleMutationObserver(demoRouter);
+ addDemoModalRoutes();
+ interceptFetchRequest();
+
+ setupDemoAddresses();
+ setupDemoAccount();
+
+ insertFakeNimTransactions();
+ insertFakeBtcTransactions();
+
+ if (useConfig().config.polygon.enabled) {
+ insertFakeUsdcTransactions();
+ insertFakeUsdtTransactions();
+ }
+
+ attachIframeListeners();
+ replaceStakingFlow(demoRouter);
+ replaceBuyNimFlow(demoRouter);
+
+ listenForSwapChanges();
+}
+
+/**
+ * Adds routes pointing to our demo modals.
+ */
+function addDemoModalRoutes(): void {
+ demoRouter.addRoute('root', {
+ name: DemoModal.Fallback,
+ path: `/${DemoModal.Fallback}`,
+ components: { modal: DemoFallbackModal },
+ props: { modal: true },
+ });
+ demoRouter.addRoute('root', {
+ name: DemoModal.Buy,
+ path: `/${DemoModal.Buy}`,
+ components: { modal: DemoPurchaseModal },
+ props: { modal: true },
+ });
+}
+
+/**
+ * Listens for messages from iframes (or parent frames) about changes in the user flow.
+ */
+function attachIframeListeners(): void {
+ window.addEventListener('message', (event) => {
+ if (!event.data || typeof event.data !== 'object') return;
+ const { kind, data } = event.data as DemoFlowMessage;
+ if (kind === MessageEventName.FlowChange && demoRoutes[data]) {
+ // Dynamic import to avoid circular dependencies
+ import('@/stores/Account').then(({ useAccountStore }) => {
+ import('@nimiq/utils').then(({ CryptoCurrency }) => {
+ useAccountStore().setActiveCurrency(CryptoCurrency.NIM);
+ });
+ });
+
+ demoRouter.push({
+ path: demoRoutes[data],
+ });
+ }
+ });
+
+ demoRouter.afterEach((to) => {
+ const match = Object.entries(demoRoutes).find(([, route]) => route === to.path);
+ if (!match) return;
+ window.parent.postMessage({ kind: MessageEventName.FlowChange, data: match[0] as DemoFlowType }, '*');
+ });
+}
+
+// Export types and constants for backward compatibility
+export type { DemoState, DemoFlowType, DemoFlowMessage };
+export { checkIfDemoIsActive, DemoModal, MessageEventName, demoRoutes };
+
+// Export the main transaction insertion function that was exposed in the original module
+export { dangerouslyInsertFakeBuyNimTransaction };
+
+// Export transaction creation functions
+export {
+ createStakeTransaction,
+ createSwapOutgoingTransaction,
+ createSwapIncomingTransaction,
+ completeSwapTransactions,
+ transformNimTransaction,
+ transformBtcTransaction,
+ insertFakeNimTransactions,
+ insertFakeBtcTransactions,
+ insertFakeUsdcTransactions,
+ insertFakeUsdtTransactions,
+ updateNimBalance,
+ updateBtcBalance,
+} from './DemoTransactions';
+
+// Export the DemoHubApi class
+export { DemoHubApi } from './DemoHubApi';