From 8afa54bebdff0a48404b1f28bf262dac9d0d13ed Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 11:38:52 +0200
Subject: [PATCH 01/52] Fix eslint config for api
---
api/.eslintrc | 38 --------------------------------------
api/.eslintrc.js | 39 +++++++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+), 38 deletions(-)
delete mode 100644 api/.eslintrc
create mode 100644 api/.eslintrc.js
diff --git a/api/.eslintrc b/api/.eslintrc
deleted file mode 100644
index ccbfcfc65..000000000
--- a/api/.eslintrc
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "parser": "@typescript-eslint/parser",
- "plugins": ["@typescript-eslint"],
- "rules": {
- "no-console": 0,
- "no-underscore-dangle": 0,
- "no-unused-vars": "off",
- "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "next" }],
- "no-use-before-define": "off",
- "@typescript-eslint/no-use-before-define": ["error", { "variables": false }],
- "no-multi-str": 0,
- "import/extensions": [
- "error",
- "ignorePackages",
- {
- "ts": "never"
- }
- ]
- },
- "env": {
- "es2020": true,
- "node": true,
- "mocha": true
- },
- "parserOptions": {
- "ecmaVersion": 8,
- "sourceType": "module",
- "project": "./tsconfig.json"
- },
- "extends": ["airbnb-base", "prettier", "plugin:@typescript-eslint/recommended"],
- "settings": {
- "import/resolver": {
- "node": {
- "extensions": [".ts"]
- }
- }
- }
-}
diff --git a/api/.eslintrc.js b/api/.eslintrc.js
new file mode 100644
index 000000000..c5698455b
--- /dev/null
+++ b/api/.eslintrc.js
@@ -0,0 +1,39 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint'],
+ rules: {
+ 'no-console': 0,
+ 'no-underscore-dangle': 0,
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: 'next' }],
+ 'no-use-before-define': 'off',
+ '@typescript-eslint/no-use-before-define': ['error', { variables: false }],
+ 'no-multi-str': 0,
+ 'import/extensions': [
+ 'error',
+ 'ignorePackages',
+ {
+ ts: 'never',
+ },
+ ],
+ },
+ env: {
+ es2020: true,
+ node: true,
+ mocha: true,
+ },
+ parserOptions: {
+ ecmaVersion: 8, // Note: ecmaVersion 8 is ES2017. Consider updating if using newer features.
+ sourceType: 'module',
+ project: './tsconfig.json', // Path relative to tsconfigRootDir
+ tsconfigRootDir: __dirname, // Correctly resolves the directory of this config file
+ },
+ extends: ['airbnb-base', 'prettier', 'plugin:@typescript-eslint/recommended'],
+ settings: {
+ 'import/resolver': {
+ node: {
+ extensions: ['.ts'],
+ },
+ },
+ },
+};
From 3d898574bf2723d745ec15790e1086529478fcd5 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 11:42:20 +0200
Subject: [PATCH 02/52] Refactor ramp-recovery.worker.ts and make it execute on
start
---
api/src/api/workers/ramp-recovery.worker.ts | 82 ++++++++++++++-------
1 file changed, 57 insertions(+), 25 deletions(-)
diff --git a/api/src/api/workers/ramp-recovery.worker.ts b/api/src/api/workers/ramp-recovery.worker.ts
index 60cd1304a..d08c021bd 100644
--- a/api/src/api/workers/ramp-recovery.worker.ts
+++ b/api/src/api/workers/ramp-recovery.worker.ts
@@ -4,15 +4,25 @@ import RampState from '../../models/rampState.model';
import logger from '../../config/logger';
import phaseProcessor from '../services/phases/phase-processor';
+const TEN_MINUTES_IN_MS = 10 * 60 * 1000;
+
/**
* Worker to recover failed ramp states
*/
class RampRecoveryWorker {
private job: CronJob;
- constructor() {
- // Run every 5 minutes
- this.job = new CronJob('*/5 * * * *', this.recover.bind(this));
+ constructor(cronTime = '*/5 * * * *') {
+ // Run immediately and then according to schedule
+ this.job = new CronJob(
+ cronTime,
+ this.recover.bind(this),
+ null, // onComplete
+ false, // start
+ undefined, // timeZone
+ null, // context
+ true, // runOnInit - This makes it run immediately
+ );
}
/**
@@ -34,6 +44,7 @@ class RampRecoveryWorker {
/**
* Recover failed ramp states
*/
+ // eslint-disable-next-line class-methods-use-this
private async recover(): Promise {
try {
logger.info('Running ramp recovery worker');
@@ -46,44 +57,65 @@ class RampRecoveryWorker {
[Op.ne]: 'complete',
},
updatedAt: {
- [Op.lt]: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago
+ [Op.lt]: new Date(Date.now() - TEN_MINUTES_IN_MS), // 10 minutes ago
},
- presignedTxs: {[Op.not]: null},
+ presignedTxs: { [Op.not]: null },
},
});
- logger.info(`Found ${staleStates.length} stale ramp states`);
+ if (staleStates.length === 0) {
+ logger.info('No stale ramp states found.');
+ return;
+ }
+
+ logger.info(`Found ${staleStates.length} stale ramp states to process.`);
- // Process each stale state
- for (const state of staleStates) {
+ // Process each stale state concurrently
+ const recoveryPromises = staleStates.map(async (state) => {
try {
- logger.info(`Recovering ramp state ${state.id} in phase ${state.currentPhase}`);
-
+ logger.info(`Attempting recovery for ramp state ${state.id} in phase ${state.currentPhase}`);
// Process the state
await phaseProcessor.processRamp(state.id);
+ logger.info(`Successfully processed ramp state ${state.id}`);
+ return { status: 'fulfilled', stateId: state.id };
} catch (error: any) {
logger.error(`Error recovering ramp state ${state.id}:`, error);
- // Add error to the state
- const errorLogs = [
- ...(state.errorLogs || []),
- {
- phase: state.currentPhase,
- timestamp: new Date().toISOString(),
- error: error.message || 'Unknown error',
- details: error.stack || {},
- },
- ];
+ // Prepare error log entry
+ const errorLogEntry = {
+ phase: state.currentPhase,
+ timestamp: new Date().toISOString(),
+ error: error.message || 'Unknown error during recovery',
+ details: error.stack || 'No stack trace available',
+ };
- await state.update({ errorLogs });
+ // Attempt to update the state with the error log
+ try {
+ const errorLogs = [...(state.errorLogs || []), errorLogEntry];
+ await state.update({ errorLogs });
+ logger.info(`Updated ramp state ${state.id} with error log.`);
+ } catch (updateError: any) {
+ logger.error(`Failed to update ramp state ${state.id} with error log:`, updateError);
+ // Log the original error as well if the update fails
+ logger.error(`Original recovery error for state ${state.id}:`, error);
+ }
+ // Return a rejected status for Promise.allSettled
+ return { status: 'rejected', stateId: state.id, reason: error };
}
- }
+ });
+
+ // Wait for all recovery attempts to settle
+ const results = await Promise.allSettled(recoveryPromises);
- logger.info('Ramp recovery worker completed');
+ // Log summary of results
+ const successfulRecoveries = results.filter((r) => r.status === 'fulfilled').length;
+ const failedRecoveries = results.length - successfulRecoveries;
+ logger.info(`Ramp recovery attempt completed. Successful: ${successfulRecoveries}, Failed: ${failedRecoveries}`);
} catch (error) {
- logger.error('Error in ramp recovery worker:', error);
+ // Catch errors from the initial findAll or other unexpected issues
+ logger.error('Critical error in ramp recovery worker:', error);
}
}
}
-export default new RampRecoveryWorker();
+export default RampRecoveryWorker;
From 2ec9f722fe651a27e31a352f610f28e042bf1af3 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 12:01:02 +0200
Subject: [PATCH 03/52] Refactor cleanup.worker.ts
---
api/src/api/workers/cleanup.worker.ts | 246 ++++++++++++++++----------
1 file changed, 157 insertions(+), 89 deletions(-)
diff --git a/api/src/api/workers/cleanup.worker.ts b/api/src/api/workers/cleanup.worker.ts
index bea7d20f7..b1900e389 100644
--- a/api/src/api/workers/cleanup.worker.ts
+++ b/api/src/api/workers/cleanup.worker.ts
@@ -1,71 +1,74 @@
+import { CronJob } from 'cron';
+import { CleanupPhase } from 'shared'; // <-- Import CleanupPhase
import logger from '../../config/logger';
-import quoteService from '../services/ramp/quote.service';
import { BaseRampService } from '../services/ramp/base.service';
import RampState from '../../models/rampState.model';
-import { postProcessHandlers } from '../services/phases/post-process';
+import { postProcessHandlers, BasePostProcessHandler } from '../services/phases/post-process';
+
+interface HandlerError {
+ name: CleanupPhase; // <-- Use CleanupPhase type
+ error: string;
+}
/**
- * Worker to clean up expired quotes
+ * Worker to clean up expired quotes and post-process completed ramps
*/
-export class CleanupWorker {
- private readonly rampService: BaseRampService;
-
- private readonly intervalMs: number;
+class CleanupWorker {
+ private job: CronJob;
- private interval: NodeJS.Timeout | null = null;
+ private readonly rampService: BaseRampService;
- constructor(intervalMs = 60000) {
- // Default to 1 minute
+ constructor(cronTime = '*/5 * * * *') {
this.rampService = new BaseRampService();
- this.intervalMs = intervalMs;
+
+ // Run immediately and then according to schedule
+ this.job = new CronJob(
+ cronTime,
+ this.cleanup.bind(this),
+ null, // onComplete
+ false, // start
+ undefined, // timeZone
+ null, // context
+ true, // runOnInit - Run immediately on start
+ );
}
/**
* Start the cleanup worker
*/
public start(): void {
- if (this.interval) {
- return;
- }
-
logger.info('Starting cleanup worker');
-
- this.interval = setInterval(async () => {
- try {
- await this.cleanup();
- } catch (error) {
- logger.error('Error in cleanup worker:', error);
- }
- }, this.intervalMs);
+ this.job.start();
}
/**
* Stop the cleanup worker
*/
public stop(): void {
- if (!this.interval) {
- return;
- }
-
logger.info('Stopping cleanup worker');
-
- clearInterval(this.interval);
- this.interval = null;
+ this.job.stop();
}
/**
* Run the cleanup process
*/
+ // eslint-disable-next-line class-methods-use-this
private async cleanup(): Promise {
- // Clean up expired quotes
- const expiredQuotesCount = await this.rampService.cleanupExpiredQuotes();
- if (expiredQuotesCount > 0) {
- logger.info(`Cleaned up ${expiredQuotesCount} expired quotes`);
- }
+ logger.info('Running cleanup worker cycle');
+ try {
+ // Clean up expired quotes
+ const expiredQuotesCount = await this.rampService.cleanupExpiredQuotes();
+ if (expiredQuotesCount > 0) {
+ logger.info(`Cleaned up ${expiredQuotesCount} expired quotes`);
+ }
- // Post-process completed RampStates
- await this.postProcessCompletedStates();
+ // Post-process completed RampStates
+ await this.postProcessCompletedStates();
+ logger.info('Cleanup worker cycle completed');
+ } catch (error) {
+ logger.error('Error during cleanup worker cycle:', error);
+ }
// TODO should we remove expired quotes from the database eventually? Maybe after 1 day or so?
}
@@ -86,6 +89,7 @@ export class CleanupWorker {
});
if (states.length === 0) {
+ logger.info('No completed RampStates found needing post-processing.');
return;
}
@@ -94,91 +98,155 @@ export class CleanupWorker {
const processPromises = states.map(async (state) => {
try {
await this.processCleanup(state);
+ return { status: 'fulfilled', stateId: state.id };
} catch (error) {
- logger.error(`Error post-processing state ${state.id}:`, error);
+ logger.error(`Error processing cleanup for state ${state.id}:`, error);
+ // Don't update the state here, processCleanup handles its own updates
+ return { status: 'rejected', stateId: state.id, reason: error };
}
});
- await Promise.all(processPromises);
+ // Use allSettled to allow individual state processing to fail without stopping others
+ const results = await Promise.allSettled(processPromises);
+ const successful = results.filter((r) => r.status === 'fulfilled').length;
+ const failed = results.length - successful;
+ logger.info(
+ `Post-processing attempt completed for ${states.length} states. Successful: ${successful}, Failed: ${failed}`,
+ );
} catch (error) {
- logger.error('Error in postProcessCompletedStates:', error);
+ logger.error('Error fetching states in postProcessCompletedStates:', error);
}
}
/**
- * Process a state with appropriate cleanup handlers
+ * Process a single state with appropriate cleanup handlers
* @param state The state to process
*/
+ // eslint-disable-next-line class-methods-use-this
private async processCleanup(state: RampState): Promise {
// Identify which handlers should process this state
- const applicableHandlers = postProcessHandlers.filter(handler => handler.shouldProcess(state));
-
+ const applicableHandlers = postProcessHandlers.filter((handler) => handler.shouldProcess(state));
+
if (applicableHandlers.length === 0) {
- logger.info(`No applicable cleanup handlers for state ${state.id}`);
+ logger.info(`No applicable cleanup handlers for state ${state.id}. Marking as complete.`);
+ // Mark as complete if no handlers apply
+ await RampState.update(
+ {
+ postCompleteState: {
+ ...state.postCompleteState,
+ cleanup: {
+ ...(state.postCompleteState?.cleanup || {}), // Ensure cleanup object exists
+ cleanupCompleted: true,
+ cleanupAt: new Date(),
+ errors: null,
+ },
+ },
+ },
+ { where: { id: state.id } },
+ );
return;
}
-
+
logger.info(`Found ${applicableHandlers.length} applicable cleanup handlers for state ${state.id}`);
-
- // Get the current list of errors (if any)
- const currentErrors = state.postCompleteState.cleanup.errors || [];
- let updatedErrors = [...currentErrors];
- let allSuccessful = true;
-
- // If there are errors, remove from applicable those that are NOT in the current errors.
- // We can retry only the ones that failed
- const filteredApplicableHandlers = applicableHandlers.filter(handler =>
- currentErrors.some(error => error.name === handler.getCleanupName())
- );
-
- // Process each handler
- for (const handler of filteredApplicableHandlers) {
+
+ const currentErrors: HandlerError[] = state.postCompleteState?.cleanup?.errors || [];
+ let handlersToRun: BasePostProcessHandler[];
+
+ // If there are existing errors, only retry the handlers that failed previously.
+ // Otherwise, run all applicable handlers.
+ if (currentErrors.length > 0) {
+ const failedHandlerNames = new Set(currentErrors.map((err) => err.name));
+ handlersToRun = applicableHandlers.filter((handler) => failedHandlerNames.has(handler.getCleanupName()));
+ logger.info(`Retrying ${handlersToRun.length} previously failed handlers for state ${state.id}`);
+ if (handlersToRun.length === 0) {
+ logger.warn(`State ${state.id} has errors, but no matching applicable handlers found for retry.`);
+ // Decide if we should mark complete or leave as is. Leaving as is for now.
+ return;
+ }
+ } else {
+ handlersToRun = applicableHandlers;
+ }
+
+ // Process handlers concurrently
+ const handlerPromises = handlersToRun.map(async (handler) => {
const handlerName = handler.getCleanupName();
logger.info(`Processing state ${state.id} with handler ${handler.constructor.name}`);
-
- // Process with this handler
- const [success, error] = await handler.process(state);
-
- if (!success) {
- allSuccessful = false;
-
- // Add or update error entry
- const errorMessage = error ? error.message : 'Unknown error';
- const newError = { name: handlerName, error: errorMessage };
-
- updatedErrors = updatedErrors.filter(err => err.name !== handlerName);
- updatedErrors.push(newError);
-
- logger.error(`Handler ${handler.constructor.name} failed for state ${state.id}: ${errorMessage}`);
- } else {
- // Remove any existing error for this handler on success, if it exists there.
- updatedErrors = updatedErrors.filter(err => err.name !== handlerName);
+ try {
+ const [success, error] = await handler.process(state);
+ if (!success) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error during handler processing';
+ logger.error(`Handler ${handler.constructor.name} failed for state ${state.id}: ${errorMessage}`);
+ return { status: 'rejected', name: handlerName, error: errorMessage };
+ }
logger.info(`Handler ${handler.constructor.name} succeeded for state ${state.id}`);
+ return { status: 'fulfilled', name: handlerName };
+ } catch (processError: any) {
+ const errorMessage =
+ processError instanceof Error ? processError.message : 'Exception during handler processing';
+ logger.error(`Exception in handler ${handler.constructor.name} for state ${state.id}:`, processError);
+ // Ensure handlerName retains CleanupPhase type
+ return { status: 'rejected', name: handlerName as CleanupPhase, error: errorMessage };
}
- }
-
- // Update the state with the results
+ });
+
+ const results = await Promise.allSettled(handlerPromises);
+
+ // Aggregate results and update state
+ const finalErrors = [...currentErrors]; // Start with existing errors
+ // The 'allSuccessfulThisRun' variable is not needed as completion is determined by finalErrors.length
+
+ results.forEach((result) => {
+ if (result.status === 'fulfilled') {
+ // Remove error if handler succeeded this time
+ const index = finalErrors.findIndex((err) => err.name === result.value.name);
+ if (index > -1) {
+ finalErrors.splice(index, 1);
+ }
+ } else {
+ // status === 'rejected'
+ // Explicitly type the reason object to ensure name is CleanupPhase
+ const failedHandler = result.reason as { name: CleanupPhase; error: string };
+ // Add or update error entry
+ const existingErrorIndex = finalErrors.findIndex((err) => err.name === failedHandler.name);
+ if (existingErrorIndex > -1) {
+ finalErrors[existingErrorIndex].error = failedHandler.error; // Update error message
+ } else {
+ // Now failedHandler.name is correctly typed as CleanupPhase
+ finalErrors.push({ name: failedHandler.name, error: failedHandler.error });
+ }
+ }
+ });
+
+ // Determine overall completion status
+ // It's complete only if all *applicable* handlers succeeded *eventually* (i.e., no errors remain)
+ const cleanupCompleted = finalErrors.length === 0;
+
+ // Update the state
await RampState.update(
{
postCompleteState: {
...state.postCompleteState,
cleanup: {
- ...state.postCompleteState.cleanup,
- cleanupCompleted: allSuccessful,
- cleanupAt: new Date(),
- errors: updatedErrors.length > 0 ? updatedErrors : null,
+ ...(state.postCompleteState?.cleanup || {}), // Ensure cleanup object exists
+ cleanupCompleted,
+ cleanupAt: new Date(), // Update timestamp on every attempt
+ errors: finalErrors.length > 0 ? finalErrors : null,
},
},
},
- { where: { id: state.id } }
+ { where: { id: state.id } },
);
-
- if (allSuccessful) {
+
+ if (cleanupCompleted) {
logger.info(`All cleanup handlers successful for state ${state.id}, marked cleanup as complete`);
} else {
- logger.warn(`Some cleanup handlers failed for state ${state.id}, will retry on next cycle`);
+ logger.warn(
+ `Some cleanup handlers failed for state ${state.id}. Errors: ${JSON.stringify(
+ finalErrors,
+ )}. Will retry on next cycle.`,
+ );
}
}
}
-export default new CleanupWorker();
+export default CleanupWorker;
From 6f2fbd4592ec747dfab4a04f9f40139fa91b988c Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 12:01:22 +0200
Subject: [PATCH 04/52] Adjust to new constructor in api/src/index.ts
---
api/src/index.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/api/src/index.ts b/api/src/index.ts
index 4d05c0bb3..e7061fab3 100755
--- a/api/src/index.ts
+++ b/api/src/index.ts
@@ -19,8 +19,8 @@ import { ApiManager } from './api/services/pendulum/apiManager';
import { testDatabaseConnection } from './config/database';
import { runMigrations } from './database/migrator';
import './models'; // Initialize models
-import cleanupWorker from './api/workers/cleanup.worker';
-import rampRecoveryWorker from './api/workers/ramp-recovery.worker';
+import CleanupWorker from './api/workers/cleanup.worker';
+import RampRecoveryWorker from './api/workers/ramp-recovery.worker';
import registerPhaseHandlers from './api/services/phases/register-handlers';
import { EventPoller } from './api/services/brla/webhooks';
@@ -59,8 +59,8 @@ const initializeApp = async () => {
await apiManager.populateAllApis();
// Start background workers
- cleanupWorker.start();
- rampRecoveryWorker.start();
+ new CleanupWorker().start();
+ new RampRecoveryWorker().start();
// Register phase handlers
registerPhaseHandlers();
From 455bad0ad63aa305c36b6bab2da33cefe8bef6aa Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 14:45:26 +0200
Subject: [PATCH 05/52] Add logs
---
frontend/src/components/OfframpSummaryDialog/index.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index 7d41a8378..496cb3220 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -116,8 +116,11 @@ const BRLOnrampDetails = () => {
const { t } = useTranslation();
const rampState = useRampState();
+ console.log("In BRLAOnrampDetails", rampState, rampDirection);
if (rampDirection !== RampDirection.ONRAMP) return null;
+ console.log("after rampDirection")
if (!rampState?.ramp?.brCode) return null;
+ console.log("after brcode")
return (
From 9fcf47b0c3224f80f2bd98a60703a80220260cf0 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 15:39:44 +0200
Subject: [PATCH 06/52] Add more logs
---
.../hooks/offramp/useRampService/useRegisterRamp.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
index c8b0f18f7..d4d2fe767 100644
--- a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
@@ -180,7 +180,20 @@ export const useRegisterRamp = () => {
moonbeamApiComponents.api,
);
+ console.log("setRampRegistered(true)")
setRampRegistered(true);
+ console.log("setRampState to ", {
+ quote: executionInput.quote,
+ ramp: rampProcess,
+ signedTransactions,
+ requiredUserActionsCompleted: false,
+ userSigningMeta: {
+ squidRouterApproveHash: undefined,
+ squidRouterSwapHash: undefined,
+ assetHubToPendulumHash: undefined,
+ },
+ })
+
setRampState({
quote: executionInput.quote,
ramp: rampProcess,
From b7485a5a65740ff0e9896059d243031fe64b15fc Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 17:49:03 +0200
Subject: [PATCH 07/52] Improve handling of Stellar errors
---
.../phases/handlers/initial-phase-handler.ts | 46 ++++++++++++-------
.../handlers/spacewalk-redeem-handler.ts | 23 ++++++----
.../handlers/stellar-payment-handler.ts | 35 ++++++++------
.../stellar-post-process-handler.ts | 34 +++++++++-----
4 files changed, 87 insertions(+), 51 deletions(-)
diff --git a/api/src/api/services/phases/handlers/initial-phase-handler.ts b/api/src/api/services/phases/handlers/initial-phase-handler.ts
index 39ffee476..57a9ba9da 100644
--- a/api/src/api/services/phases/handlers/initial-phase-handler.ts
+++ b/api/src/api/services/phases/handlers/initial-phase-handler.ts
@@ -1,12 +1,12 @@
import { HORIZON_URL, RampPhase } from 'shared';
+import { Transaction, Horizon, NetworkError, Networks } from 'stellar-sdk';
import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
import logger from '../../../../config/logger';
-import { Transaction } from 'stellar-sdk';
-import { Horizon, NetworkError, Networks } from 'stellar-sdk';
export const horizonServer = new Horizon.Server(HORIZON_URL);
const NETWORK_PASSPHRASE = Networks.PUBLIC;
+
/**
* Handler for the initial phase
*/
@@ -26,31 +26,45 @@ export class InitialPhaseHandler extends BasePhaseHandler {
protected async executePhase(state: RampState): Promise {
logger.info(`Executing initial phase for ramp ${state.id}`);
- //Only stellar case requires an initial operation, sending the create ephemeral transaction
+ // Check if signed_transactions are present for offramps. If they are not, return early.
+ if (state.type === 'off' && (state.presignedTxs === null || state.presignedTxs.length > 0)) {
+ throw new Error('InitialPhaseHandler: No signed transactions found. Cannot proceed.');
+ }
+
+ // Only stellar case requires an initial operation, sending the create ephemeral transaction
if (state.state.stellarTarget) {
- try {
- const { txData: stellarCreationTransactionXDR } = this.getPresignedTransaction(state, 'stellarCreateAccount');
+ const { txData: stellarCreationTransactionXDR } = this.getPresignedTransaction(state, 'stellarCreateAccount');
+ if (typeof stellarCreationTransactionXDR !== 'string') {
+ throw new Error(
+ 'InitialPhaseHandler: `stellarCreateAccount` transaction is not a string -> not an encoded Stellar transaction.',
+ );
+ }
+ try {
const stellarCreationTransaction = new Transaction(stellarCreationTransactionXDR, NETWORK_PASSPHRASE);
await horizonServer.submitTransaction(stellarCreationTransaction);
return this.transitionToNextPhase(state, 'fundEphemeral');
} catch (e) {
- const horizonError = e as { response: { data: { extras: any } } };
- console.log(
- `Could not submit the stellar account creation transaction ${JSON.stringify(
- horizonError.response.data.extras.result_codes,
- )}`,
- );
+ const horizonError = e as NetworkError;
+ if (horizonError.response.data?.status === 400) {
+ console.log(
+ `Could not submit the stellar account creation transaction ${JSON.stringify(
+ horizonError.response.data.extras.result_codes,
+ )}`,
+ );
- // TODO this error may need adjustment, as the `tx_bad_seq` may be due to parallel ramps and ephemeral creations.
- if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
- console.log('Recovery mode: Creation already performed.');
+ // TODO this error may need adjustment, as the `tx_bad_seq` may be due to parallel ramps and ephemeral creations.
+ if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
+ console.log('Recovery mode: Creation already performed.');
- return this.transitionToNextPhase(state, 'fundEphemeral');
- } else {
+ return this.transitionToNextPhase(state, 'fundEphemeral');
+ }
console.error(horizonError.response.data.extras);
throw new Error('Could not submit the stellar creation transaction');
+ } else {
+ console.error(horizonError.response.data);
+ throw new Error('Could not submit the stellar creation transaction');
}
}
}
diff --git a/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts b/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
index 197f1111b..7fd352c4f 100644
--- a/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
+++ b/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
@@ -1,10 +1,10 @@
-import { EventListener, RampPhase, decodeSubmittableExtrinsic } from 'shared';
+import { decodeSubmittableExtrinsic, EventListener, RampPhase } from 'shared';
+import Big from 'big.js';
import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
import { ApiManager } from '../../pendulum/apiManager';
import { StateMetadata } from '../meta-state-types';
-import Big from 'big.js';
import { checkBalancePeriodically } from '../../stellar/checkBalance';
import { createVaultService } from '../../stellar/vaultService';
@@ -40,9 +40,14 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
throw new Error('SpacewalkRedeemPhaseHandler: State metadata corrupted. This is a bug.');
}
- try {
- const { txData: spacewalkRedeemTransaction } = this.getPresignedTransaction(state, 'spacewalkRedeem');
+ const { txData: spacewalkRedeemTransaction } = this.getPresignedTransaction(state, 'spacewalkRedeem');
+ if (typeof spacewalkRedeemTransaction !== 'string') {
+ throw new Error(
+ 'SpacewalkRedeemPhaseHandler: Presigned transaction is not a string -> not an encoded Stellar transaction.',
+ );
+ }
+ try {
const accountData = await pendulumNode.api.query.system.account(pendulumEphemeralAddress);
const currentEphemeralAccountNonce = await accountData.nonce.toNumber();
@@ -55,7 +60,7 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
);
return this.transitionToNextPhase(state, 'stellarPayment');
}
-
+
const vaultService = await createVaultService(
pendulumNode,
stellarTarget.stellarTokenDetails.stellarAsset.code.hex,
@@ -84,11 +89,11 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
stellarTarget.stellarTokenDetails.stellarAsset.code.string,
);
return this.transitionToNextPhase(state, 'stellarPayment');
- } else {
- // Generic failure of the extrinsic itself OR lack of funds to even make the transaction
- console.log(`Failed to request redeem: ${e}`);
- throw new Error(`Failed to request redeem`);
}
+
+ // Generic failure of the extrinsic itself OR lack of funds to even make the transaction
+ console.log(`Failed to request redeem: ${e}`);
+ throw new Error(`Failed to request redeem`);
}
}
diff --git a/api/src/api/services/phases/handlers/stellar-payment-handler.ts b/api/src/api/services/phases/handlers/stellar-payment-handler.ts
index d0e006798..37ea1d6e2 100644
--- a/api/src/api/services/phases/handlers/stellar-payment-handler.ts
+++ b/api/src/api/services/phases/handlers/stellar-payment-handler.ts
@@ -1,6 +1,5 @@
import { HORIZON_URL, RampPhase } from 'shared';
-import { Horizon, Networks, Transaction } from 'stellar-sdk';
-
+import { Horizon, NetworkError, Networks, Transaction } from 'stellar-sdk';
import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
@@ -13,26 +12,34 @@ export class StellarPaymentPhaseHandler extends BasePhaseHandler {
}
protected async executePhase(state: RampState): Promise {
- try {
- const { txData: offrampingTransactionXDR } = this.getPresignedTransaction(state, 'stellarPayment');
+ const { txData: offrampingTransactionXDR } = this.getPresignedTransaction(state, 'stellarPayment');
+ if (typeof offrampingTransactionXDR !== 'string') {
+ throw new Error('Invalid transaction data');
+ }
+ try {
const offrampingTransaction = new Transaction(offrampingTransactionXDR, NETWORK_PASSPHRASE);
await horizonServer.submitTransaction(offrampingTransaction);
return this.transitionToNextPhase(state, 'complete');
} catch (e) {
- const horizonError = e as { response: { data: { extras: any } } };
-
- console.log(
- `Could not submit the offramp transaction ${JSON.stringify(horizonError.response.data.extras.result_codes)}`,
- );
- // check https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions
- if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
- console.log('Assuming offramp was already performed.');
- return this.transitionToNextPhase(state, 'complete');
- } else {
+ const horizonError = e as NetworkError;
+
+ if (horizonError.response.data?.status === 400) {
+ console.log(
+ `Could not submit the offramp transaction ${JSON.stringify(horizonError.response.data.extras.result_codes)}`,
+ );
+ // check https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions
+ if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
+ console.log('Assuming offramp was already performed.');
+ return this.transitionToNextPhase(state, 'complete');
+ }
+
console.error(horizonError.response.data.extras);
throw new Error('Could not submit the offramping transaction');
+ } else {
+ console.error('Error while submitting the offramp transaction', e);
+ throw new Error('Could not submit the offramping transaction');
}
}
}
diff --git a/api/src/api/services/phases/post-process/stellar-post-process-handler.ts b/api/src/api/services/phases/post-process/stellar-post-process-handler.ts
index e9cac4a03..11d44b322 100644
--- a/api/src/api/services/phases/post-process/stellar-post-process-handler.ts
+++ b/api/src/api/services/phases/post-process/stellar-post-process-handler.ts
@@ -1,5 +1,5 @@
-import { HORIZON_URL, Networks as AppNetworks, PresignedTx, RampPhase, FiatToken, CleanupPhase } from 'shared';
-import { Horizon, Networks as StellarNetworks, Transaction } from 'stellar-sdk';
+import { CleanupPhase, FiatToken, HORIZON_URL } from 'shared';
+import { Horizon, NetworkError, Networks as StellarNetworks, Transaction } from 'stellar-sdk';
import logger from '../../../../config/logger';
import RampState from '../../../../models/rampState.model';
import { BasePostProcessHandler } from './base-post-process-handler';
@@ -43,25 +43,35 @@ export class StellarPostProcessHandler extends BasePostProcessHandler {
try {
const { txData: stellarCleanupTransactionXDR } = this.getPresignedTransaction(state, 'stellarCleanup');
- const stellarCleanupTransactionTransaction = new Transaction(stellarCleanupTransactionXDR as string, NETWORK_PASSPHRASE);
+ const stellarCleanupTransactionTransaction = new Transaction(
+ stellarCleanupTransactionXDR as string,
+ NETWORK_PASSPHRASE,
+ );
await horizonServer.submitTransaction(stellarCleanupTransactionTransaction);
-
+
logger.info(`Successfully processed Stellar cleanup for ramp state ${state.id}`);
return [true, null];
} catch (e) {
try {
- const horizonError = e as { response: { data: { extras: any } } };
- logger.info(
- `Could not submit the cleanup transaction ${JSON.stringify(horizonError.response.data.extras.result_codes)}`,
- );
+ const horizonError = e as NetworkError;
+ if (horizonError.response.data?.status === 400) {
+ logger.info(
+ `Could not submit the cleanup transaction ${JSON.stringify(
+ horizonError.response?.data?.extras.result_codes,
+ )}`,
+ );
+
+ if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
+ logger.info('Recovery mode: Cleanup already performed.');
+ return [true, null];
+ }
- if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
- logger.info('Recovery mode: Cleanup already performed.');
- return [true, null];
- } else {
logger.error(horizonError.response.data.extras);
return [false, this.createErrorObject('Could not submit the cleanup transaction')];
}
+
+ logger.error('Error while submitting the cleanup transaction', e);
+ return [false, this.createErrorObject('Could not submit the cleanup transaction')];
} catch (parseError) {
// If we can't parse the error as a Horizon error, it's a different type of error
return [false, this.createErrorObject(e instanceof Error ? e : String(e))];
From 2981ef710bcb6b7ce4796cf64becbab3db8f262b Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 18:42:10 +0200
Subject: [PATCH 08/52] Replace usages of console log with `logger`
---
api/bun.lockb | Bin 418180 -> 418180 bytes
api/src/api/controllers/brla.controller.ts | 3 +-
.../api/controllers/pendulum.controller.ts | 5 ++-
.../api/controllers/subsidize.controller.ts | 5 ++-
.../api/services/brla/brlaTeleportService.ts | 9 ++--
api/src/api/services/brla/webhooks.ts | 3 +-
api/src/api/services/moonbeam/balance.ts | 3 +-
api/src/api/services/pendulum/apiManager.ts | 13 +++---
.../handlers/brla-payout-moonbeam-handler.ts | 3 +-
.../phases/handlers/brla-teleport-handler.ts | 5 ++-
.../phases/handlers/fund-ephemeral-handler.ts | 7 +--
.../phases/handlers/initial-phase-handler.ts | 4 +-
.../handlers/moonbeam-to-pendulum-handler.ts | 3 +-
.../moonbeam-to-pendulum-xcm-handler.ts | 3 +-
.../phases/handlers/nabla-approve-handler.ts | 5 ++-
.../phases/handlers/nabla-swap-handler.ts | 7 +--
.../handlers/spacewalk-redeem-handler.ts | 11 ++---
.../handlers/stellar-payment-handler.ts | 5 ++-
.../handlers/subsidize-post-swap-handler.ts | 3 +-
.../handlers/subsidize-pre-swap-handler.ts | 9 ++--
api/src/api/services/stellar/checkBalance.ts | 5 ++-
api/src/api/services/stellar/getVaults.ts | 3 +-
api/src/api/services/stellar/loadAccount.ts | 3 +-
api/src/api/services/stellar/vaultService.ts | 41 +++++++++---------
.../services/transactions/nabla/approve.ts | 9 ++--
.../api/services/transactions/nabla/swap.ts | 3 +-
.../transactions/offrampTransactions.ts | 10 +----
.../transactions/onrampTransactions.ts | 7 ++-
.../stellar/offrampTransaction.ts | 3 +-
29 files changed, 103 insertions(+), 87 deletions(-)
diff --git a/api/bun.lockb b/api/bun.lockb
index 66a8e5dabefcb8baa3c31f4a5d6875e7a2c76fb0..470048535446ff0dbab302bdf4a42f12907f66cf 100755
GIT binary patch
delta 40
ucmZoUEZK5cvY~~sg{g&k3(H@74#qeOJwszXqjpvY79eKb&g#HcuLuAUybGHE
delta 40
ucmZoUEZK5cvY~~sg{g&k3(H@74yK&MlGNf7hIUp579eKb&g#HcuLuAnxD25H
diff --git a/api/src/api/controllers/brla.controller.ts b/api/src/api/controllers/brla.controller.ts
index f24557593..829ce0956 100644
--- a/api/src/api/controllers/brla.controller.ts
+++ b/api/src/api/controllers/brla.controller.ts
@@ -5,6 +5,7 @@ import { BrlaApiService } from '../services/brla/brlaApiService';
import { eventPoller } from '../..';
import { BrlaTeleportService } from '../services/brla/brlaTeleportService';
import { generateReferenceLabel } from '../services/brla/helpers';
+import logger from '../../config/logger';
// BRLA API requires the date in the format YYYY-MMM-DD
function convertDateToBRLAFormat(dateNumber: number) {
@@ -351,7 +352,7 @@ export const triggerPayIn = async (
res.status(400).json({ error: 'taxId invalid' });
return;
}
- console.log('Requesting teleport:', subaccount.id, amount, receiverAddress);
+ logger.info('Requesting teleport:', subaccount.id, amount, receiverAddress);
const teleportService = BrlaTeleportService.getInstance();
await teleportService.requestTeleport(subaccount.id, Number(amount), receiverAddress);
diff --git a/api/src/api/controllers/pendulum.controller.ts b/api/src/api/controllers/pendulum.controller.ts
index 1cf593d63..13e4c47b6 100644
--- a/api/src/api/controllers/pendulum.controller.ts
+++ b/api/src/api/controllers/pendulum.controller.ts
@@ -13,6 +13,7 @@ import { fundEphemeralAccount, getFundingData } from '../services/pendulum/pendu
import { ChainDecimals, multiplyByPowerOfTen, nativeToDecimal } from '../services/pendulum/helpers';
import { SlackNotifier } from '../services/slack.service';
import { ApiManager } from '../services/pendulum/apiManager';
+import logger from '../../config/logger';
// DEPRECATED
export const fundEphemeralAccountController = async (
@@ -60,7 +61,7 @@ export const sendStatusWithPk = async (): Promise => {
// Wait for all required token balances check.
await Promise.all(
Object.entries(TOKEN_CONFIG).map(async ([token, tokenConfig]: [string, StellarTokenConfig | XCMTokenConfig]) => {
- console.log(`Checking token ${token} balance...`);
+ logger.info(`Checking token ${token} balance...`);
if (!tokenConfig.pendulumCurrencyId) {
throw new Error(`Token ${token} does not have a currency id.`);
}
@@ -80,7 +81,7 @@ export const sendStatusWithPk = async (): Promise => {
if (remainingMaxSubsidiesAvailable.lt(SUBSIDY_MINIMUM_RATIO_FUND_UNITS)) {
isTokensSufficient = false;
- console.log(`Token ${token} balance is insufficient.`);
+ logger.info(`Token ${token} balance is insufficient.`);
const tokenDecimals = 'decimals' in tokenConfig ? tokenConfig.decimals : ChainDecimals;
slackNotifier.sendMessage({
diff --git a/api/src/api/controllers/subsidize.controller.ts b/api/src/api/controllers/subsidize.controller.ts
index eff87b10e..7aba7bb53 100644
--- a/api/src/api/controllers/subsidize.controller.ts
+++ b/api/src/api/controllers/subsidize.controller.ts
@@ -6,6 +6,7 @@ import { StellarTokenConfig, TOKEN_CONFIG, XCMTokenConfig } from 'shared';
import { SubsidizeEndpoints } from 'shared/src/endpoints/subsidize.endpoints';
import { PENDULUM_FUNDING_SEED } from '../../constants/constants';
import { ApiManager } from '../services/pendulum/apiManager';
+import logger from '../../config/logger';
export const getFundingAccount = () => {
if (!PENDULUM_FUNDING_SEED) {
@@ -39,7 +40,7 @@ export const subsidizePreSwap = async (
): Promise => {
try {
const { address, amountRaw, tokenToSubsidize } = req.body;
- console.log('Subsidize pre swap', address, amountRaw, tokenToSubsidize);
+ logger.info('Subsidize pre swap', address, amountRaw, tokenToSubsidize);
const config = getPendulumCurrencyConfig(tokenToSubsidize);
@@ -72,7 +73,7 @@ export const subsidizePostSwap = async (
): Promise => {
try {
const { address, amountRaw, token } = req.body;
- console.log('Subsidize post swap', address, amountRaw, token);
+ logger.info('Subsidize post swap', address, amountRaw, token);
const config = getPendulumCurrencyConfig(token);
diff --git a/api/src/api/services/brla/brlaTeleportService.ts b/api/src/api/services/brla/brlaTeleportService.ts
index 26b4f0043..3a94c791f 100644
--- a/api/src/api/services/brla/brlaTeleportService.ts
+++ b/api/src/api/services/brla/brlaTeleportService.ts
@@ -1,6 +1,7 @@
import { BrlaApiService } from './brlaApiService';
import { FastQuoteQueryParams, BrlaSupportedChain, OnchainLog, SmartContractOperationType } from './types';
import { verifyReferenceLabel } from './helpers';
+import logger from '../../../config/logger';
// This service is used to request and keep tracks of teleports (transfers) from BRLA's
// controlled accounts.
@@ -60,7 +61,7 @@ export class BrlaTeleportService {
status: 'claimed',
receiverAddress,
};
- console.log('Requesting teleport:', teleport);
+ logger.info('Requesting teleport:', teleport);
this.teleports.set(subaccountId, teleport);
this.maybeStartPeriodicChecks();
}
@@ -77,7 +78,7 @@ export class BrlaTeleportService {
throw new Error('Teleport not in arrived state.');
}
- console.log('Starting teleport:', teleport);
+ logger.info('Starting teleport:', teleport);
const fastQuoteParams: FastQuoteQueryParams = {
subaccountId: teleport.subaccountId,
operation: 'swap',
@@ -101,7 +102,7 @@ export class BrlaTeleportService {
this.maybeStartPeriodicChecks();
} catch (e) {
- console.log('Error starting teleport:', e);
+ logger.error('Error starting teleport:', e);
this.teleports.set(subaccountId, { ...teleport, status: 'failed' });
}
}
@@ -141,7 +142,7 @@ export class BrlaTeleportService {
if (lastContractOp.operationName === SmartContractOperationType.MINT && lastContractOp.executed === true) {
this.completedTeleports.set(subaccountId, { ...teleport, status: 'completed' });
- console.log('Teleport completed:', teleport);
+ logger.info('Teleport completed:', teleport);
this.teleports.delete(subaccountId);
}
}
diff --git a/api/src/api/services/brla/webhooks.ts b/api/src/api/services/brla/webhooks.ts
index 69f40004f..611fd37cd 100644
--- a/api/src/api/services/brla/webhooks.ts
+++ b/api/src/api/services/brla/webhooks.ts
@@ -1,5 +1,6 @@
import { WEBHOOKS_CACHE_URL } from '../../../constants/constants';
import { BrlaApiService } from './brlaApiService';
+import logger from '../../../config/logger';
type SubscriptionType = 'BURN' | 'BALANCE-UPDATE' | 'MONEY-TRANSFER' | 'MINT' | 'KYC';
@@ -97,7 +98,7 @@ export class EventPoller {
// async acknowledge events
if (eventsToAcknowledge.length > 0) {
this.brlaApiService.acknowledgeEvents(eventsToAcknowledge.flatMap((event) => event.id)).catch((error) => {
- console.log('Poll: Error while acknowledging events: ', error);
+ logger.error('Poll: Error while acknowledging events: ', error);
});
}
}
diff --git a/api/src/api/services/moonbeam/balance.ts b/api/src/api/services/moonbeam/balance.ts
index 42f937b19..4095df98c 100644
--- a/api/src/api/services/moonbeam/balance.ts
+++ b/api/src/api/services/moonbeam/balance.ts
@@ -10,6 +10,7 @@ import { MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS, MOONBEAM_FUNDING_PRIVATE_KEY
import { multiplyByPowerOfTen } from '../pendulum/helpers';
import { privateKeyToAccount } from 'viem/accounts';
import { createMoonbeamClientsAndConfig } from './createServices';
+import logger from '../../../config/logger';
export function checkMoonbeamBalancePeriodically(
tokenAddress: string,
@@ -34,7 +35,7 @@ export function checkMoonbeamBalancePeriodically(
args: [brlaEvmAddress],
})) as string;
- console.log(`Moonbeam balance check: ${result.toString()} / ${amountDesiredRaw.toString()}`);
+ logger.info(`Moonbeam balance check: ${result.toString()} / ${amountDesiredRaw.toString()}`);
const someBalanceBig = new Big(result.toString());
const amountDesiredUnitsBig = new Big(amountDesiredRaw);
diff --git a/api/src/api/services/pendulum/apiManager.ts b/api/src/api/services/pendulum/apiManager.ts
index 894880e2f..7b83ec3e4 100644
--- a/api/src/api/services/pendulum/apiManager.ts
+++ b/api/src/api/services/pendulum/apiManager.ts
@@ -2,6 +2,7 @@ import { ApiPromise, WsProvider } from '@polkadot/api';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { ISubmittableResult } from '@polkadot/types/types';
import { KeyringPair } from '@polkadot/keyring/types';
+import logger from '../../../config/logger';
export type SubstrateApiNetwork = 'assethub' | 'pendulum' | 'moonbeam';
@@ -94,10 +95,10 @@ export class ApiManager {
public async populateApi(networkName: SubstrateApiNetwork): Promise {
const network = this.getNetworkConfig(networkName);
- console.log(`Connecting to node ${network.wsUrl}...`);
+ logger.info(`Connecting to node ${network.wsUrl}...`);
const newApi = await this.connectApi(networkName);
this.apiInstances.set(networkName, newApi);
- console.log(`Connected to node ${network.wsUrl}`);
+ logger.info(`Connected to node ${network.wsUrl}`);
if (!newApi.api.isConnected) await newApi.api.connect();
await newApi.api.isReady;
@@ -123,7 +124,7 @@ export class ApiManager {
const previousSpecVersion = this.previousSpecVersions.get(networkName) ?? 0;
if (currentSpecVersion !== previousSpecVersion) {
- console.log(`Spec version changed for ${networkName}, refreshing the api...`);
+ logger.info(`Spec version changed for ${networkName}, refreshing the api...`);
return await this.populateApi(networkName);
}
@@ -158,7 +159,7 @@ export class ApiManager {
return nonceRpc;
}
- console.log(
+ logger.info(
`Nonce mismatch detected on ${networkName}. RPC: ${nonceRpc}, ApiManager: ${lastUsedNonce}, sending transaction with nonce ${
lastUsedNonce + 1
}`,
@@ -184,12 +185,12 @@ export class ApiManager {
try {
const nonce = await this.getNonce(senderKeypair, networkName);
- console.log(`Sending transaction on ${networkName} with nonce ${nonce}`);
+ logger.info(`Sending transaction on ${networkName} with nonce ${nonce}`);
return call.signAndSend(senderKeypair, { nonce });
} catch (initialError: any) {
// Only retry if the error is regarding bad signature error
if (initialError.name === 'RpcError' && initialError.message.includes('Transaction has a bad signature')) {
- console.log(
+ logger.info(
`Bad signature error encountered while sending transaction on ${networkName}, attempting to refresh the api...`,
);
diff --git a/api/src/api/services/phases/handlers/brla-payout-moonbeam-handler.ts b/api/src/api/services/phases/handlers/brla-payout-moonbeam-handler.ts
index 70fe9ed6c..bccffcdda 100644
--- a/api/src/api/services/phases/handlers/brla-payout-moonbeam-handler.ts
+++ b/api/src/api/services/phases/handlers/brla-payout-moonbeam-handler.ts
@@ -6,6 +6,7 @@ import { StateMetadata } from '../meta-state-types';
import { BasePhaseHandler } from '../base-phase-handler';
import { BrlaApiService } from '../../brla/brlaApiService';
import { checkMoonbeamBalancePeriodically } from '../../moonbeam/balance';
+import logger from '../../../../config/logger';
export class BrlaPayoutOnMoonbeamPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -49,7 +50,7 @@ export class BrlaPayoutOnMoonbeamPhaseHandler extends BasePhaseHandler {
if (balanceCheckError.message === 'Balance did not meet the limit within the specified time') {
throw new Error(`BrlaPayoutOnMoonbeamPhaseHandler: balanceCheckError ${balanceCheckError.message}`);
} else {
- console.log('Error checking Moonbeam balance:', balanceCheckError);
+ logger.error('Error checking Moonbeam balance:', balanceCheckError);
throw new Error(`Error checking Moonbeam balance`);
}
}
diff --git a/api/src/api/services/phases/handlers/brla-teleport-handler.ts b/api/src/api/services/phases/handlers/brla-teleport-handler.ts
index 14bb568ce..cb473c6ae 100644
--- a/api/src/api/services/phases/handlers/brla-teleport-handler.ts
+++ b/api/src/api/services/phases/handlers/brla-teleport-handler.ts
@@ -7,6 +7,7 @@ import { BasePhaseHandler } from '../base-phase-handler';
import { BrlaApiService } from '../../brla/brlaApiService';
import { checkMoonbeamBalancePeriodically } from '../../moonbeam/balance';
import { BrlaTeleportService } from '../../brla/brlaTeleportService';
+import logger from '../../../../config/logger';
export class BrlaTeleportPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -30,7 +31,7 @@ export class BrlaTeleportPhaseHandler extends BasePhaseHandler {
if (!subaccount) {
throw new Error('Subaccount not found');
}
- console.log('Requesting teleport:', subaccount.id, inputAmountBrla, moonbeamEphemeralAddress);
+ logger.info('Requesting teleport:', subaccount.id, inputAmountBrla, moonbeamEphemeralAddress);
const teleportService = BrlaTeleportService.getInstance();
await teleportService.requestTeleport(
subaccount.id,
@@ -62,7 +63,7 @@ export class BrlaTeleportPhaseHandler extends BasePhaseHandler {
if (balanceCheckError.message === 'Balance did not meet the limit within the specified time') {
throw new Error(`BrlaTeleportPhaseHandler: balanceCheckError ${balanceCheckError.message}`);
} else {
- console.log('Error checking Moonbeam balance:', balanceCheckError);
+ logger.error('Error checking Moonbeam balance:', balanceCheckError);
throw new Error(`BrlaTeleportPhaseHandler: Error checking Moonbeam balance`);
}
}
diff --git a/api/src/api/services/phases/handlers/fund-ephemeral-handler.ts b/api/src/api/services/phases/handlers/fund-ephemeral-handler.ts
index 50aae14d3..9db1ed38e 100644
--- a/api/src/api/services/phases/handlers/fund-ephemeral-handler.ts
+++ b/api/src/api/services/phases/handlers/fund-ephemeral-handler.ts
@@ -10,6 +10,7 @@ import { multiplyByPowerOfTen } from '../../pendulum/helpers';
import { GLMR_FUNDING_AMOUNT_RAW, PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS } from '../../../../constants/constants';
import { TOKEN_CONFIG } from 'shared';
import { fundMoonbeamEphemeralAccount } from '../../moonbeam/balance';
+import logger from '../../../../config/logger';
export class FundEphemeralPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -39,7 +40,7 @@ export class FundEphemeralPhaseHandler extends BasePhaseHandler {
}
if (!isPendulumFunded) {
- console.log('Funding pen ephemeral...');
+ logger.info('Funding pen ephemeral...');
if (state.type === 'on' && state.to !== 'assethub') {
await fundEphemeralAccount('pendulum', pendulumEphemeralAddress, true);
} else if (state.state.outputCurrency === FiatToken.BRL) {
@@ -48,11 +49,11 @@ export class FundEphemeralPhaseHandler extends BasePhaseHandler {
await fundEphemeralAccount('pendulum', pendulumEphemeralAddress, false);
}
} else {
- console.log('Pendulum ephemeral address already funded.');
+ logger.info('Pendulum ephemeral address already funded.');
}
if (state.type === 'on' && !isMoonbeamFunded) {
- console.log('Funding moonbeam ephemeral...');
+ logger.info('Funding moonbeam ephemeral...');
await fundMoonbeamEphemeralAccount(moonbeamEphemeralAddress);
}
} catch (e) {
diff --git a/api/src/api/services/phases/handlers/initial-phase-handler.ts b/api/src/api/services/phases/handlers/initial-phase-handler.ts
index 57a9ba9da..24a571254 100644
--- a/api/src/api/services/phases/handlers/initial-phase-handler.ts
+++ b/api/src/api/services/phases/handlers/initial-phase-handler.ts
@@ -48,7 +48,7 @@ export class InitialPhaseHandler extends BasePhaseHandler {
} catch (e) {
const horizonError = e as NetworkError;
if (horizonError.response.data?.status === 400) {
- console.log(
+ logger.info(
`Could not submit the stellar account creation transaction ${JSON.stringify(
horizonError.response.data.extras.result_codes,
)}`,
@@ -56,7 +56,7 @@ export class InitialPhaseHandler extends BasePhaseHandler {
// TODO this error may need adjustment, as the `tx_bad_seq` may be due to parallel ramps and ephemeral creations.
if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
- console.log('Recovery mode: Creation already performed.');
+ logger.info('Recovery mode: Creation already performed.');
return this.transitionToNextPhase(state, 'fundEphemeral');
}
diff --git a/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts b/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts
index 0c7b77765..505369c74 100644
--- a/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts
+++ b/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts
@@ -16,6 +16,7 @@ import { MOONBEAM_EXECUTOR_PRIVATE_KEY, MOONBEAM_RECEIVER_CONTRACT_ADDRESS } fro
import { createMoonbeamClientsAndConfig } from '../../moonbeam/createServices';
import splitReceiverABI from '../../../../../../mooncontracts/splitReceiverABI.json';
import { waitUntilTrue } from '../../../helpers/functions';
+import logger from '../../../../config/logger';
export class MoonbeamToPendulumPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -76,7 +77,7 @@ export class MoonbeamToPendulumPhaseHandler extends BasePhaseHandler {
await waitUntilTrue(isHashRegisteredInSplitReceiver);
}
} catch (e) {
- console.log(e);
+ logger.error(e);
throw new Error('MoonbeamToPendulumPhaseHandler: Failed to wait for hash registration in split receiver.');
}
diff --git a/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts b/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts
index 3ffef4762..20223e52b 100644
--- a/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts
+++ b/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts
@@ -7,6 +7,7 @@ import { StateMetadata } from '../meta-state-types';
import { ApiManager } from '../../pendulum/apiManager';
import { waitUntilTrue } from '../../../helpers/functions';
import { submitMoonbeamXcm, submitXcm } from '../../xcm/send';
+import logger from '../../../../config/logger';
export class MoonbeamToPendulumXcmPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -55,7 +56,7 @@ export class MoonbeamToPendulumXcmPhaseHandler extends BasePhaseHandler {
}
try {
- console.log('waiting for token to arrive on pendulum...');
+ logger.info('waiting for token to arrive on pendulum...');
await waitUntilTrue(didInputTokenArrivedOnPendulum, 5000);
} catch (e) {
console.error('Error while waiting for transaction receipt:', e);
diff --git a/api/src/api/services/phases/handlers/nabla-approve-handler.ts b/api/src/api/services/phases/handlers/nabla-approve-handler.ts
index 6ddf2f545..332aae7b8 100644
--- a/api/src/api/services/phases/handlers/nabla-approve-handler.ts
+++ b/api/src/api/services/phases/handlers/nabla-approve-handler.ts
@@ -4,6 +4,7 @@ import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
import { ApiManager } from '../../pendulum/apiManager';
+import logger from '../../../../config/logger';
export class NablaApprovePhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -30,7 +31,7 @@ export class NablaApprovePhaseHandler extends BasePhaseHandler {
const result = await submitExtrinsic(approvalExtrinsic);
if (result.status.type === 'error') {
- console.log(`Could not approve token: ${result.status.error.toString()}`);
+ logger.error(`Could not approve token: ${result.status.error.toString()}`);
throw new Error('Could not approve token');
}
@@ -45,7 +46,7 @@ export class NablaApprovePhaseHandler extends BasePhaseHandler {
} else {
errorMessage = 'Something went wrong';
}
- console.log(`Could not approve the required amount of token: ${errorMessage}`);
+ logger.error(`Could not approve the required amount of token: ${errorMessage}`);
throw e;
}
diff --git a/api/src/api/services/phases/handlers/nabla-swap-handler.ts b/api/src/api/services/phases/handlers/nabla-swap-handler.ts
index 850347d46..5f3fee643 100644
--- a/api/src/api/services/phases/handlers/nabla-swap-handler.ts
+++ b/api/src/api/services/phases/handlers/nabla-swap-handler.ts
@@ -9,6 +9,7 @@ import { ApiManager } from '../../pendulum/apiManager';
import { routerAbi } from '../../../../contracts/Router';
import { defaultReadLimits } from '../../../helpers/contracts';
import { StateMetadata } from '../meta-state-types';
+import logger from '../../../../config/logger';
export class NablaSwapPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -41,7 +42,7 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler {
try {
const { txData: nablaSwapTransaction } = this.getPresignedTransaction(state, 'nablaSwap');
- console.log('before RESPONSE prepareNablaSwapTransaction');
+ logger.info('before RESPONSE prepareNablaSwapTransaction');
// get an up to date quote for the AMM
const response = await readMessage({
abi: new Abi(routerAbi),
@@ -64,7 +65,7 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler {
const ouputAmountQuoteRaw = Big(response.value[0].toString());
if (ouputAmountQuoteRaw.lt(Big(nablaSoftMinimumOutputRaw))) {
- console.log(
+ logger.info(
`The estimated output amount is too low to swap. Expected: ${nablaSoftMinimumOutputRaw}, got: ${ouputAmountQuoteRaw}`,
);
throw new Error("Won't execute the swap now. The estimated output amount is too low.");
@@ -74,7 +75,7 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler {
const result = await submitExtrinsic(swapExtrinsic);
if (result.status.type === 'error') {
- console.log(`Could not swap token: ${result.status.error.toString()}`);
+ logger.error(`Could not swap token: ${result.status.error.toString()}`);
throw new Error('Could not swap token');
}
} catch (e) {
diff --git a/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts b/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
index 7fd352c4f..be510a231 100644
--- a/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
+++ b/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts
@@ -8,6 +8,7 @@ import { StateMetadata } from '../meta-state-types';
import { checkBalancePeriodically } from '../../stellar/checkBalance';
import { createVaultService } from '../../stellar/vaultService';
+import logger from '../../../../config/logger';
const maxWaitingTimeMinutes = 10;
const maxWaitingTimeMs = maxWaitingTimeMinutes * 60 * 1000;
@@ -67,12 +68,12 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
stellarTarget.stellarTokenDetails.stellarAsset.issuer.hex,
outputAmountBeforeFees.raw,
);
- console.log(`Requesting redeem of ${outputAmountBeforeFees.units} tokens for vault ${vaultService.vaultId}`);
+ logger.info(`Requesting redeem of ${outputAmountBeforeFees.units} tokens for vault ${vaultService.vaultId}`);
const redeemExtrinsic = decodeSubmittableExtrinsic(spacewalkRedeemTransaction, pendulumNode.api);
const redeemRequestEvent = await vaultService.submitRedeem(pendulumEphemeralAddress, redeemExtrinsic);
- console.log(`Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${vaultService.vaultId}`);
+ logger.info(`Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${vaultService.vaultId}`);
// TODO we may want to use a singleton for the event listener across the backend.
const eventListener = EventListener.getEventListener(pendulumNode.api);
@@ -82,7 +83,7 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
} catch (e) {
// This is a potentially recoverable error (due to redeem request done before app shut down, but not registered)
if ((e as Error).message.includes('AmountExceedsUserBalance')) {
- console.log(`Recovery mode: Redeem already performed. Waiting for execution and Stellar balance arrival.`);
+ logger.info(`Recovery mode: Redeem already performed. Waiting for execution and Stellar balance arrival.`);
await this.waitForOutputTokensToArriveOnStellar(
outputAmountBeforeFees.units,
stellarEphemeralAccountId,
@@ -92,7 +93,7 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
}
// Generic failure of the extrinsic itself OR lack of funds to even make the transaction
- console.log(`Failed to request redeem: ${e}`);
+ logger.error(`Failed to request redeem: ${e}`);
throw new Error(`Failed to request redeem`);
}
}
@@ -115,7 +116,7 @@ export class SpacewalkRedeemPhaseHandler extends BasePhaseHandler {
stellarPollingTimeMs,
maxWaitingTimeMs,
);
- console.log('Balance check completed successfully.');
+ logger.info('Balance check completed successfully.');
} catch (balanceCheckError) {
throw new Error(`Stellar balance did not arrive on time`);
}
diff --git a/api/src/api/services/phases/handlers/stellar-payment-handler.ts b/api/src/api/services/phases/handlers/stellar-payment-handler.ts
index 37ea1d6e2..4efdd3e87 100644
--- a/api/src/api/services/phases/handlers/stellar-payment-handler.ts
+++ b/api/src/api/services/phases/handlers/stellar-payment-handler.ts
@@ -2,6 +2,7 @@ import { HORIZON_URL, RampPhase } from 'shared';
import { Horizon, NetworkError, Networks, Transaction } from 'stellar-sdk';
import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
+import logger from '../../../../config/logger';
const NETWORK_PASSPHRASE = Networks.PUBLIC;
const horizonServer = new Horizon.Server(HORIZON_URL);
@@ -26,12 +27,12 @@ export class StellarPaymentPhaseHandler extends BasePhaseHandler {
const horizonError = e as NetworkError;
if (horizonError.response.data?.status === 400) {
- console.log(
+ logger.error(
`Could not submit the offramp transaction ${JSON.stringify(horizonError.response.data.extras.result_codes)}`,
);
// check https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions
if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') {
- console.log('Assuming offramp was already performed.');
+ logger.info('Assuming offramp was already performed.');
return this.transitionToNextPhase(state, 'complete');
}
diff --git a/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts b/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts
index eb816d10b..77332c8b9 100644
--- a/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts
+++ b/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts
@@ -5,6 +5,7 @@ import Big from 'big.js';
import { StateMetadata } from '../meta-state-types';
import { ApiManager } from '../../pendulum/apiManager';
import { getFundingAccount } from '../../../controllers/subsidize.controller';
+import logger from '../../../../config/logger';
export class SubsidizePostSwapPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -37,7 +38,7 @@ export class SubsidizePostSwapPhaseHandler extends BasePhaseHandler {
const requiredAmount = Big(outputAmountBeforeFees.raw).sub(currentBalance);
if (requiredAmount.gt(Big(0))) {
// Do the actual subsidizing.
- console.log('Subsidizing post-swap with', requiredAmount.toString());
+ logger.info('Subsidizing post-swap with', requiredAmount.toString());
const fundingAccountKeypair = getFundingAccount();
await pendulumNode.api.tx.tokens
.transfer(
diff --git a/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts b/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
index fe671b816..b96fd670d 100644
--- a/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
+++ b/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
@@ -1,10 +1,11 @@
+import { RampPhase } from 'shared';
+import Big from 'big.js';
import { BasePhaseHandler } from '../base-phase-handler';
-import { FiatToken, RampPhase } from 'shared';
import RampState from '../../../../models/rampState.model';
-import Big from 'big.js';
import { StateMetadata } from '../meta-state-types';
import { ApiManager } from '../../pendulum/apiManager';
import { getFundingAccount } from '../../../controllers/subsidize.controller';
+import logger from '../../../../config/logger';
export class SubsidizePreSwapPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
@@ -37,8 +38,8 @@ export class SubsidizePreSwapPhaseHandler extends BasePhaseHandler {
const requiredAmount = Big(inputAmountBeforeSwapRaw).sub(currentBalance);
if (requiredAmount.gt(Big(0))) {
// Do the actual subsidizing.
- console.log('Subsidizing pre-swap with', requiredAmount.toString());
- console.log(
+ logger.info('Subsidizing pre-swap with', requiredAmount.toString());
+ logger.info(
'Target value: ',
inputAmountBeforeSwapRaw.toString(),
'Current value: ',
diff --git a/api/src/api/services/stellar/checkBalance.ts b/api/src/api/services/stellar/checkBalance.ts
index 85814760a..4e28822a7 100644
--- a/api/src/api/services/stellar/checkBalance.ts
+++ b/api/src/api/services/stellar/checkBalance.ts
@@ -2,6 +2,7 @@ import { Horizon } from 'stellar-sdk';
import Big from 'big.js';
import { HORIZON_URL } from '../../../constants/constants';
+import logger from '../../../config/logger';
export function checkBalancePeriodically(
stellarTargetAccountId: string,
@@ -15,7 +16,7 @@ export function checkBalancePeriodically(
const intervalId = setInterval(async () => {
try {
const someBalanceUnits = await getStellarBalanceUnits(stellarTargetAccountId, stellarAssetCode);
- console.log(`Balance check: ${someBalanceUnits.toString()} / ${amountDesiredUnitsBig.toString()}`);
+ logger.info(`Balance check: ${someBalanceUnits.toString()} / ${amountDesiredUnitsBig.toString()}`);
if (someBalanceUnits.gte(amountDesiredUnitsBig)) {
clearInterval(intervalId);
@@ -51,7 +52,7 @@ const getStellarBalanceUnits = async (publicKey: string, assetCode: string): Pro
return new Big(balanceUnits);
} catch (error) {
- console.log(error);
+ logger.error(error)
throw new Error('Error Reading Stellar Balance');
}
};
diff --git a/api/src/api/services/stellar/getVaults.ts b/api/src/api/services/stellar/getVaults.ts
index efed92f68..be855d204 100644
--- a/api/src/api/services/stellar/getVaults.ts
+++ b/api/src/api/services/stellar/getVaults.ts
@@ -1,5 +1,6 @@
import { ApiPromise } from '@polkadot/api';
import Big from 'big.js';
+import logger from '../../../config/logger';
function vaultHasEnoughRedeemable(vault: any, redeemableAmount: string): boolean {
// issuedTokens - toBeRedeemedTokens = redeemableTokens
@@ -33,7 +34,7 @@ export async function getVaultsForCurrency(
if (vaultsForCurrency.length === 0) {
const errorMessage = `No vaults found for currency ${assetCodeHex} and amount ${redeemableAmountRaw}`;
- console.log(errorMessage);
+ logger.error(errorMessage);
throw new Error(errorMessage);
}
diff --git a/api/src/api/services/stellar/loadAccount.ts b/api/src/api/services/stellar/loadAccount.ts
index f3f0c2634..1ac79f37a 100644
--- a/api/src/api/services/stellar/loadAccount.ts
+++ b/api/src/api/services/stellar/loadAccount.ts
@@ -1,5 +1,6 @@
import { Horizon } from 'stellar-sdk';
import { HORIZON_URL } from '../../../constants/constants';
+import logger from '../../../config/logger';
const horizonServer = new Horizon.Server(HORIZON_URL);
@@ -24,7 +25,7 @@ export async function loadAccountWithRetry(
// The account does not exist
return null;
}
- console.log(`Attempt ${i + 1} to load account ${ephemeralAccountId} failed: ${err}`);
+ logger.info(`Attempt ${i + 1} to load account ${ephemeralAccountId} failed: ${err}`);
lastError = err;
}
}
diff --git a/api/src/api/services/stellar/vaultService.ts b/api/src/api/services/stellar/vaultService.ts
index 69303151e..bfd4f3914 100644
--- a/api/src/api/services/stellar/vaultService.ts
+++ b/api/src/api/services/stellar/vaultService.ts
@@ -1,10 +1,11 @@
import { SpacewalkPrimitivesVaultId } from '@pendulum-chain/types/interfaces';
import { SubmittableExtrinsic } from '@polkadot/api-base/types';
-import { API } from '../pendulum/apiManager';
-import { getVaultsForCurrency } from './getVaults';
import { ISubmittableResult } from '@polkadot/types/types';
import { getAddressForFormat, parseEventRedeemRequest, SpacewalkRedeemRequestEvent } from 'shared';
+import { API } from '../pendulum/apiManager';
+import { getVaultsForCurrency } from './getVaults';
+import logger from '../../../config/logger';
export async function createVaultService(
apiComponents: API,
@@ -44,33 +45,30 @@ export class VaultService {
const { status, events, dispatchError } = submissionResult;
if (status.isFinalized) {
- console.log(`Requested redeem for vault ${this.vaultId} with status ${status.type}`);
+ logger.info(`Requested redeem for vault ${this.vaultId} with status ${status.type}`);
// Try to find a 'system.ExtrinsicFailed' event
- const systemExtrinsicFailedEvent = events.find((record) => {
- return record.event.section === 'system' && record.event.method === 'ExtrinsicFailed';
- });
+ const systemExtrinsicFailedEvent = events.find(
+ (record) => record.event.section === 'system' && record.event.method === 'ExtrinsicFailed',
+ );
if (dispatchError) {
reject(this.handleDispatchError(dispatchError, systemExtrinsicFailedEvent, 'Redeem Request'));
}
- //find all redeem request events and filter the one that matches the requester
- const redeemEvents = events.filter((event) => {
- return (
- event.event.section.toLowerCase() === 'redeem' && event.event.method.toLowerCase() === 'requestredeem'
- );
- });
+ // find all redeem request events and filter the one that matches the requester
+ const redeemEvents = events.filter(
+ (event) =>
+ event.event.section.toLowerCase() === 'redeem' && event.event.method.toLowerCase() === 'requestredeem',
+ );
const event = redeemEvents
.map((event) => parseEventRedeemRequest(event))
- .filter((event) => {
- return event.redeemer === getAddressForFormat(senderAddress, this.apiComponents!.ss58Format);
- });
+ .filter((event) => event.redeemer === getAddressForFormat(senderAddress, this.apiComponents!.ss58Format));
if (event.length == 0) {
reject(new Error(`No redeem event found for account ${senderAddress}`));
}
- //we should only find one event corresponding to the issue request
+ // we should only find one event corresponding to the issue request
if (event.length != 1) {
reject(new Error('Inconsistent amount of redeem request events for account'));
}
@@ -91,7 +89,8 @@ export class VaultService {
const { name, section, method } = decoded;
return new Error(`Dispatch error: ${section}.${method}:: ${name}`);
- } else if (systemExtrinsicFailedEvent) {
+ }
+ if (systemExtrinsicFailedEvent) {
const eventName =
systemExtrinsicFailedEvent?.event.data && systemExtrinsicFailedEvent?.event.data.length > 0
? systemExtrinsicFailedEvent?.event.data[0].toString()
@@ -101,12 +100,12 @@ export class VaultService {
phase,
event: { method, section },
} = systemExtrinsicFailedEvent;
- console.log(`Extrinsic failed in phase ${phase.toString()} with ${section}.${method}:: ${eventName}`);
+ logger.error(`Extrinsic failed in phase ${phase.toString()} with ${section}.${method}:: ${eventName}`);
return new Error(`Failed to dispatch ${extrinsicCalled}`);
- } else {
- console.log('Encountered some other error: ', dispatchError?.toString(), JSON.stringify(dispatchError));
- return new Error(`Unknown error during ${extrinsicCalled}`);
}
+
+ logger.error('Encountered some other error: ', dispatchError?.toString(), JSON.stringify(dispatchError));
+ return new Error(`Unknown error during ${extrinsicCalled}`);
}
}
diff --git a/api/src/api/services/transactions/nabla/approve.ts b/api/src/api/services/transactions/nabla/approve.ts
index 5c705b9f2..d88685cae 100644
--- a/api/src/api/services/transactions/nabla/approve.ts
+++ b/api/src/api/services/transactions/nabla/approve.ts
@@ -12,6 +12,7 @@ import {
defaultWriteLimits,
parseContractBalanceResponse,
} from '../../../helpers/contracts';
+import logger from '../../../../config/logger';
export interface PrepareNablaApproveParams {
inputTokenDetails: PendulumDetails;
@@ -37,7 +38,7 @@ async function createApproveExtrinsic({
contractAbi,
callerAddress,
}: CreateApproveExtrinsicOptions) {
- console.log('write', `call approve ${token} for ${spender} with amount ${amount} `);
+ logger.info('write', `call approve ${token} for ${spender} with amount ${amount} `);
const { execution, result: readMessageResult } = await createExecuteMessageExtrinsic({
abi: contractAbi,
@@ -82,7 +83,7 @@ export async function prepareNablaApproveTransaction({
if (response.type !== 'success') {
const message = 'Could not load token allowance';
- console.log(message);
+ logger.info(message);
throw new Error(message);
}
@@ -91,7 +92,7 @@ export async function prepareNablaApproveTransaction({
// maybe do allowance
if (currentAllowance === undefined || currentAllowance.rawBalance.lt(Big(amountRaw))) {
try {
- console.log(`Preparing transaction to approve tokens: ${amountRaw} ${inputTokenDetails.pendulumAssetSymbol}`);
+ logger.info(`Preparing transaction to approve tokens: ${amountRaw} ${inputTokenDetails.pendulumAssetSymbol}`);
return createApproveExtrinsic({
api,
amount: amountRaw,
@@ -101,7 +102,7 @@ export async function prepareNablaApproveTransaction({
callerAddress: pendulumEphemeralAddress,
});
} catch (e) {
- console.log(`Could not approve token: ${e}`);
+ logger.info(`Could not approve token: ${e}`);
return Promise.reject('Could not approve token');
}
}
diff --git a/api/src/api/services/transactions/nabla/swap.ts b/api/src/api/services/transactions/nabla/swap.ts
index 1a70b5f2d..aead42056 100644
--- a/api/src/api/services/transactions/nabla/swap.ts
+++ b/api/src/api/services/transactions/nabla/swap.ts
@@ -6,6 +6,7 @@ import { createWriteOptions, defaultWriteLimits } from '../../../helpers/contrac
import { API } from '../../pendulum/apiManager';
import { config } from '../../../../config';
import { routerAbi } from '../../../../contracts/Router';
+import logger from '../../../../config/logger';
export interface PrepareNablaSwapParams {
inputTokenDetails: PendulumDetails;
@@ -88,7 +89,7 @@ export async function prepareNablaSwapTransaction({
callerAddress: pendulumEphemeralAddress,
});
} catch (e) {
- console.log(`Error creating swap extrinsic: ${e}`);
+ logger.error(`Error creating swap extrinsic: ${e}`);
throw Error("Couldn't create swap extrinsic");
}
}
diff --git a/api/src/api/services/transactions/offrampTransactions.ts b/api/src/api/services/transactions/offrampTransactions.ts
index 77d0decf2..d64e9b58f 100644
--- a/api/src/api/services/transactions/offrampTransactions.ts
+++ b/api/src/api/services/transactions/offrampTransactions.ts
@@ -129,7 +129,6 @@ export async function prepareOfframpTransactions({
pendulumAddressDestination: pendulumEphemeralEntry.address,
fromAddress: userAddress,
});
- console.log('squid txs done');
unsignedTxs.push({
txData: encodeEvmTransactionData(approveData) as any,
phase: 'squidrouterApprove',
@@ -161,7 +160,7 @@ export async function prepareOfframpTransactions({
'usdc',
inputAmountRaw,
);
- console.log('assethub to pendulum txs done');
+ logger.info('assethub to pendulum txs done');
unsignedTxs.push({
txData: encodeSubmittableExtrinsic(assethubToPendulumTransaction),
phase: 'assethubToPendulum',
@@ -173,7 +172,7 @@ export async function prepareOfframpTransactions({
// Create unsigned transactions for each ephemeral account
for (const account of signingAccounts) {
- console.log(`Processing account ${account.address} on network ${account.network}`);
+ logger.info(`Processing account ${account.address} on network ${account.network}`);
const accountNetworkId = getNetworkId(account.network);
if (!isOnChainToken(quote.inputCurrency)) {
@@ -222,7 +221,6 @@ export async function prepareOfframpTransactions({
nonce: 1,
signer: account.address,
});
- console.log('nabla txs done');
stateMeta = {
...stateMeta,
nablaSoftMinimumOutputRaw,
@@ -233,7 +231,6 @@ export async function prepareOfframpTransactions({
inputTokenPendulumDetails.pendulumCurrencyId,
outputTokenPendulumDetails.pendulumCurrencyId,
);
- console.log('pendulum cleanup done');
unsignedTxs.push({
txData: encodeSubmittableExtrinsic(pendulumCleanupTransaction),
phase: 'pendulumCleanup',
@@ -254,7 +251,6 @@ export async function prepareOfframpTransactions({
outputAmountBeforeFeesRaw,
outputTokenDetails.pendulumCurrencyId,
);
- console.log('pendulum to moonbeam txs done');
unsignedTxs.push({
txData: encodeSubmittableExtrinsic(pendulumToMoonbeamTransaction),
phase: 'pendulumToMoonbeam',
@@ -313,7 +309,6 @@ export async function prepareOfframpTransactions({
if (!stellarPaymentData) {
throw new Error('Stellar payment data must be provided for offramp');
}
- console.log('build and merge ...');
const { paymentTransaction, mergeAccountTransaction, createAccountTransaction, expectedSequenceNumber } =
await buildPaymentAndMergeTx({
ephemeralAccountId: account.address,
@@ -321,7 +316,6 @@ export async function prepareOfframpTransactions({
paymentData: stellarPaymentData,
tokenConfigStellar: outputTokenDetails,
});
- console.log('build and merge done');
unsignedTxs.push({
txData: createAccountTransaction,
phase: 'stellarCreateAccount',
diff --git a/api/src/api/services/transactions/onrampTransactions.ts b/api/src/api/services/transactions/onrampTransactions.ts
index 55b7f3be2..bfb47bd1f 100644
--- a/api/src/api/services/transactions/onrampTransactions.ts
+++ b/api/src/api/services/transactions/onrampTransactions.ts
@@ -1,5 +1,7 @@
import {
+ AccountMeta,
AMM_MINIMUM_OUTPUT_SOFT_MARGIN,
+ encodeSubmittableExtrinsic,
getAnyFiatTokenDetails,
getNetworkFromDestination,
getNetworkId,
@@ -12,8 +14,6 @@ import {
isOnChainTokenDetails,
Networks,
UnsignedTx,
- AccountMeta,
- encodeSubmittableExtrinsic,
} from 'shared';
import Big from 'big.js';
import { QuoteTicketAttributes } from '../../../models/quoteTicket.model';
@@ -23,7 +23,7 @@ import { createMoonbeamToPendulumXCM } from './xcm/moonbeamToPendulum';
import { createPendulumToMoonbeamTransfer } from './xcm/pendulumToMoonbeam';
import { multiplyByPowerOfTen } from '../pendulum/helpers';
import { createPendulumToAssethubTransfer } from './xcm/pendulumToAssethub';
-import { createNablaTransactionsForOnramp, createNablaTransactionsForQuote } from './nabla';
+import { createNablaTransactionsForOnramp } from './nabla';
import { preparePendulumCleanupTransaction } from './pendulum/cleanup';
import { prepareMoonbeamCleanupTransaction } from './moonbeam/cleanup';
import { StateMetadata } from '../phases/meta-state-types';
@@ -174,7 +174,6 @@ export async function prepareOnrampTransactions(
nablaSoftMinimumOutput,
outputTokenDetails.pendulumDecimals,
).toFixed();
- console.log('fixed? soft minimum output raw....', nablaSoftMinimumOutputRaw);
const { approveTransaction, swapTransaction } = await createNablaTransactionsForOnramp(
inputAmountUnits,
quote,
diff --git a/api/src/api/services/transactions/stellar/offrampTransaction.ts b/api/src/api/services/transactions/stellar/offrampTransaction.ts
index 54ad65d19..e67130bee 100644
--- a/api/src/api/services/transactions/stellar/offrampTransaction.ts
+++ b/api/src/api/services/transactions/stellar/offrampTransaction.ts
@@ -3,6 +3,7 @@ import { FUNDING_SECRET, STELLAR_BASE_FEE, SEQUENCE_TIME_WINDOW_IN_SECONDS } fro
import { StellarTokenDetails, PaymentData, HORIZON_URL, STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS } from 'shared';
import { HorizonServer } from 'stellar-sdk/lib/horizon/server';
import Big from 'big.js';
+import logger from '../../../../config/logger';
const FUNDING_PUBLIC_KEY = FUNDING_SECRET ? Keypair.fromSecret(FUNDING_SECRET).publicKey() : '';
const NETWORK_PASSPHRASE = Networks.PUBLIC;
@@ -33,7 +34,7 @@ export async function buildPaymentAndMergeTx({
const maxTime = Date.now() + 1000 * 60 * 10;
if (!FUNDING_SECRET) {
- console.log('Secret not defined');
+ logger.error('Stellar funding secret not defined');
throw new Error('Stellar funding secret not defined');
}
From 4880585241f6954e66c8ef24768d1ba8d70aeca0 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 18:50:21 +0200
Subject: [PATCH 09/52] Show quote timer in ramp summary
---
.../components/OfframpSummaryDialog/index.tsx | 36 ++++++++++++++++++-
frontend/src/translations/en.json | 3 +-
frontend/src/translations/pt.json | 3 +-
3 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index 496cb3220..7fcf2795f 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -1,5 +1,5 @@
import { ArrowDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
-import { useState, FC, useMemo } from 'react';
+import { useState, FC, useMemo, useEffect } from 'react';
import Big from 'big.js';
import {
@@ -115,6 +115,34 @@ const BRLOnrampDetails = () => {
const rampDirection = useRampDirection();
const { t } = useTranslation();
const rampState = useRampState();
+ const [timeLeft, setTimeLeft] = useState({ minutes: 5, seconds: 0 });
+
+ useEffect(() => {
+ if (!rampState?.ramp?.createdAt) return;
+
+ const createdAtTimestamp = new Date(rampState.ramp.createdAt).getTime();
+ const targetTimestamp = createdAtTimestamp + 5 * 60 * 1000; // 5 minutes in milliseconds
+
+ const intervalId = setInterval(() => {
+ const now = Date.now();
+ const diff = targetTimestamp - now;
+
+ if (diff <= 0) {
+ setTimeLeft({ minutes: 0, seconds: 0 });
+ clearInterval(intervalId);
+ // Optionally: Add logic here to handle timer expiration, e.g., close dialog, show message
+ return;
+ }
+
+ const minutes = Math.floor((diff / (1000 * 60)) % 60);
+ const seconds = Math.floor((diff / 1000) % 60);
+ setTimeLeft({ minutes, seconds });
+ }, 1000);
+
+ // Cleanup interval on component unmount or when dependencies change
+ return () => clearInterval(intervalId);
+ }, [rampState?.ramp?.createdAt]);
+
console.log("In BRLAOnrampDetails", rampState, rampDirection);
if (rampDirection !== RampDirection.ONRAMP) return null;
@@ -122,10 +150,13 @@ const BRLOnrampDetails = () => {
if (!rampState?.ramp?.brCode) return null;
console.log("after brcode")
+ const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
+
return (
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.title')}
+ {/* Timer moved below */}
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.description')}
@@ -138,6 +169,9 @@ const BRLOnrampDetails = () => {
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.pixCode')}:
+
+ {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
+
);
};
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index caf66f8ab..72031c909 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -187,7 +187,8 @@
"description": "Continue in your bank's app",
"qrCode": "Select Pix in your bank's app and scan the QR code below.",
"copyCode": "Or copy the Pix code below, select Pix in your bank's app and paste the code provided.",
- "pixCode": "Pix code"
+ "pixCode": "Pix code",
+ "timerLabel": "Quote expires in:"
},
"headerText": {
"buy": "You're buying",
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index 32bd48ade..77bd29d24 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -187,7 +187,8 @@
"description": "Continue no app do seu banco",
"qrCode": "Selecione PIX no app do seu banco e escaneie o QR code abaixo.",
"copyCode": "Ou copie o código PIX abaixo, selecione PIX no app do seu banco e cole o código fornecido.",
- "pixCode": "Código PIX"
+ "pixCode": "Código PIX",
+ "timerLabel": "Cotação expira em:"
},
"headerText": {
"buy": "Você está comprando",
From 2f44ac9b31f6d333a5ce38c5f6df093cecf92fd1 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 18:54:59 +0200
Subject: [PATCH 10/52] Make text of CopyButton copyable by default
---
frontend/src/components/CopyButton/index.tsx | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/frontend/src/components/CopyButton/index.tsx b/frontend/src/components/CopyButton/index.tsx
index be019d8ac..c33fcae3c 100644
--- a/frontend/src/components/CopyButton/index.tsx
+++ b/frontend/src/components/CopyButton/index.tsx
@@ -1,3 +1,5 @@
+import { useClipboard } from '../../hooks/useClipboard';
+
interface CopyButtonProps {
text: string;
className?: string;
@@ -5,8 +7,13 @@ interface CopyButtonProps {
onClick?: () => void;
}
-export const CopyButton = ({ text, className, onClick }: CopyButtonProps) => (
-
-);
+export const CopyButton = (props: CopyButtonProps) => {
+ const clipboard = useClipboard();
+ const onClick = props.onClick || (() => clipboard.copyToClipboard(props.text));
+
+ return (
+
+ );
+};
From ba1accc1f15364aa6e1287049c2be3d99d7928c7 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 19:00:31 +0200
Subject: [PATCH 11/52] Disable submit button if ramp is expired
---
.../components/OfframpSummaryDialog/index.tsx | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index 7fcf2795f..1116dfd63 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -143,12 +143,8 @@ const BRLOnrampDetails = () => {
return () => clearInterval(intervalId);
}, [rampState?.ramp?.createdAt]);
-
- console.log("In BRLAOnrampDetails", rampState, rampDirection);
if (rampDirection !== RampDirection.ONRAMP) return null;
- console.log("after rampDirection")
if (!rampState?.ramp?.brCode) return null;
- console.log("after brcode")
const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
@@ -167,9 +163,8 @@ const BRLOnrampDetails = () => {
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.pixCode')}:
-
+
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
@@ -255,8 +250,9 @@ export const OfframpSummaryDialog: FC = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
const { selectedNetwork } = useNetwork();
- const { setRampExecutionInput, setRampInitiating, setRampStarted, setRampSummaryVisible, setRampPaymentConfirmed } = useRampActions();
- const offrampState = useRampState();
+ const { setRampExecutionInput, setRampInitiating, setRampStarted, setRampSummaryVisible, setRampPaymentConfirmed } =
+ useRampActions();
+ const rampState = useRampState();
const executionInput = useRampExecutionInput();
const visible = useRampSummaryVisible();
const { onRampConfirm } = useRampSubmission();
@@ -272,10 +268,16 @@ export const OfframpSummaryDialog: FC = () => {
if (!isOnramp) {
if (!anchorUrl && getAnyFiatTokenDetails(fiatToken).type === TokenType.Stellar) return true;
if (!executionInput.brlaEvmAddress && getAnyFiatTokenDetails(fiatToken).type === 'moonbeam') return true;
+ // For onramps, we register immediately when opening this summary, so the ramp should be available.
+ if (!rampState?.ramp === undefined) return true;
+ // Check if ramp is already expired
+ if (rampState?.ramp?.createdAt && Date.now() - new Date(rampState?.ramp?.createdAt).getTime() > 5 * 60 * 1000) {
+ return true;
+ }
}
return isSubmitted;
- }, [anchorUrl, executionInput, fiatToken, isOnramp, isSubmitted]);
+ }, [anchorUrl, executionInput, fiatToken, isOnramp, isSubmitted, rampState?.ramp]);
if (!visible) return null;
if (!executionInput) return null;
@@ -318,7 +320,7 @@ export const OfframpSummaryDialog: FC = () => {
style={{ flex: '1 1 calc(50% - 0.75rem/2)' }}
onClick={onSubmit}
>
- {offrampState !== undefined ? (
+ {rampState !== undefined ? (
<>
{t('components.dialogs.OfframpSummaryDialog.processing')}
>
From d61b12e9fc944f7492b203f4f971fe081664bc20 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 19:05:44 +0200
Subject: [PATCH 12/52] Remove custom message for nablaApprove phase
---
frontend/src/pages/progress/phaseMessages.ts | 110 +++++++++----------
1 file changed, 55 insertions(+), 55 deletions(-)
diff --git a/frontend/src/pages/progress/phaseMessages.ts b/frontend/src/pages/progress/phaseMessages.ts
index f45093f72..182dc5b0b 100644
--- a/frontend/src/pages/progress/phaseMessages.ts
+++ b/frontend/src/pages/progress/phaseMessages.ts
@@ -1,59 +1,59 @@
import { FiatToken, getAnyFiatTokenDetails, getNetworkDisplayName, getNetworkFromDestination, getOnChainTokenDetailsOrDefault, isNetworkEVM, Networks, OnChainToken, RampPhase } from 'shared';
import { RampState } from '../../types/phases';
-
-// TODO type t translation function
-export function getMessageForPhase(ramp: RampState | undefined, t: any): string {
-
- if (!ramp || !ramp.ramp) return t('pages.progress.initial');
-
- const currentState = ramp.ramp!
- const quote = ramp.quote;
- const currentPhase = currentState.currentPhase;
-
- const fromNetwork = getNetworkFromDestination(quote.from);
- const toNetwork = getNetworkFromDestination(quote.to);
-
- const inputAssetSymbol = currentState.type === 'off' ? getOnChainTokenDetailsOrDefault(fromNetwork!, quote.inputCurrency as OnChainToken).assetSymbol : getAnyFiatTokenDetails(quote.inputCurrency as FiatToken).assetSymbol;
- const outputAssetSymbol = currentState.type === 'off' ? getAnyFiatTokenDetails(quote.outputCurrency as FiatToken).assetSymbol : getOnChainTokenDetailsOrDefault(toNetwork!, quote.outputCurrency as OnChainToken).assetSymbol;
-
-
- // For offramp, network means the starting network. For onramp, network referres the destination network.
- const network = currentState.type === 'off' ? getNetworkDisplayName(fromNetwork!) : getNetworkDisplayName(toNetwork!);
- const isEVM = currentState.type === 'off' ? isNetworkEVM(fromNetwork!) : isNetworkEVM(toNetwork!);
-
- if (currentPhase === 'complete') return t('pages.progress.success');
-
- const getSwappingMessage = () => t('pages.progress.swappingTo', { assetSymbol: outputAssetSymbol });
- const getApproveMessage = () => t('pages.progress.nablaApprove', { assetSymbol: inputAssetSymbol });
- const getMoonbeamToPendulumMessage = () => t('pages.progress.moonbeamToPendulum', { assetSymbol: inputAssetSymbol });
- const getSquidrouterSwapMessage = () => t('pages.progress.squidrouterSwap', { assetSymbol: outputAssetSymbol, network: toNetwork });
-
- const getTransferringMessage = () => t('pages.progress.transferringToLocalPartner');
-
- const messages: Record = {
- initial: t('pages.progress.initial'),
- stellarCreateAccount: t('pages.progress.createStellarAccount'),
- fundEphemeral: t('pages.progress.fundEphemeral'),
- nablaApprove: getApproveMessage(),
- nablaSwap: getSwappingMessage(),
- subsidizePreSwap: getSwappingMessage(),
- subsidizePostSwap: getSwappingMessage(),
- moonbeamToPendulum: getMoonbeamToPendulumMessage(),
- moonbeamToPendulumXcm: getMoonbeamToPendulumMessage(),
- assethubToPendulum: t('pages.progress.assethubToPendulum', { assetSymbol: inputAssetSymbol }),
- pendulumToMoonbeam: t('pages.progress.pendulumToMoonbeam', { assetSymbol: outputAssetSymbol }),
- spacewalkRedeem: t('pages.progress.executeSpacewalkRedeem', { assetSymbol: outputAssetSymbol }),
- brlaPayoutOnMoonbeam: getTransferringMessage(),
- stellarPayment: t('pages.progress.stellarPayment', { assetSymbol: outputAssetSymbol }),
- squidrouterApprove: getSquidrouterSwapMessage(),
- squidrouterSwap: getSquidrouterSwapMessage(),
- pendulumToAssethub: t('pages.progress.pendulumToAssethub', { assetSymbol: outputAssetSymbol }),
- brlaTeleport: t('pages.progress.brlaTeleport'),
- failed: '', // Not relevant for progress page
- complete: '', // Not relevant for progress page
- timedOut: '', // Not relevant for progress page
- };
-
- return messages[currentPhase];
+import { TFunction } from 'i18next';
+
+export function getMessageForPhase(ramp: RampState | undefined, t: TFunction<'translation', undefined>): string {
+ if (!ramp || !ramp.ramp) return t('pages.progress.initial');
+
+ const currentState = ramp.ramp!;
+ const quote = ramp.quote;
+ const currentPhase = currentState.currentPhase;
+
+ const fromNetwork = getNetworkFromDestination(quote.from);
+ const toNetwork = getNetworkFromDestination(quote.to);
+
+ const inputAssetSymbol =
+ currentState.type === 'off'
+ ? getOnChainTokenDetailsOrDefault(fromNetwork!, quote.inputCurrency as OnChainToken).assetSymbol
+ : getAnyFiatTokenDetails(quote.inputCurrency as FiatToken).assetSymbol;
+ const outputAssetSymbol =
+ currentState.type === 'off'
+ ? getAnyFiatTokenDetails(quote.outputCurrency as FiatToken).assetSymbol
+ : getOnChainTokenDetailsOrDefault(toNetwork!, quote.outputCurrency as OnChainToken).assetSymbol;
+
+ if (currentPhase === 'complete') return t('pages.progress.success');
+
+ const getSwappingMessage = () => t('pages.progress.swappingTo', { assetSymbol: outputAssetSymbol });
+ const getMoonbeamToPendulumMessage = () => t('pages.progress.moonbeamToPendulum', { assetSymbol: inputAssetSymbol });
+ const getSquidrouterSwapMessage = () =>
+ t('pages.progress.squidrouterSwap', { assetSymbol: outputAssetSymbol, network: toNetwork });
+
+ const getTransferringMessage = () => t('pages.progress.transferringToLocalPartner');
+
+ const messages: Record = {
+ initial: t('pages.progress.initial'),
+ stellarCreateAccount: t('pages.progress.createStellarAccount'),
+ fundEphemeral: t('pages.progress.fundEphemeral'),
+ nablaApprove: getSwappingMessage(),
+ nablaSwap: getSwappingMessage(),
+ subsidizePreSwap: getSwappingMessage(),
+ subsidizePostSwap: getSwappingMessage(),
+ moonbeamToPendulum: getMoonbeamToPendulumMessage(),
+ moonbeamToPendulumXcm: getMoonbeamToPendulumMessage(),
+ assethubToPendulum: t('pages.progress.assethubToPendulum', { assetSymbol: inputAssetSymbol }),
+ pendulumToMoonbeam: t('pages.progress.pendulumToMoonbeam', { assetSymbol: outputAssetSymbol }),
+ spacewalkRedeem: t('pages.progress.executeSpacewalkRedeem', { assetSymbol: outputAssetSymbol }),
+ brlaPayoutOnMoonbeam: getTransferringMessage(),
+ stellarPayment: t('pages.progress.stellarPayment', { assetSymbol: outputAssetSymbol }),
+ squidrouterApprove: getSquidrouterSwapMessage(),
+ squidrouterSwap: getSquidrouterSwapMessage(),
+ pendulumToAssethub: t('pages.progress.pendulumToAssethub', { assetSymbol: outputAssetSymbol }),
+ brlaTeleport: t('pages.progress.brlaTeleport'),
+ failed: '', // Not relevant for progress page
+ complete: '', // Not relevant for progress page
+ timedOut: '', // Not relevant for progress page
+ };
+
+ return messages[currentPhase];
}
From 5b4208695ed29f94850a2b1ab470e782dda08829 Mon Sep 17 00:00:00 2001
From: Gianfranco
Date: Mon, 14 Apr 2025 16:17:04 -0300
Subject: [PATCH 13/52] re introduce rounding for offramp case
---
api/src/api/helpers/quote.ts | 4 ++--
api/src/api/services/ramp/quote.service.ts | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/api/src/api/helpers/quote.ts b/api/src/api/helpers/quote.ts
index cc28e89d1..7d3890115 100644
--- a/api/src/api/helpers/quote.ts
+++ b/api/src/api/helpers/quote.ts
@@ -8,11 +8,11 @@ export function calculateTotalReceive(toAmount: Big, outputCurrency: RampCurrenc
const fixedFees = new Big(
outputTokenDetails.offrampFeesFixedComponent ? outputTokenDetails.offrampFeesFixedComponent : 0,
);
- const fees = toAmount.mul(feeBasisPoints).div(10000).add(fixedFees).round(6, 0);
+ const fees = toAmount.mul(feeBasisPoints).div(10000).add(fixedFees).round(2, 1);
const totalReceiveRaw = toAmount.minus(fees);
if (totalReceiveRaw.gt(0)) {
- return totalReceiveRaw.toFixed(6, 0);
+ return totalReceiveRaw.toFixed(2, 0);
}
return '0';
}
diff --git a/api/src/api/services/ramp/quote.service.ts b/api/src/api/services/ramp/quote.service.ts
index 82179e089..7c182be97 100644
--- a/api/src/api/services/ramp/quote.service.ts
+++ b/api/src/api/services/ramp/quote.service.ts
@@ -248,7 +248,7 @@ export class QuoteService extends BaseRampService {
const effectiveFeesOfframp = amountOut.preciseQuotedAmountOut.preciseBigDecimal
.minus(outputAmountAfterFees)
- .toFixed(6, 0);
+ .toFixed(2, 0);
const effectiveFeesOnrampBrl = new Big(inputAmount).minus(inputAmountAfterFees);
const effectiveFeesOnramp = effectiveFeesOnrampBrl.mul(amountOut.effectiveExchangeRate).toFixed(6, 0);
From 25605b0bdca6ba8a4bfa283cae0718ba56dd41c3 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 21:30:13 +0200
Subject: [PATCH 14/52] Translate validation errors
---
.../BrlaComponents/BrlaSwapFields/index.tsx | 17 +--
frontend/src/hooks/ramp/useRampValidation.ts | 105 +++++++++++-------
frontend/src/translations/en.json | 10 +-
frontend/src/translations/pt.json | 10 +-
4 files changed, 87 insertions(+), 55 deletions(-)
diff --git a/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx b/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
index ca18ad174..e1381703a 100644
--- a/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
+++ b/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
@@ -20,9 +20,7 @@ const OFFRAMP_FIELDS = [
{ id: StandardBrlaFieldOptions.PIX_ID, label: 'PIX', index: 1 },
];
-const ONRAMP_FIELDS = [
- { id: StandardBrlaFieldOptions.TAX_ID, label: 'CPF', index: 0 }
-]
+const ONRAMP_FIELDS = [{ id: StandardBrlaFieldOptions.TAX_ID, label: 'CPF', index: 0 }];
/**
* BrlaSwapFields component
@@ -37,9 +35,10 @@ export const BrlaSwapFields: FC = () => {
const fiatToken = useFiatToken();
- const rampDirection = useRampDirection();
+ const rampDirection = useRampDirection();
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
- const FIELDS = rampDirection === RampDirection.OFFRAMP ? OFFRAMP_FIELDS : ONRAMP_FIELDS;
+ const FIELDS = isOnramp ? ONRAMP_FIELDS : OFFRAMP_FIELDS;
return (
@@ -56,9 +55,11 @@ export const BrlaSwapFields: FC = () => {
/>
))}
-
- CPF must belong to you.
-
+ {!isOnramp && (
+
+ CPF must belong to you.
+
+ )}
)}
diff --git a/frontend/src/hooks/ramp/useRampValidation.ts b/frontend/src/hooks/ramp/useRampValidation.ts
index 7e2233bd2..7672bd502 100644
--- a/frontend/src/hooks/ramp/useRampValidation.ts
+++ b/frontend/src/hooks/ramp/useRampValidation.ts
@@ -18,16 +18,21 @@ import { config } from '../../config';
import { useRampDirection } from '../../stores/rampDirectionStore';
import { RampDirection } from '../../components/RampToggle';
import { useVortexAccount } from '../useVortexAccount';
+import { useTranslation } from 'react-i18next';
+import { TFunction } from 'i18next';
-function validateOnramp({
- inputAmount,
- fromToken,
- trackEvent,
-}: {
- inputAmount: Big;
- fromToken: FiatTokenDetails;
- trackEvent: (event: TrackableEvent) => void;
-}): string | null {
+function validateOnramp(
+ t: TFunction<'translation', undefined>,
+ {
+ inputAmount,
+ fromToken,
+ trackEvent,
+ }: {
+ inputAmount: Big;
+ fromToken: FiatTokenDetails;
+ trackEvent: (event: TrackableEvent) => void;
+ },
+): string | null {
const maxAmountUnits = multiplyByPowerOfTen(Big(fromToken.maxWithdrawalAmountRaw), -fromToken.decimals);
const minAmountUnits = multiplyByPowerOfTen(Big(fromToken.minWithdrawalAmountRaw), -fromToken.decimals);
@@ -37,9 +42,10 @@ function validateOnramp({
error_message: 'more_than_maximum_withdrawal',
input_amount: inputAmount ? inputAmount.toString() : '0',
});
- return `Maximum onramp amount is ${stringifyBigWithSignificantDecimals(maxAmountUnits, 2)} ${
- fromToken.fiat.symbol
- }.`;
+ return t('pages.swap.error.moreThanMaximumWithdrawal.buy', {
+ maxAmountUnits: stringifyBigWithSignificantDecimals(maxAmountUnits, 2),
+ assetSymbol: fromToken.fiat.symbol,
+ });
}
if (inputAmount && !inputAmount.eq(0) && minAmountUnits.gt(inputAmount)) {
@@ -48,29 +54,33 @@ function validateOnramp({
error_message: 'less_than_minimum_withdrawal',
input_amount: inputAmount ? inputAmount.toString() : '0',
});
- return `Minimum onramp amount is ${stringifyBigWithSignificantDecimals(minAmountUnits, 2)} ${
- fromToken.fiat.symbol
- }.`;
+ return t('pages.swap.error.lessThanMinimumWithdrawal.buy', {
+ minAmountUnits: stringifyBigWithSignificantDecimals(minAmountUnits, 2),
+ assetSymbol: fromToken.fiat.symbol,
+ });
}
return null;
}
-function validateOfframp({
- inputAmount,
- fromToken,
- toToken,
- quote,
- userInputTokenBalance,
- trackEvent,
-}: {
- inputAmount: Big;
- fromToken: OnChainTokenDetails;
- toToken: FiatTokenDetails;
- quote: QuoteEndpoints.QuoteResponse;
- userInputTokenBalance: string | null;
- trackEvent: (event: TrackableEvent) => void;
-}): string | null {
+function validateOfframp(
+ t: TFunction<'translation', undefined>,
+ {
+ inputAmount,
+ fromToken,
+ toToken,
+ quote,
+ userInputTokenBalance,
+ trackEvent,
+ }: {
+ inputAmount: Big;
+ fromToken: OnChainTokenDetails;
+ toToken: FiatTokenDetails;
+ quote: QuoteEndpoints.QuoteResponse;
+ userInputTokenBalance: string | null;
+ trackEvent: (event: TrackableEvent) => void;
+ },
+): string | null {
if (typeof userInputTokenBalance === 'string') {
if (Big(userInputTokenBalance).lt(inputAmount ?? 0)) {
trackEvent({
@@ -78,7 +88,10 @@ function validateOfframp({
error_message: 'insufficient_balance',
input_amount: inputAmount ? inputAmount.toString() : '0',
});
- return `Insufficient balance. Your balance is ${userInputTokenBalance} ${fromToken?.assetSymbol}.`;
+ return t('pages.swap.error.insufficientFunds', {
+ userInputTokenBalance,
+ assetSymbol: fromToken?.assetSymbol,
+ });
}
}
@@ -92,9 +105,10 @@ function validateOfframp({
error_message: 'more_than_maximum_withdrawal',
input_amount: inputAmount ? inputAmount.toString() : '0',
});
- return `Maximum withdrawal amount is ${stringifyBigWithSignificantDecimals(maxAmountUnits, 2)} ${
- toToken.fiat.symbol
- }.`;
+ return t('pages.swap.error.moreThanMinimumWithdrawal.sell', {
+ minAmountUnits: stringifyBigWithSignificantDecimals(minAmountUnits, 2),
+ assetSymbol: toToken.fiat.symbol,
+ });
}
const amountOut = quote ? Big(quote.outputAmount) : Big(0);
@@ -106,9 +120,11 @@ function validateOfframp({
error_message: 'less_than_minimum_withdrawal',
input_amount: inputAmount ? inputAmount.toString() : '0',
});
- return `Minimum withdrawal amount is ${stringifyBigWithSignificantDecimals(minAmountUnits, 2)} ${
- toToken.fiat.symbol
- }.`;
+
+ return t('pages.swap.error.lessThanMinimumWithdrawal.sell', {
+ minAmountUnits: stringifyBigWithSignificantDecimals(minAmountUnits, 2),
+ assetSymbol: toToken.fiat.symbol,
+ });
}
}
@@ -116,6 +132,8 @@ function validateOfframp({
}
export const useRampValidation = () => {
+ const { t } = useTranslation();
+
const { inputAmount: inputAmountString, onChainToken, fiatToken } = useRampFormStore();
const { quote, loading: quoteLoading } = useQuoteStore();
const { selectedNetwork } = useNetwork();
@@ -145,13 +163,13 @@ export const useRampValidation = () => {
let validationError = null;
if (isOnramp) {
- validationError = validateOnramp({
+ validationError = validateOnramp(t, {
inputAmount,
fromToken: fromToken as FiatTokenDetails,
trackEvent,
});
} else {
- validationError = validateOfframp({
+ validationError = validateOfframp(t, {
inputAmount,
fromToken: fromToken as OnChainTokenDetails,
toToken: toToken as FiatTokenDetails,
@@ -166,15 +184,16 @@ export const useRampValidation = () => {
return null;
}, [
+ isDisconnected,
isOnramp,
+ quoteLoading,
+ t,
inputAmount,
fromToken,
+ trackEvent,
toToken,
quote,
- userInputTokenBalance,
- quoteLoading,
- trackEvent,
- isDisconnected,
+ userInputTokenBalance?.balance,
]);
const setInitializeFailed = useCallback((message?: string | null) => {
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 9633282de..63505b75c 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -56,8 +56,14 @@
"noApi": "No API found."
},
"insufficientFunds": "Exceeds balance. Your balance is {{userInputTokenBalance}} {{assetSymbol}}",
- "moreThanMaximumWithdrawal": "Maximum withdrawal amount is {{maxAmountUnits}} {{assetSymbol}}.",
- "lessThanMinimumWithdrawal": "Minimum withdrawal amount is {{minAmountUnits}} {{assetSymbol}}.",
+ "moreThanMaximumWithdrawal": {
+ "buy": "Maximum buy amount is {{maxAmountUnits}} {{assetSymbol}}.",
+ "sell": "Maximum sell amount is {{maxAmountUnits}} {{assetSymbol}}."
+ },
+ "lessThanMinimumWithdrawal": {
+ "buy": "Minimum buy amount is {{minAmountUnits}} {{assetSymbol}}.",
+ "sell": "Minimum sell amount is {{minAmountUnits}} {{assetSymbol}}."
+ },
"insufficientLiquidity": "The amount is temporarily not available. Please, try with a smaller amount."
},
"developedBy": "Developed by"
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index 101019d4d..aaf7a48ec 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -56,8 +56,14 @@
"noApi": "Nenhuma API encontrada."
},
"insufficientFunds": "Saldo insuficiente. Seu saldo é {{userInputTokenBalance}} {{assetSymbol}}",
- "moreThanMaximumWithdrawal": "O valor máximo de saque é {{maxAmountUnits}} {{assetSymbol}}.",
- "lessThanMinimumWithdrawal": "O valor mínimo de saque é {{minAmountUnits}} {{assetSymbol}}.",
+ "moreThanMaximumWithdrawal": {
+ "sell": "O valor máximo de compra é {{maxAmountUnits}} {{assetSymbol}}.",
+ "sell": "O valor máximo de venda é {{maxAmountUnits}} {{assetSymbol}}."
+ },
+ "lessThanMinimumWithdrawal": {
+ "buy": "O valor mínimo de compra é {{minAmountUnits}} {{assetSymbol}}.",
+ "sell": "O valor mínimo de venda é {{minAmountUnits}} {{assetSymbol}}."
+ },
"insufficientLiquidity": "O valor está temporariamente indisponível. Por favor, tente com um valor menor."
},
"developedBy": "Desenvolvido por"
From 68cbe0a74b3d10818ddbb0b6f471985a5cd1941d Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 21:47:29 +0200
Subject: [PATCH 15/52] Fix wrong condition in initial-phase-handler.ts
---
api/src/api/services/phases/handlers/initial-phase-handler.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/api/services/phases/handlers/initial-phase-handler.ts b/api/src/api/services/phases/handlers/initial-phase-handler.ts
index 24a571254..619e738a3 100644
--- a/api/src/api/services/phases/handlers/initial-phase-handler.ts
+++ b/api/src/api/services/phases/handlers/initial-phase-handler.ts
@@ -27,7 +27,7 @@ export class InitialPhaseHandler extends BasePhaseHandler {
logger.info(`Executing initial phase for ramp ${state.id}`);
// Check if signed_transactions are present for offramps. If they are not, return early.
- if (state.type === 'off' && (state.presignedTxs === null || state.presignedTxs.length > 0)) {
+ if (state.type === 'off' && (state.presignedTxs === null || state.presignedTxs.length === 0)) {
throw new Error('InitialPhaseHandler: No signed transactions found. Cannot proceed.');
}
From bc332f8927a78e0c822ffc89b594a07dfdd7b92b Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 21:47:42 +0200
Subject: [PATCH 16/52] Fix missing import in offrampTransactions.ts
---
.../api/services/transactions/offrampTransactions.ts | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/api/src/api/services/transactions/offrampTransactions.ts b/api/src/api/services/transactions/offrampTransactions.ts
index 97979e89d..dae0286f0 100644
--- a/api/src/api/services/transactions/offrampTransactions.ts
+++ b/api/src/api/services/transactions/offrampTransactions.ts
@@ -1,5 +1,8 @@
import {
+ AccountMeta,
+ addAdditionalTransactionsToMeta,
AMM_MINIMUM_OUTPUT_SOFT_MARGIN,
+ encodeSubmittableExtrinsic,
FiatToken,
getAnyFiatTokenDetails,
getNetworkFromDestination,
@@ -11,11 +14,9 @@ import {
isOnChainToken,
isStellarOutputTokenDetails,
Networks,
- AccountMeta,
- encodeSubmittableExtrinsic,
- addAdditionalTransactionsToMeta,
+ PaymentData,
+ UnsignedTx,
} from 'shared';
-import { UnsignedTx, PaymentData } from 'shared';
import Big from 'big.js';
import { Keypair } from 'stellar-sdk';
@@ -30,6 +31,7 @@ import { createPendulumToMoonbeamTransfer } from './xcm/pendulumToMoonbeam';
import { StateMetadata } from '../phases/meta-state-types';
import { preparePendulumCleanupTransaction } from './pendulum/cleanup';
import { createAssethubToPendulumXCM } from './xcm/assethubToPendulum';
+import logger from '../../../config/logger';
interface OfframpTransactionParams {
quote: QuoteTicketAttributes;
From b7fb27b70a0eaa6c6879f2ada3de08bd7ecc9882 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Mon, 14 Apr 2025 21:48:01 +0200
Subject: [PATCH 17/52] Fix wrong import in cleanup.worker.test.ts
---
api/src/api/workers/cleanup.worker.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/api/workers/cleanup.worker.test.ts b/api/src/api/workers/cleanup.worker.test.ts
index 06f0b8e36..5decb31b0 100644
--- a/api/src/api/workers/cleanup.worker.test.ts
+++ b/api/src/api/workers/cleanup.worker.test.ts
@@ -1,6 +1,6 @@
// eslint-disable-next-line import/no-unresolved
import { describe, expect, it, mock, beforeEach } from 'bun:test';
-import { CleanupWorker } from './cleanup.worker';
+import CleanupWorker from './cleanup.worker';
import RampState from '../../models/rampState.model';
mock.module('../../config/logger', () => ({
From 80f31eb2f36bddc98b9cc85c73c2fd33af644d1f Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 10:28:40 +0200
Subject: [PATCH 18/52] Add logs for debugging
---
.../offramp/useRampService/useRegisterRamp.ts | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
index bb06aa96e..5fd5d29b9 100644
--- a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
@@ -96,6 +96,8 @@ export const useRegisterRamp = () => {
setCanRegisterRamp(true);
};
+ console.log("canRegisterRamp", canRegisterRamp)
+
const { checkLock, verifyLock, releaseLock } = useProcessLock(REGISTER_KEY_LOCAL_STORAGE);
// @TODO: maybe change to useCallback
@@ -105,33 +107,46 @@ export const useRegisterRamp = () => {
// Check if we can proceed with the registration process
const lockResult = checkLock();
if (!lockResult.canProceed) {
+ console.log("Cannot proceed with ramp registration, lock already exists");
return;
}
const { processRef } = lockResult;
+ console.log("processRef", processRef);
const registerRampProcess = async () => {
// Verify we still own the lock before proceeding
if (!verifyLock(processRef)) {
+ console.log("In registerRampProcess, lock is not valid anymore for processRef", processRef);
return;
}
+ console.log("after verifyLock check")
+
if (!executionInput) {
throw new Error('Missing execution input');
}
+ console.log("after executionInput check")
+
if (!chainId) {
throw new Error('Missing chainId');
}
+ console.log("after chainId check")
+
if (!pendulumApiComponents?.api) {
throw new Error('Missing pendulumApiComponents');
}
+ console.log("after pendulumApiComponents check")
+
if (!moonbeamApiComponents?.api) {
throw new Error('Missing moonbeamApiComponents');
}
+ console.log("after checks")
+
const quoteId = executionInput.quote.id;
const signingAccounts: AccountMeta[] = [
{ address: executionInput.ephemerals.stellarEphemeral.address, network: Networks.Stellar },
From e09ea9e7cc0af7e943c6a5b74499313331404b88 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 11:08:33 +0200
Subject: [PATCH 19/52] add BRLA disclaimer for Onramp
---
.../components/BrlaComponents/BrlaSwapFields/index.tsx | 8 ++++++--
frontend/src/translations/en.json | 3 ++-
frontend/src/translations/pt.json | 3 ++-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx b/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
index e1381703a..5c79bb16e 100644
--- a/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
+++ b/frontend/src/components/BrlaComponents/BrlaSwapFields/index.tsx
@@ -55,10 +55,14 @@ export const BrlaSwapFields: FC = () => {
/>
))}
- {!isOnramp && (
-
+ {isOnramp ? (
+
CPF must belong to you.
+ ) : (
+
+ CPF and Pix key need to belong to the same person.
+
)}
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 63505b75c..9b2347378 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -269,7 +269,8 @@
},
"brlaSwapField": {
"placeholder": "Enter your {{label}}",
- "disclaimer": "CPF and Pix key need to belong to the <1>same person1>."
+ "disclaimerOfframp": "CPF and Pix key need to belong to the <1>same person1>.",
+ "disclaimerOnramp": "CPF must belong to <1>you1>."
},
"swapSubmitButton": {
"confirming": "Confirming",
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index aaf7a48ec..ea583df0e 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -268,7 +268,8 @@
},
"brlaSwapField": {
"placeholder": "Digite seu {{label}}",
- "disclaimer": "CPF e chave Pix precisam pertencer à <1>mesma pessoa1>."
+ "disclaimerOfframp": "CPF e chave Pix precisam pertencer à <1>mesma pessoa1>.",
+ "disclaimerOnramp": "CPF deve pertencer a <1>você1>."
},
"swapSubmitButton": {
"confirming": "Confirmando",
From b02f42f691804618f65d9511e78bfcadfbca2d8e Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:16:50 +0200
Subject: [PATCH 20/52] Reset ramp State when closing summary
---
frontend/src/components/OfframpSummaryDialog/index.tsx | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index 49f13867d..aa458281f 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -257,8 +257,7 @@ export const OfframpSummaryDialog: FC = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
const { selectedNetwork } = useNetwork();
- const { setRampExecutionInput, setRampInitiating, setRampStarted, setRampSummaryVisible, setRampPaymentConfirmed } =
- useRampActions();
+ const { resetRampState, setRampPaymentConfirmed } = useRampActions();
const rampState = useRampState();
const executionInput = useRampExecutionInput();
const visible = useRampSummaryVisible();
@@ -294,11 +293,7 @@ export const OfframpSummaryDialog: FC = () => {
: getAnyFiatTokenDetails(fiatToken);
const onClose = () => {
- setIsSubmitted(false);
- setRampExecutionInput(undefined);
- setRampStarted(false);
- setRampInitiating(false);
- setRampSummaryVisible(false);
+ resetRampState();
};
const onSubmit = () => {
From 66fdafc47200cdf99134cf352f043e3bf66576da Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:17:39 +0200
Subject: [PATCH 21/52] Add more logs
---
.../offramp/useRampService/useRegisterRamp.ts | 38 +++++++++++++------
.../offramp/useRampService/useStartRamp.ts | 1 +
.../src/hooks/offramp/useSubmitOfframp.ts | 3 +-
3 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
index 5fd5d29b9..c23cb2a0e 100644
--- a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
@@ -82,6 +82,8 @@ export const useRegisterRamp = () => {
// TODO if user declined signing, do something
const [userDeclinedSigning, setUserDeclinedSigning] = useState(false);
+ // This should be called for onramps, when the user opens the summary dialog, and for offramps, when the user
+ // clicks on the Continue button in the form (BRL) or comes back from the anchor page.
const registerRamp = async (executionInput: RampExecutionInput) => {
prepareOfframpSubmission(executionInput);
@@ -90,13 +92,16 @@ export const useRegisterRamp = () => {
if (executionInput.quote.rampType === 'off' && executionInput.fiatToken !== FiatToken.BRL) {
console.log('Registering ramp for Stellar offramps');
await handleOnAnchorWindowOpen();
+ } else if (executionInput.quote.rampType === 'off' && executionInput.fiatToken === FiatToken.BRL) {
+ // TODO Implement waiting for user input (the ramp summary dialog should show the 'Confirm' button and once clicked,
+ // setCanRegisterRamp to true
}
// For other ramps, we can continue registering right away
setCanRegisterRamp(true);
};
- console.log("canRegisterRamp", canRegisterRamp)
+ console.log('canRegisterRamp', canRegisterRamp);
const { checkLock, verifyLock, releaseLock } = useProcessLock(REGISTER_KEY_LOCAL_STORAGE);
@@ -107,45 +112,45 @@ export const useRegisterRamp = () => {
// Check if we can proceed with the registration process
const lockResult = checkLock();
if (!lockResult.canProceed) {
- console.log("Cannot proceed with ramp registration, lock already exists");
+ console.log('Cannot proceed with ramp registration, lock already exists');
return;
}
const { processRef } = lockResult;
- console.log("processRef", processRef);
+ console.log('processRef', processRef);
const registerRampProcess = async () => {
// Verify we still own the lock before proceeding
if (!verifyLock(processRef)) {
- console.log("In registerRampProcess, lock is not valid anymore for processRef", processRef);
+ console.log('In registerRampProcess, lock is not valid anymore for processRef', processRef);
return;
}
- console.log("after verifyLock check")
+ console.log('after verifyLock check');
if (!executionInput) {
throw new Error('Missing execution input');
}
- console.log("after executionInput check")
+ console.log('after executionInput check');
if (!chainId) {
throw new Error('Missing chainId');
}
- console.log("after chainId check")
+ console.log('after chainId check');
if (!pendulumApiComponents?.api) {
throw new Error('Missing pendulumApiComponents');
}
- console.log("after pendulumApiComponents check")
+ console.log('after pendulumApiComponents check');
if (!moonbeamApiComponents?.api) {
throw new Error('Missing moonbeamApiComponents');
}
- console.log("after checks")
+ console.log('after checks');
const quoteId = executionInput.quote.id;
const signingAccounts: AccountMeta[] = [
@@ -199,9 +204,9 @@ export const useRegisterRamp = () => {
moonbeamApiComponents.api,
);
- console.log("setRampRegistered(true)")
+ console.log('setRampRegistered(true)');
setRampRegistered(true);
- console.log("setRampState to ", {
+ console.log('setRampState to ', {
quote: executionInput.quote,
ramp: rampProcess,
signedTransactions,
@@ -211,7 +216,7 @@ export const useRegisterRamp = () => {
squidRouterSwapHash: undefined,
assetHubToPendulumHash: undefined,
},
- })
+ });
setRampState({
quote: executionInput.quote,
@@ -270,6 +275,15 @@ export const useRegisterRamp = () => {
requiredMetaIsEmpty && // User signing metadata hasn't been populated yet
chainId !== undefined; // Chain ID is available
+ console.log(
+ 'shouldRequestSignatures',
+ shouldRequestSignatures,
+ 'rampStarted',
+ rampStarted,
+ 'requiredMetaIsEmpty',
+ requiredMetaIsEmpty,
+ );
+
if (!rampState || rampState?.ramp?.type === 'on' || !shouldRequestSignatures || userDeclinedSigning) {
return; // Exit early if conditions aren't met
}
diff --git a/frontend/src/hooks/offramp/useRampService/useStartRamp.ts b/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
index 4e028f1d9..7f5060cd1 100644
--- a/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
@@ -48,6 +48,7 @@ export const useStartRamp = () => {
})
.catch((err) => {
console.error('Error starting ramp:', err);
+ // TODO this can fail if the ramp 'expired'. We should handle this case and show a message to the user
});
}, [rampPaymentConfirmed, rampStarted, rampState, setRampStarted]);
};
diff --git a/frontend/src/hooks/offramp/useSubmitOfframp.ts b/frontend/src/hooks/offramp/useSubmitOfframp.ts
index 53d0c49b1..41c4f9ae9 100644
--- a/frontend/src/hooks/offramp/useSubmitOfframp.ts
+++ b/frontend/src/hooks/offramp/useSubmitOfframp.ts
@@ -6,13 +6,12 @@ import { useVortexAccount } from '../useVortexAccount';
import { useNetwork } from '../../contexts/network';
import { useEventsContext } from '../../contexts/events';
import { useSiweContext } from '../../contexts/siwe';
-import { getOnChainTokenDetailsOrDefault, getAnyFiatTokenDetails, getTokenDetailsSpacewalk, FiatToken } from 'shared';
+import { FiatToken, getAnyFiatTokenDetails, getOnChainTokenDetailsOrDefault, getTokenDetailsSpacewalk } from 'shared';
import { fetchTomlValues } from '../../services/stellar';
import { sep24First } from '../../services/anchor/sep24/first';
import { sep10 } from '../../services/anchor/sep10';
import { useRampActions } from '../../stores/offrampStore';
import { useSep24Actions } from '../../stores/sep24Store';
-import { usePendulumNode } from '../../contexts/polkadotNode';
import { SIGNING_SERVICE_URL } from '../../constants/constants';
import { RampExecutionInput } from '../../types/phases';
import { useToastMessage } from '../../helpers/notifications';
From d0fdd23c662e3e9d122f6aabff45b4e613bc1563 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:17:59 +0200
Subject: [PATCH 22/52] Move throwing of API errors out of try-catch
---
api/src/api/services/ramp/quote.service.ts | 79 ++++++++++++----------
1 file changed, 43 insertions(+), 36 deletions(-)
diff --git a/api/src/api/services/ramp/quote.service.ts b/api/src/api/services/ramp/quote.service.ts
index 7c182be97..6af68f45d 100644
--- a/api/src/api/services/ramp/quote.service.ts
+++ b/api/src/api/services/ramp/quote.service.ts
@@ -164,43 +164,58 @@ export class QuoteService extends BaseRampService {
rampType: 'on' | 'off',
from: DestinationType,
to: DestinationType,
- ): Promise<{ receiveAmount: string; fees: string; outputAmountBeforeFees: string; outputAmountMoonbeamRaw: string, inputAmountAfterFees: string }> {
+ ): Promise<{
+ receiveAmount: string;
+ fees: string;
+ outputAmountBeforeFees: string;
+ outputAmountMoonbeamRaw: string;
+ inputAmountAfterFees: string;
+ }> {
const apiManager = ApiManager.getInstance();
const networkName = 'pendulum';
const apiInstance = await apiManager.getApi(networkName);
- try {
- const fromNetwork = getNetworkFromDestination(from);
- const toNetwork = getNetworkFromDestination(to);
- if (rampType === 'on' && !toNetwork) {
- throw new APIError({
- status: httpStatus.BAD_REQUEST,
- message: 'Invalid toNetwork for onramp.',
- });
- }
- if (rampType === 'off' && !fromNetwork) {
+ const fromNetwork = getNetworkFromDestination(from);
+ const toNetwork = getNetworkFromDestination(to);
+ if (rampType === 'on' && !toNetwork) {
+ throw new APIError({
+ status: httpStatus.BAD_REQUEST,
+ message: 'Invalid toNetwork for onramp.',
+ });
+ }
+ if (rampType === 'off' && !fromNetwork) {
+ throw new APIError({
+ status: httpStatus.BAD_REQUEST,
+ message: 'Invalid fromNetwork for offramp.',
+ });
+ }
+ const outTokenDetails = toNetwork ? getOnChainTokenDetails(toNetwork, outputCurrency as OnChainToken) : undefined;
+ if (rampType === 'on') {
+ if (!outTokenDetails || !isEvmTokenDetails(outTokenDetails)) {
throw new APIError({
status: httpStatus.BAD_REQUEST,
- message: 'Invalid fromNetwork for offramp.',
+ message: 'Invalid token details for onramp',
});
}
+ }
+ if (Big(inputAmount).lte(0)) {
+ throw new APIError({
+ status: httpStatus.BAD_REQUEST,
+ message: 'Invalid input amount',
+ });
+ }
+
+ try {
const inputTokenPendulumDetails =
rampType === 'on' ? getPendulumDetails(inputCurrency) : getPendulumDetails(inputCurrency, fromNetwork);
const outputTokenPendulumDetails =
rampType === 'on' ? getPendulumDetails(outputCurrency, toNetwork) : getPendulumDetails(outputCurrency);
- if (Big(inputAmount).lte(0)) {
- throw new APIError({
- status: httpStatus.BAD_REQUEST,
- message: 'Invalid input amount',
- });
- }
-
const inputAmountAfterFees =
rampType === 'on' ? calculateTotalReceiveOnramp(new Big(inputAmount), inputCurrency) : inputAmount;
- let amountOut = await getTokenOutAmount({
+ const amountOut = await getTokenOutAmount({
api: apiInstance.api,
fromAmountString: inputAmountAfterFees,
inputTokenDetails: inputTokenPendulumDetails,
@@ -210,35 +225,27 @@ export class QuoteService extends BaseRampService {
// if onramp, adjust for axlUSDC price difference.
const outputAmountMoonbeamRaw: string = amountOut.preciseQuotedAmountOut.rawBalance.toFixed(); // Store the value before the adjustment.
if (rampType === 'on') {
- const outTokenDetails = getOnChainTokenDetails(getNetworkFromDestination(to)!, outputCurrency as OnChainToken);
- if (!outTokenDetails || !isEvmTokenDetails(outTokenDetails)) {
- throw new APIError({
- status: httpStatus.BAD_REQUEST,
- message: 'Invalid token details for onramp',
- });
- }
-
const routeParams = createOnrampRouteParams(
'0x30a300612ab372cc73e53ffe87fb73d62ed68da3', // It does not matter.
amountOut.preciseQuotedAmountOut.rawBalance.toFixed(),
- outTokenDetails,
+ outTokenDetails!,
getNetworkFromDestination(to)!,
'0x30a300612ab372cc73e53ffe87fb73d62ed68da3',
);
const routeResult = await getRoute(routeParams);
const { route } = routeResult.data;
- const toAmountMin = route.estimate.toAmountMin;
+ const { toAmountMin } = route.estimate;
amountOut.preciseQuotedAmountOut = parseContractBalanceResponse(
- outTokenDetails.pendulumDecimals,
+ outTokenDetails!.pendulumDecimals,
BigInt(toAmountMin),
);
- (amountOut.roundedDownQuotedAmountOut = amountOut.preciseQuotedAmountOut.preciseBigDecimal.round(2, 0)),
- (amountOut.effectiveExchangeRate = stringifyBigWithSignificantDecimals(
- amountOut.preciseQuotedAmountOut.preciseBigDecimal.div(new Big(inputAmountAfterFees)),
- 4,
- ));
+ amountOut.roundedDownQuotedAmountOut = amountOut.preciseQuotedAmountOut.preciseBigDecimal.round(2, 0);
+ amountOut.effectiveExchangeRate = stringifyBigWithSignificantDecimals(
+ amountOut.preciseQuotedAmountOut.preciseBigDecimal.div(new Big(inputAmountAfterFees)),
+ 4,
+ );
}
const outputAmountAfterFees =
From 27fc64e153d218840938f8aeb764ff7b5bac9f3b Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:22:27 +0200
Subject: [PATCH 23/52] Set quote and output amount to undefined if quote
fetching failed
---
frontend/src/stores/ramp/useQuoteStore.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/stores/ramp/useQuoteStore.ts b/frontend/src/stores/ramp/useQuoteStore.ts
index 5bad6434d..638f9eefd 100644
--- a/frontend/src/stores/ramp/useQuoteStore.ts
+++ b/frontend/src/stores/ramp/useQuoteStore.ts
@@ -132,7 +132,7 @@ export const useQuoteStore = create((set) => ({
} catch (error) {
console.error('Error fetching quote:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to get quote';
- set({ error: errorMessage, loading: false });
+ set({ error: errorMessage, loading: false, quote: undefined, outputAmount: undefined });
}
},
From bb95c3b412998b9660bb9f3683ebad95aef2599e Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:26:39 +0200
Subject: [PATCH 24/52] Show error when trying to onramp from AssetHub
---
frontend/src/hooks/ramp/useRampValidation.ts | 6 +++++-
frontend/src/translations/en.json | 4 +++-
frontend/src/translations/pt.json | 4 +++-
3 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/frontend/src/hooks/ramp/useRampValidation.ts b/frontend/src/hooks/ramp/useRampValidation.ts
index 7672bd502..6bea384c4 100644
--- a/frontend/src/hooks/ramp/useRampValidation.ts
+++ b/frontend/src/hooks/ramp/useRampValidation.ts
@@ -4,6 +4,7 @@ import {
FiatTokenDetails,
getAnyFiatTokenDetails,
getOnChainTokenDetailsOrDefault,
+ Networks,
OnChainTokenDetails,
QuoteEndpoints,
} from 'shared';
@@ -180,13 +181,16 @@ export const useRampValidation = () => {
}
if (validationError) return validationError;
- if (quoteLoading) return 'Calculating quote...';
+ if (quoteLoading) return t('components.swap.validation.calculatingQuote')
+ if (isOnramp && selectedNetwork === Networks.AssetHub)
+ return t('components.swap.validation.assetHubNotSupported');
return null;
}, [
isDisconnected,
isOnramp,
quoteLoading,
+ selectedNetwork,
t,
inputAmount,
fromToken,
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 9b2347378..c739e09c7 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -260,7 +260,9 @@
"pixId": {
"required": "PIX key is required when transferring BRL",
"format": "PIX key does not match any of the valid formats"
- }
+ },
+ "calculatingQuote": "Calculating quote...",
+ "assetHubNotSupported": "Please select a different network. Buy is currently not available on AssetHub."
}
},
"benefitsList": {
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index ea583df0e..0c8890945 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -259,7 +259,9 @@
"pixId": {
"required": "Chave PIX é obrigatória para transferências em BRL",
"format": "A chave PIX não corresponde a nenhum dos formatos válidos"
- }
+ },
+ "calculatingQuote": "Calculando cotação...",
+ "assetHubNotSupported": "Por favor, selecione uma rede diferente. A compra não está disponível no momento no AssetHub."
}
},
"benefitsList": {
From 4391a0105049776eae66a1f5b9bda455d3609656 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:32:29 +0200
Subject: [PATCH 25/52] Await crypto initialization in shared
---
shared/src/helpers/signUnsigned.ts | 101 +++++++++++++++--------------
1 file changed, 53 insertions(+), 48 deletions(-)
diff --git a/shared/src/helpers/signUnsigned.ts b/shared/src/helpers/signUnsigned.ts
index 1bcfaca39..e3c3a380e 100644
--- a/shared/src/helpers/signUnsigned.ts
+++ b/shared/src/helpers/signUnsigned.ts
@@ -1,9 +1,9 @@
-import { createWalletClient, http } from "viem";
-import { privateKeyToAccount } from "viem/accounts";
-import { Keyring } from "@polkadot/api";
-import { Transaction, Keypair, Networks as StellarNetworks } from "stellar-sdk";
-import { ApiPromise } from "@polkadot/api";
-import { moonbeam } from "viem/chains";
+import {createWalletClient, http} from "viem";
+import {privateKeyToAccount} from "viem/accounts";
+import {Keyring} from "@polkadot/api";
+import {Transaction, Keypair, Networks as StellarNetworks} from "stellar-sdk";
+import {ApiPromise} from "@polkadot/api";
+import {moonbeam} from "viem/chains";
import {
isEvmTransactionData,
PresignedTx,
@@ -11,8 +11,9 @@ import {
EphemeralAccount,
decodeSubmittableExtrinsic,
} from "../index";
-import { u8aToHex } from "@polkadot/util";
-import { hdEthereum, mnemonicToLegacySeed } from "@polkadot/util-crypto";
+import {u8aToHex} from "@polkadot/util";
+import {hdEthereum, mnemonicToLegacySeed} from "@polkadot/util-crypto";
+import {cryptoWaitReady} from '@polkadot/util-crypto';
// Number of transactions to pre-sign for each transaction
const NUMBER_OF_PRESIGNED_TXS = 3;
@@ -24,9 +25,9 @@ export function addAdditionalTransactionsToMeta(
if (multiSignedTxs.length <= 1) {
return primaryTx;
}
-
+
const additionalTxs: Record = {};
-
+
for (let i = 1; i < multiSignedTxs.length; i++) {
const additionalTx = multiSignedTxs[i];
const nonceOffset = i;
@@ -34,16 +35,16 @@ export function addAdditionalTransactionsToMeta(
const txName = `${primaryTx.phase}${nonceOffset}`;
additionalTxs[txName] = additionalTx;
}
-
+
return {
...primaryTx,
- meta: { ...primaryTx.meta, additionalTxs }
+ meta: {...primaryTx.meta, additionalTxs}
};
}
/**
* Signs multiple Stellar transactions with increasing sequence numbers
- *
+ *
* @param tx - The original backend-signed transaction. Can contain meta field with multiple-nonce transactions.
* @param keypair - The Stellar keypair to sign with
* @param networkPassphrase - The Stellar network passphrase
@@ -58,12 +59,12 @@ async function signMultipleStellarTransactions(
const transaction = new Transaction(tx.txData as string, networkPassphrase);
transaction.sign(keypair);
-
+
const primarySignedTxData = transaction
.toEnvelope()
.toXDR()
.toString("base64");
-
+
const signedTx: PresignedTx = {
...tx,
txData: primarySignedTxData
@@ -84,14 +85,15 @@ async function signMultipleStellarTransactions(
.toString("base64");
signedTx.meta.additionalTxs[key].txData = extraTransactionSigned;
- };
-
+ }
+ ;
+
return signedTx;
}
/**
* Signs multiple Substrate (Pendulum) transactions with increasing nonces
- *
+ *
* @param tx - The original unsigned transaction
* @param keypair - The keypair to sign with
* @param api - The Polkadot API instance
@@ -105,29 +107,29 @@ async function signMultipleSubstrateTransactions(
startingNonce: number
): Promise {
const signedTxs: PresignedTx[] = [];
-
+
for (let i = 0; i < NUMBER_OF_PRESIGNED_TXS; i++) {
const currentNonce = startingNonce + i;
const extrinsic = decodeSubmittableExtrinsic(tx.txData as string, api);
-
- await extrinsic.signAsync(keypair, { nonce: currentNonce, era: 0 });
-
+
+ await extrinsic.signAsync(keypair, {nonce: currentNonce, era: 0});
+
const signedTxData = extrinsic.toHex();
- const signedTx: PresignedTx = {
+ const signedTx: PresignedTx = {
...tx,
nonce: currentNonce,
txData: signedTxData
};
-
+
signedTxs.push(signedTx);
}
-
+
return signedTxs;
}
/**
* Signs multiple EVM (Moonbeam) transactions with increasing nonces
- *
+ *
* @param tx - The original unsigned transaction
* @param walletClient - The viem wallet client
* @param startingNonce - The starting nonce value
@@ -139,16 +141,16 @@ async function signMultipleEvmTransactions(
startingNonce: number
): Promise {
const signedTxs: PresignedTx[] = [];
-
+
if (!isEvmTransactionData(tx.txData)) {
throw new Error("Invalid EVM transaction data format");
}
for (let i = 0; i < NUMBER_OF_PRESIGNED_TXS; i++) {
const currentNonce = startingNonce + i;
-
+
// Ensure the transaction data is in the correct format
- const txData = {
+ const txData = {
to: tx.txData.to,
data: tx.txData.data,
value: BigInt(tx.txData.value),
@@ -157,18 +159,18 @@ async function signMultipleEvmTransactions(
maxFeePerGas: tx.txData.maxFeePerGas ? BigInt(tx.txData.maxFeePerGas) * 5n : BigInt(187500000000),
maxPriorityFeePerGas: tx.txData.maxPriorityFeePerGas ? BigInt(tx.txData.maxPriorityFeePerGas) * 5n : BigInt(187500000000),
};
-
+
const signedTxData = await walletClient.signTransaction(txData);
-
+
const signedTx: PresignedTx = {
...tx,
nonce: currentNonce,
txData: signedTxData
};
-
+
signedTxs.push(signedTx);
}
-
+
return signedTxs;
}
@@ -219,6 +221,9 @@ export async function signUnsignedTransactions(
pendulumApi: ApiPromise,
moonbeamApi: ApiPromise,
): Promise {
+ // Wait for initialization of crypto libraries
+ await cryptoWaitReady();
+
const signedTxs: PresignedTx[] = [];
try {
@@ -260,7 +265,7 @@ export async function signUnsignedTransactions(
throw new Error("Invalid Pendulum transaction data format");
}
- const keyring = new Keyring({ type: "sr25519" });
+ const keyring = new Keyring({type: "sr25519"});
const keypair = keyring.addFromUri(ephemerals.pendulumEphemeral.secret);
const multiSignedTxs = await signMultipleSubstrateTransactions(
@@ -269,11 +274,11 @@ export async function signUnsignedTransactions(
pendulumApi,
tx.nonce
);
-
+
const primaryTx = multiSignedTxs[0];
-
+
const txWithMeta = addAdditionalTransactionsToMeta(primaryTx, multiSignedTxs);
-
+
signedTxs.push(txWithMeta);
}
@@ -282,16 +287,16 @@ export async function signUnsignedTransactions(
if (!ephemerals.moonbeamEphemeral) {
throw new Error("Missing EVM ephemeral account");
}
-
+
const ethDerPath = `m/44'/60'/${0}'/${0}/${0}`;
-
+
if (isEvmTransactionData(tx.txData)) {
const privateKey = u8aToHex(
hdEthereum(mnemonicToLegacySeed(ephemerals.moonbeamEphemeral.secret, '', false, 64), ethDerPath)
.secretKey
);
const evmAccount = privateKeyToAccount(privateKey);
-
+
const walletClient = createWalletClient({
account: evmAccount,
chain: moonbeam,
@@ -303,27 +308,27 @@ export async function signUnsignedTransactions(
walletClient,
tx.nonce
);
-
+
const primaryTx = multiSignedTxs[0];
-
+
const txWithMeta = addAdditionalTransactionsToMeta(primaryTx, multiSignedTxs);
-
+
signedTxs.push(txWithMeta);
} else {
- const keyring = new Keyring({ type: 'ethereum' });
+ const keyring = new Keyring({type: 'ethereum'});
const keypair = keyring.addFromUri(`${ephemerals.moonbeamEphemeral.secret}/${ethDerPath}`);
-
+
const multiSignedTxs = await signMultipleSubstrateTransactions(
tx,
keypair,
moonbeamApi,
tx.nonce
);
-
+
const primaryTx = multiSignedTxs[0];
-
+
const txWithMeta = addAdditionalTransactionsToMeta(primaryTx, multiSignedTxs);
-
+
signedTxs.push(txWithMeta);
}
}
From 87583a5acb8f1858ffb5c9121b0d7a0dfe39b9a3 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 11:53:46 +0200
Subject: [PATCH 26/52] Make sure the quote is refetched when closing the
summary dialog
---
.../src/components/OfframpSummaryDialog/index.tsx | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index aa458281f..b2f66460b 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -31,6 +31,7 @@ import { useTranslation } from 'react-i18next';
import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
import { QRCodeSVG } from 'qrcode.react';
import { CopyButton } from '../CopyButton';
+import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
interface AssetDisplayProps {
amount: string;
@@ -268,6 +269,8 @@ export const OfframpSummaryDialog: FC = () => {
const fiatToken = useFiatToken();
const onChainToken = useOnChainToken();
+ const { quote, fetchQuote } = useQuoteStore();
+
const submitButtonDisabled = useMemo(() => {
if (!executionInput) return true;
@@ -294,6 +297,14 @@ export const OfframpSummaryDialog: FC = () => {
const onClose = () => {
resetRampState();
+ // Make sure a new quote is fetched immediately. The previous one was consumed when this dialog was opened
+ fetchQuote({
+ rampType: isOnramp ? 'on' : 'off',
+ inputAmount: Big(quote?.inputAmount || '0'),
+ onChainToken,
+ fiatToken,
+ selectedNetwork,
+ });
};
const onSubmit = () => {
From 79b020688af206051193d3a3758e3f28b2f313a6 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 12:51:25 +0200
Subject: [PATCH 27/52] Set expected time for brlaTeleport phase to 90 seconds
---
frontend/src/pages/progress/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/progress/index.tsx b/frontend/src/pages/progress/index.tsx
index 17d4908a5..944a3f182 100644
--- a/frontend/src/pages/progress/index.tsx
+++ b/frontend/src/pages/progress/index.tsx
@@ -67,7 +67,7 @@ const useProgressUpdate = (
export const ONRAMPING_PHASE_SECONDS: Record = {
initial: 0,
fundEphemeral: 20,
- brlaTeleport: 30,
+ brlaTeleport: 90,
moonbeamToPendulumXcm: 30,
subsidizePreSwap: 24,
nablaApprove: 24,
From 6f8c9b9357d9bfd72b98c0e0a0676bde411348b1 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 12:51:45 +0200
Subject: [PATCH 28/52] Refactor expiry in summary dialog
---
.../components/OfframpSummaryDialog/index.tsx | 112 ++++++++++++------
frontend/src/stores/ramp/useQuoteStore.ts | 1 +
2 files changed, 74 insertions(+), 39 deletions(-)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index b2f66460b..5b908b65d 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -33,6 +33,9 @@ import { QRCodeSVG } from 'qrcode.react';
import { CopyButton } from '../CopyButton';
import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
+// Define onramp expiry time in minutes. This is not arbitrary, but based on the assumptions imposed by the backend.
+const ONRAMP_EXPIRY_MINUTES = 5;
+
interface AssetDisplayProps {
amount: string;
symbol: string;
@@ -124,39 +127,10 @@ const BRLOnrampDetails = () => {
const rampDirection = useRampDirection();
const { t } = useTranslation();
const rampState = useRampState();
- const [timeLeft, setTimeLeft] = useState({ minutes: 5, seconds: 0 });
-
- useEffect(() => {
- if (!rampState?.ramp?.createdAt) return;
-
- const createdAtTimestamp = new Date(rampState.ramp.createdAt).getTime();
- const targetTimestamp = createdAtTimestamp + 5 * 60 * 1000; // 5 minutes in milliseconds
-
- const intervalId = setInterval(() => {
- const now = Date.now();
- const diff = targetTimestamp - now;
-
- if (diff <= 0) {
- setTimeLeft({ minutes: 0, seconds: 0 });
- clearInterval(intervalId);
- // Optionally: Add logic here to handle timer expiration, e.g., close dialog, show message
- return;
- }
-
- const minutes = Math.floor((diff / (1000 * 60)) % 60);
- const seconds = Math.floor((diff / 1000) % 60);
- setTimeLeft({ minutes, seconds });
- }, 1000);
-
- // Cleanup interval on component unmount or when dependencies change
- return () => clearInterval(intervalId);
- }, [rampState?.ramp?.createdAt]);
if (rampDirection !== RampDirection.ONRAMP) return null;
if (!rampState?.ramp?.brCode) return null;
- const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
-
return (
@@ -172,9 +146,6 @@ const BRLOnrampDetails = () => {
{t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
-
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
-
);
};
@@ -184,6 +155,7 @@ interface TransactionTokensDisplayProps {
isOnramp: boolean;
selectedNetwork: Networks;
rampDirection: RampDirection;
+ onExpiryChange: (isExpired: boolean) => void;
}
const TransactionTokensDisplay: FC = ({
@@ -191,7 +163,66 @@ const TransactionTokensDisplay: FC = ({
isOnramp,
selectedNetwork,
rampDirection,
+ onExpiryChange,
}) => {
+ const { t } = useTranslation();
+ const rampState = useRampState(); // Use rampState hook
+ const [timeLeft, setTimeLeft] = useState({ minutes: 0, seconds: 0 }); // Initialize differently
+ const [isExpired, setIsExpired] = useState(false);
+
+ useEffect(() => {
+ let targetTimestamp: number | null = null;
+
+ if (isOnramp) {
+ // Onramp: Use ramp creation time + expiry duration
+ const createdAt = rampState?.ramp?.createdAt;
+ if (createdAt) {
+ targetTimestamp = new Date(createdAt).getTime() + ONRAMP_EXPIRY_MINUTES * 60 * 1000;
+ }
+ } else {
+ // Offramp: Use quote expiry time directly
+ const expiresAt = executionInput.quote.expiresAt;
+ targetTimestamp = new Date(expiresAt).getTime();
+ }
+
+ if (targetTimestamp === null) {
+ // If no valid timestamp, mark as expired immediately
+ setTimeLeft({ minutes: 0, seconds: 0 });
+ setIsExpired(true);
+ onExpiryChange(true);
+ return;
+ }
+
+ const intervalId = setInterval(() => {
+ const now = Date.now();
+ const diff = targetTimestamp - now;
+
+ if (diff <= 0) {
+ setTimeLeft({ minutes: 0, seconds: 0 });
+ setIsExpired(true);
+ onExpiryChange(true); // Notify parent component
+ clearInterval(intervalId);
+ return;
+ }
+
+ const minutes = Math.floor((diff / (1000 * 60)) % 60);
+ const seconds = Math.floor((diff / 1000) % 60);
+ setTimeLeft({ minutes, seconds });
+ setIsExpired(false);
+ onExpiryChange(false);
+ }, 1000);
+
+ return () => clearInterval(intervalId);
+ }, [
+ isOnramp,
+ rampState?.ramp?.createdAt,
+ rampState?.quote.expiresAt,
+ onExpiryChange,
+ executionInput.quote.expiresAt,
+ ]);
+
+ const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
+
const fromToken = isOnramp
? getAnyFiatTokenDetails(executionInput.fiatToken)
: getOnChainTokenDetailsOrDefault(selectedNetwork, executionInput.onChainToken);
@@ -248,6 +279,7 @@ const TransactionTokensDisplay: FC = ({
direction={rampDirection}
/>
+ Quote expires in: {formattedTime}
);
};
@@ -256,6 +288,7 @@ export const OfframpSummaryDialog: FC = () => {
const { t } = useTranslation();
const [isSubmitted, setIsSubmitted] = useState(false);
+ const [isQuoteExpired, setIsQuoteExpired] = useState(false); // State for quote expiry
const { selectedNetwork } = useNetwork();
const { resetRampState, setRampPaymentConfirmed } = useRampActions();
@@ -271,22 +304,22 @@ export const OfframpSummaryDialog: FC = () => {
const { quote, fetchQuote } = useQuoteStore();
+ // Handler for quote expiry changes
+ const handleExpiryChange = (expired: boolean) => {
+ setIsQuoteExpired(expired);
+ };
+
const submitButtonDisabled = useMemo(() => {
if (!executionInput) return true;
+ if (isQuoteExpired) return true; // Disable if quote is expired
if (!isOnramp) {
if (!anchorUrl && getAnyFiatTokenDetails(fiatToken).type === TokenType.Stellar) return true;
if (!executionInput.brlaEvmAddress && getAnyFiatTokenDetails(fiatToken).type === 'moonbeam') return true;
- // For onramps, we register immediately when opening this summary, so the ramp should be available.
- if (!rampState?.ramp === undefined) return true;
- // Check if ramp is already expired
- if (rampState?.ramp?.createdAt && Date.now() - new Date(rampState?.ramp?.createdAt).getTime() > 5 * 60 * 1000) {
- return true;
- }
}
return isSubmitted;
- }, [anchorUrl, executionInput, fiatToken, isOnramp, isSubmitted, rampState?.ramp]);
+ }, [anchorUrl, executionInput, fiatToken, isOnramp, isSubmitted, isQuoteExpired]);
if (!visible) return null;
if (!executionInput) return null;
@@ -361,6 +394,7 @@ export const OfframpSummaryDialog: FC = () => {
isOnramp={isOnramp}
selectedNetwork={selectedNetwork}
rampDirection={rampDirection}
+ onExpiryChange={handleExpiryChange} // Pass handler down
/>
);
diff --git a/frontend/src/stores/ramp/useQuoteStore.ts b/frontend/src/stores/ramp/useQuoteStore.ts
index 638f9eefd..fc6054685 100644
--- a/frontend/src/stores/ramp/useQuoteStore.ts
+++ b/frontend/src/stores/ramp/useQuoteStore.ts
@@ -98,6 +98,7 @@ export const useQuoteStore = create((set) => ({
error: null,
outputAmount: undefined,
exchangeRate: 0,
+ quoteFetchedAt: null, // Initialize timestamp
fetchQuote: async (params: QuoteParams) => {
const { inputAmount } = params;
From f8d7a084e65bac97f723d77678c6defce7ae79e8 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 13:11:11 +0200
Subject: [PATCH 29/52] improve RampSummary button logic
---
.../components/OfframpSummaryDialog/index.tsx | 147 ++++++++++++++----
frontend/src/translations/en.json | 1 +
frontend/src/translations/pt.json | 1 +
3 files changed, 117 insertions(+), 32 deletions(-)
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
index 49f13867d..bb898b099 100644
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ b/frontend/src/components/OfframpSummaryDialog/index.tsx
@@ -17,7 +17,6 @@ import { useGetAssetIcon } from '../../hooks/useGetAssetIcon';
import { useNetwork } from '../../contexts/network';
import { ExchangeRate } from '../ExchangeRate';
-import { FiatIcon } from '../FiatIcon';
import { NetworkIcon } from '../NetworkIcon';
import { Dialog } from '../Dialog';
import { Spinner } from '../Spinner';
@@ -89,13 +88,9 @@ const FeeDetails = ({
{fiatToken.offrampFeesFixedComponent ? ` + ${fiatToken.offrampFeesFixedComponent} ${fiatSymbol}` : ''})
- {isOfframp ? (
-
- ) : (
-
- )}
+
- {feesCost} {isOfframp ? (toToken as OnChainTokenDetails).assetSymbol : fiatSymbol}
+ {feesCost} {(toToken as OnChainTokenDetails).assetSymbol}
@@ -251,6 +246,88 @@ const TransactionTokensDisplay: FC = ({
);
};
+const useButtonContent = ({
+ isSubmitted,
+ toToken,
+ submitButtonDisabled,
+ isQuoteExpired,
+}: {
+ isSubmitted: boolean;
+ toToken: FiatTokenDetails;
+ submitButtonDisabled: boolean;
+ isQuoteExpired: boolean;
+}) => {
+ const rampState = useRampState();
+ const { t } = useTranslation();
+ const rampDirection = useRampDirection();
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const isOfframp = rampDirection === RampDirection.OFFRAMP;
+ const isBRCodeReady = Boolean(rampState?.ramp?.brCode);
+
+ // BRL offramp has no redirect, it is the only with type moonbeam
+ const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
+ const isAnchorWithRedirect = !isAnchorWithoutRedirect;
+
+ return useMemo(() => {
+ if (submitButtonDisabled) {
+ return {
+ text: t('components.swapSubmitButton.processing'),
+ icon: ,
+ };
+ }
+
+ if (isQuoteExpired) {
+ return {
+ text: t('components.swapSubmitButton.quoteExpired'),
+ icon: null,
+ };
+ }
+
+ if (isOfframp && rampState !== undefined) {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.processing'),
+ icon: ,
+ };
+ }
+
+ if (isOnramp && isBRCodeReady) {
+ return {
+ text: t('components.swapSubmitButton.confirmPayment'),
+ icon: null,
+ };
+ }
+
+ if (isOfframp && isAnchorWithRedirect) {
+ if (isSubmitted) {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.continueOnPartnersPage'),
+ icon: ,
+ };
+ } else {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.continueWithPartner'),
+ icon: ,
+ };
+ }
+ }
+
+ return {
+ text: t('components.swapSubmitButton.processing'),
+ icon: ,
+ };
+ }, [
+ submitButtonDisabled,
+ isQuoteExpired,
+ isOfframp,
+ rampState,
+ isOnramp,
+ isBRCodeReady,
+ isAnchorWithRedirect,
+ t,
+ isSubmitted,
+ ]);
+};
+
export const OfframpSummaryDialog: FC = () => {
const { t } = useTranslation();
@@ -266,13 +343,26 @@ export const OfframpSummaryDialog: FC = () => {
const anchorUrl = useSep24StoreCachedAnchorUrl();
const rampDirection = useRampDirection();
const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const isOfframp = rampDirection === RampDirection.OFFRAMP;
const fiatToken = useFiatToken();
const onChainToken = useOnChainToken();
+ const isQuoteExpired = useMemo(() => {
+ if (!rampState?.ramp?.createdAt) {
+ return false;
+ }
+
+ const creationTime = new Date(rampState.ramp.createdAt).getTime();
+ const currentTime = Date.now();
+ const expirationTime = 5 * 60 * 1000; // 5 minutes in milliseconds
+
+ return currentTime - creationTime > expirationTime;
+ }, [rampState?.ramp?.createdAt]);
+
const submitButtonDisabled = useMemo(() => {
if (!executionInput) return true;
- if (!isOnramp) {
+ if (isOfframp) {
if (!anchorUrl && getAnyFiatTokenDetails(fiatToken).type === TokenType.Stellar) return true;
if (!executionInput.brlaEvmAddress && getAnyFiatTokenDetails(fiatToken).type === 'moonbeam') return true;
// For onramps, we register immediately when opening this summary, so the ramp should be available.
@@ -281,18 +371,28 @@ export const OfframpSummaryDialog: FC = () => {
if (rampState?.ramp?.createdAt && Date.now() - new Date(rampState?.ramp?.createdAt).getTime() > 5 * 60 * 1000) {
return true;
}
+
+ const isBRCodeReady = Boolean(isOnramp && rampState?.ramp?.brCode);
+ if (!isBRCodeReady) return true;
}
return isSubmitted;
- }, [anchorUrl, executionInput, fiatToken, isOnramp, isSubmitted, rampState?.ramp]);
-
- if (!visible) return null;
- if (!executionInput) return null;
+ }, [anchorUrl, executionInput, fiatToken, isOfframp, isOnramp, isSubmitted, rampState?.ramp]);
const toToken = isOnramp
? getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken)
: getAnyFiatTokenDetails(fiatToken);
+ const buttonContent = useButtonContent({
+ isSubmitted,
+ toToken,
+ submitButtonDisabled,
+ isQuoteExpired,
+ });
+
+ if (!visible) return null;
+ if (!executionInput) return null;
+
const onClose = () => {
setIsSubmitted(false);
setRampExecutionInput(undefined);
@@ -326,26 +426,9 @@ export const OfframpSummaryDialog: FC = () => {
style={{ flex: '1 1 calc(50% - 0.75rem/2)' }}
onClick={onSubmit}
>
- {rampState !== undefined ? (
- <>
- {t('components.dialogs.OfframpSummaryDialog.processing')}
- >
- ) : !isOnramp && isSubmitted ? (
- <>
- {t('components.dialogs.OfframpSummaryDialog.continueOnPartnersPage')}
- >
- ) : !isOnramp && (toToken as FiatTokenDetails).type !== 'moonbeam' ? (
- <>
- {t('components.dialogs.OfframpSummaryDialog.continueWithPartner')}{' '}
-
- >
- ) : isSubmitted ? (
- <>
- {t('components.dialogs.OfframpSummaryDialog.processing')}
- >
- ) : (
- <>{t('components.swapSubmitButton.confirmPayment')}>
- )}
+ {buttonContent.icon}
+ {buttonContent.icon && ' '}
+ {buttonContent.text}
);
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 9b2347378..a6dbc73f3 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -204,6 +204,7 @@
"onrampFee": "Onramp fee",
"continueWithPartner": "Continue with Partner",
"continueOnPartnersPage": "Continue on Partner's page",
+ "quoteExpired": "Quote expired",
"processing": "Processing",
"continue": "Continue",
"quote": "Quote",
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index ea583df0e..df5347579 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -204,6 +204,7 @@
"onrampFee": "Taxa de compra",
"continueWithPartner": "Continuar com Parceiro",
"continueOnPartnersPage": "Continuar na página do Parceiro",
+ "quoteExpired": "Cotação expirada",
"processing": "Processando",
"continue": "Continuar",
"quote": "Cotação",
From 8e19e1d5f17a698b57d7b1c8759bcea5fb9ff149 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 13:53:43 +0200
Subject: [PATCH 30/52] remove unused ref in BrlaExtendedForm
---
.../components/BrlaComponents/BrlaExtendedForm.tsx | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/frontend/src/components/BrlaComponents/BrlaExtendedForm.tsx b/frontend/src/components/BrlaComponents/BrlaExtendedForm.tsx
index bc5d8110c..ef609bdb8 100644
--- a/frontend/src/components/BrlaComponents/BrlaExtendedForm.tsx
+++ b/frontend/src/components/BrlaComponents/BrlaExtendedForm.tsx
@@ -1,4 +1,3 @@
-import { RefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { useKYCProcess } from '../../hooks/brla/useBRLAKYCProcess';
@@ -8,11 +7,7 @@ import { VerificationStatus } from './VerificationStatus';
import { BrlaFieldProps, ExtendedBrlaFieldOptions } from './BrlaField';
import { KYCForm } from './KYCForm';
-interface PIXKYCFormProps {
- feeComparisonRef: RefObject;
-}
-
-export const PIXKYCForm = ({ feeComparisonRef }: PIXKYCFormProps) => {
+export const PIXKYCForm = () => {
const { verificationStatus, statusMessage, handleFormSubmit, handleBackClick, isSubmitted } = useKYCProcess();
const { kycForm } = useKYCForm();
@@ -96,12 +91,7 @@ export const PIXKYCForm = ({ feeComparisonRef }: PIXKYCFormProps) => {
return (
{!isSubmitted ? (
-
+
) : (
)}
From 235da8ea9d96597f7c4b224c9950cabeecd271f7 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 13:54:26 +0200
Subject: [PATCH 31/52] refactor OfframpSummaryDialog into RampSummaryDialog
---
.../components/OfframpSummaryDialog/index.tsx | 473 ------------------
.../RampSummaryDialog/AssetDisplay.tsx | 17 +
.../RampSummaryDialog/BRLOnrampDetails.tsx | 34 ++
.../RampSummaryDialog/FeeDetails.tsx | 74 +++
.../RampSummaryDialog/RampSummaryButton.tsx | 162 ++++++
.../TransactionTokensDisplay.tsx | 148 ++++++
.../components/RampSummaryDialog/index.tsx | 55 ++
7 files changed, 490 insertions(+), 473 deletions(-)
delete mode 100644 frontend/src/components/OfframpSummaryDialog/index.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/AssetDisplay.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/FeeDetails.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
create mode 100644 frontend/src/components/RampSummaryDialog/index.tsx
diff --git a/frontend/src/components/OfframpSummaryDialog/index.tsx b/frontend/src/components/OfframpSummaryDialog/index.tsx
deleted file mode 100644
index 84f1cd022..000000000
--- a/frontend/src/components/OfframpSummaryDialog/index.tsx
+++ /dev/null
@@ -1,473 +0,0 @@
-import { ArrowDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
-import { useState, FC, useMemo, useEffect } from 'react';
-import Big from 'big.js';
-
-import {
- getOnChainTokenDetailsOrDefault,
- OnChainTokenDetails,
- BaseFiatTokenDetails,
- isStellarOutputTokenDetails,
- getAnyFiatTokenDetails,
- TokenType,
- FiatTokenDetails,
- isFiatTokenDetails,
- Networks,
-} from 'shared';
-import { useGetAssetIcon } from '../../hooks/useGetAssetIcon';
-import { useNetwork } from '../../contexts/network';
-
-import { ExchangeRate } from '../ExchangeRate';
-import { NetworkIcon } from '../NetworkIcon';
-import { Dialog } from '../Dialog';
-import { Spinner } from '../Spinner';
-import { useRampActions, useRampState, useRampExecutionInput, useRampSummaryVisible } from '../../stores/offrampStore';
-import { useRampSubmission } from '../../hooks/ramp/useRampSubmission';
-import { useSep24StoreCachedAnchorUrl } from '../../stores/sep24Store';
-import { useRampDirection } from '../../stores/rampDirectionStore';
-import { RampDirection } from '../../components/RampToggle';
-import { RampExecutionInput } from '../../types/phases';
-import { useTranslation } from 'react-i18next';
-import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
-import { QRCodeSVG } from 'qrcode.react';
-import { CopyButton } from '../CopyButton';
-import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
-
-// Define onramp expiry time in minutes. This is not arbitrary, but based on the assumptions imposed by the backend.
-const ONRAMP_EXPIRY_MINUTES = 5;
-
-interface AssetDisplayProps {
- amount: string;
- symbol: string;
- iconSrc: string;
- iconAlt: string;
-}
-
-const AssetDisplay = ({ amount, symbol, iconSrc, iconAlt }: AssetDisplayProps) => (
-
-
- {amount} {symbol}
-
-

-
-);
-
-interface FeeDetailsProps {
- network: Networks;
- feesCost: string;
- fiatSymbol: string;
- exchangeRate: string;
- fromToken: OnChainTokenDetails | FiatTokenDetails;
- toToken: OnChainTokenDetails | FiatTokenDetails;
- partnerUrl: string;
- direction: RampDirection;
-}
-
-const FeeDetails = ({
- network,
- feesCost,
- fiatSymbol,
- fromToken,
- toToken,
- exchangeRate,
- partnerUrl,
- direction,
-}: FeeDetailsProps) => {
- const { t } = useTranslation();
-
- const isOfframp = direction === RampDirection.OFFRAMP;
-
- const fiatToken = (isOfframp ? toToken : fromToken) as FiatTokenDetails;
- if (!isFiatTokenDetails(fiatToken)) {
- throw new Error('Invalid fiat token details');
- }
-
- return (
-
-
-
- {isOfframp
- ? t('components.dialogs.OfframpSummaryDialog.offrampFee')
- : t('components.dialogs.OfframpSummaryDialog.onrampFee')}{' '}
- ({`${fiatToken.offrampFeesBasisPoints / 100}%`}
- {fiatToken.offrampFeesFixedComponent ? ` + ${fiatToken.offrampFeesFixedComponent} ${fiatSymbol}` : ''})
-
-
-
-
- {feesCost} {(toToken as OnChainTokenDetails).assetSymbol}
-
-
-
-
-
{t('components.dialogs.OfframpSummaryDialog.quote')}
-
-
-
-
-
-
- );
-};
-
-const BRLOnrampDetails = () => {
- const rampDirection = useRampDirection();
- const { t } = useTranslation();
- const rampState = useRampState();
-
- if (rampDirection !== RampDirection.ONRAMP) return null;
- if (!rampState?.ramp?.brCode) return null;
-
- return (
-
-
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.title')}
-
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.description')}
-
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.qrCode')}
-
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
-
-
- );
-};
-
-interface TransactionTokensDisplayProps {
- executionInput: RampExecutionInput;
- isOnramp: boolean;
- selectedNetwork: Networks;
- rampDirection: RampDirection;
- onExpiryChange: (isExpired: boolean) => void;
-}
-
-const TransactionTokensDisplay: FC = ({
- executionInput,
- isOnramp,
- selectedNetwork,
- rampDirection,
- onExpiryChange,
-}) => {
- const { t } = useTranslation();
- const rampState = useRampState(); // Use rampState hook
- const [timeLeft, setTimeLeft] = useState({ minutes: 0, seconds: 0 }); // Initialize differently
- const [isExpired, setIsExpired] = useState(false);
-
- useEffect(() => {
- let targetTimestamp: number | null = null;
-
- if (isOnramp) {
- // Onramp: Use ramp creation time + expiry duration
- const createdAt = rampState?.ramp?.createdAt;
- if (createdAt) {
- targetTimestamp = new Date(createdAt).getTime() + ONRAMP_EXPIRY_MINUTES * 60 * 1000;
- }
- } else {
- // Offramp: Use quote expiry time directly
- const expiresAt = executionInput.quote.expiresAt;
- targetTimestamp = new Date(expiresAt).getTime();
- }
-
- if (targetTimestamp === null) {
- // If no valid timestamp, mark as expired immediately
- setTimeLeft({ minutes: 0, seconds: 0 });
- setIsExpired(true);
- onExpiryChange(true);
- return;
- }
-
- const intervalId = setInterval(() => {
- const now = Date.now();
- const diff = targetTimestamp - now;
-
- if (diff <= 0) {
- setTimeLeft({ minutes: 0, seconds: 0 });
- setIsExpired(true);
- onExpiryChange(true); // Notify parent component
- clearInterval(intervalId);
- return;
- }
-
- const minutes = Math.floor((diff / (1000 * 60)) % 60);
- const seconds = Math.floor((diff / 1000) % 60);
- setTimeLeft({ minutes, seconds });
- setIsExpired(false);
- onExpiryChange(false);
- }, 1000);
-
- return () => clearInterval(intervalId);
- }, [
- isOnramp,
- rampState?.ramp?.createdAt,
- rampState?.quote.expiresAt,
- onExpiryChange,
- executionInput.quote.expiresAt,
- ]);
-
- const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
-
- const fromToken = isOnramp
- ? getAnyFiatTokenDetails(executionInput.fiatToken)
- : getOnChainTokenDetailsOrDefault(selectedNetwork, executionInput.onChainToken);
-
- const toToken = isOnramp
- ? getOnChainTokenDetailsOrDefault(selectedNetwork, executionInput.onChainToken)
- : getAnyFiatTokenDetails(executionInput.fiatToken);
-
- const fromIcon = useGetAssetIcon(
- isOnramp ? (fromToken as BaseFiatTokenDetails).fiat.assetIcon : (fromToken as OnChainTokenDetails).networkAssetIcon,
- );
- const toIcon = useGetAssetIcon(
- isOnramp ? (toToken as OnChainTokenDetails).networkAssetIcon : (toToken as BaseFiatTokenDetails).fiat.assetIcon,
- );
-
- const getPartnerUrl = (): string => {
- const fiatToken = (isOnramp ? fromToken : toToken) as FiatTokenDetails;
- return isStellarOutputTokenDetails(fiatToken) ? fiatToken.anchorHomepageUrl : fiatToken.partnerUrl;
- };
-
- const fiatSymbol = isOnramp
- ? (fromToken as BaseFiatTokenDetails).fiat.symbol
- : (toToken as BaseFiatTokenDetails).fiat.symbol;
-
- return (
-
-
-
-
-
-
-
Quote expires in: {formattedTime}
-
- );
-};
-
-const useButtonContent = ({
- isSubmitted,
- toToken,
- submitButtonDisabled,
- isQuoteExpired,
-}: {
- isSubmitted: boolean;
- toToken: FiatTokenDetails;
- submitButtonDisabled: boolean;
- isQuoteExpired: boolean;
-}) => {
- const rampState = useRampState();
- const { t } = useTranslation();
- const rampDirection = useRampDirection();
- const isOnramp = rampDirection === RampDirection.ONRAMP;
- const isOfframp = rampDirection === RampDirection.OFFRAMP;
- const isBRCodeReady = Boolean(rampState?.ramp?.brCode);
-
- // BRL offramp has no redirect, it is the only with type moonbeam
- const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
- const isAnchorWithRedirect = !isAnchorWithoutRedirect;
-
- return useMemo(() => {
- if (submitButtonDisabled) {
- return {
- text: t('components.swapSubmitButton.processing'),
- icon: ,
- };
- }
-
- if (isQuoteExpired) {
- return {
- text: t('components.swapSubmitButton.quoteExpired'),
- icon: null,
- };
- }
-
- if (isOfframp && rampState !== undefined) {
- return {
- text: t('components.dialogs.OfframpSummaryDialog.processing'),
- icon: ,
- };
- }
-
- if (isOnramp && isBRCodeReady) {
- return {
- text: t('components.swapSubmitButton.confirmPayment'),
- icon: null,
- };
- }
-
- if (isOfframp && isAnchorWithRedirect) {
- if (isSubmitted) {
- return {
- text: t('components.dialogs.OfframpSummaryDialog.continueOnPartnersPage'),
- icon: ,
- };
- } else {
- return {
- text: t('components.dialogs.OfframpSummaryDialog.continueWithPartner'),
- icon: ,
- };
- }
- }
-
- return {
- text: t('components.swapSubmitButton.processing'),
- icon: ,
- };
- }, [
- submitButtonDisabled,
- isQuoteExpired,
- isOfframp,
- rampState,
- isOnramp,
- isBRCodeReady,
- isAnchorWithRedirect,
- t,
- isSubmitted,
- ]);
-};
-
-export const OfframpSummaryDialog: FC = () => {
- const { t } = useTranslation();
-
- const [isSubmitted, setIsSubmitted] = useState(false);
- const [isQuoteExpired, setIsQuoteExpired] = useState(false); // State for quote expiry
-
- const { selectedNetwork } = useNetwork();
- const { resetRampState, setRampPaymentConfirmed } = useRampActions();
- const rampState = useRampState();
- const executionInput = useRampExecutionInput();
- const visible = useRampSummaryVisible();
- const { onRampConfirm } = useRampSubmission();
- const anchorUrl = useSep24StoreCachedAnchorUrl();
- const rampDirection = useRampDirection();
- const isOnramp = rampDirection === RampDirection.ONRAMP;
- const isOfframp = rampDirection === RampDirection.OFFRAMP;
- const fiatToken = useFiatToken();
- const onChainToken = useOnChainToken();
-
- const { quote, fetchQuote } = useQuoteStore();
-
- // Handler for quote expiry changes
- const handleExpiryChange = (expired: boolean) => {
- setIsQuoteExpired(expired);
- };
-
- const submitButtonDisabled = useMemo(() => {
- if (!executionInput) return true;
- if (isQuoteExpired) return true; // Disable if quote is expired
-
- if (isOfframp) {
- if (!anchorUrl && getAnyFiatTokenDetails(fiatToken).type === TokenType.Stellar) return true;
- if (!executionInput.brlaEvmAddress && getAnyFiatTokenDetails(fiatToken).type === 'moonbeam') return true;
- }
-
- const isBRCodeReady = Boolean(isOnramp && rampState?.ramp?.brCode);
- if (!isBRCodeReady) return true;
-
- return isSubmitted;
- }, [executionInput, isQuoteExpired, isOfframp, isOnramp, rampState?.ramp?.brCode, isSubmitted, anchorUrl, fiatToken]);
-
- const toToken = isOnramp
- ? getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken)
- : getAnyFiatTokenDetails(fiatToken);
-
- const buttonContent = useButtonContent({
- isSubmitted,
- toToken,
- submitButtonDisabled,
- isQuoteExpired,
- });
-
- if (!visible) return null;
- if (!executionInput) return null;
-
- const onClose = () => {
- resetRampState();
- // Make sure a new quote is fetched immediately. The previous one was consumed when this dialog was opened
- fetchQuote({
- rampType: isOnramp ? 'on' : 'off',
- inputAmount: Big(quote?.inputAmount || '0'),
- onChainToken,
- fiatToken,
- selectedNetwork,
- });
- };
-
- const onSubmit = () => {
- setIsSubmitted(true);
-
- if (executionInput.quote.rampType === 'on') {
- setRampPaymentConfirmed(true);
- } else {
- onRampConfirm();
- }
-
- if (!isOnramp && (toToken as FiatTokenDetails).type !== 'moonbeam' && anchorUrl) {
- window.open(anchorUrl, '_blank');
- }
- };
-
- const headerText = isOnramp
- ? t('components.dialogs.OfframpSummaryDialog.headerText.buy')
- : t('components.dialogs.OfframpSummaryDialog.headerText.sell');
-
- const actions = (
-
- );
-
- const content = (
-
- );
-
- return ;
-};
diff --git a/frontend/src/components/RampSummaryDialog/AssetDisplay.tsx b/frontend/src/components/RampSummaryDialog/AssetDisplay.tsx
new file mode 100644
index 000000000..c05057a68
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/AssetDisplay.tsx
@@ -0,0 +1,17 @@
+import { FC } from 'react';
+
+interface AssetDisplayProps {
+ amount: string;
+ symbol: string;
+ iconSrc: string;
+ iconAlt: string;
+}
+
+export const AssetDisplay: FC = ({ amount, symbol, iconSrc, iconAlt }) => (
+
+
+ {amount} {symbol}
+
+

+
+);
diff --git a/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
new file mode 100644
index 000000000..c778e2e91
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
@@ -0,0 +1,34 @@
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { QRCodeSVG } from 'qrcode.react';
+import { CopyButton } from '../CopyButton';
+import { useRampDirection } from '../../stores/rampDirectionStore';
+import { useRampState } from '../../stores/offrampStore';
+import { RampDirection } from '../RampToggle';
+
+export const BRLOnrampDetails: FC = () => {
+ const rampDirection = useRampDirection();
+ const { t } = useTranslation();
+ const rampState = useRampState();
+
+ if (rampDirection !== RampDirection.ONRAMP) return null;
+ if (!rampState?.ramp?.brCode) return null;
+
+ return (
+
+
+ {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.title')}
+
+ {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.description')}
+
+ {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.qrCode')}
+
+ {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
+
+
+ );
+};
diff --git a/frontend/src/components/RampSummaryDialog/FeeDetails.tsx b/frontend/src/components/RampSummaryDialog/FeeDetails.tsx
new file mode 100644
index 000000000..79073105f
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/FeeDetails.tsx
@@ -0,0 +1,74 @@
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FiatTokenDetails, isFiatTokenDetails, Networks, OnChainTokenDetails } from 'shared';
+
+import { ExchangeRate } from '../ExchangeRate';
+import { NetworkIcon } from '../NetworkIcon';
+import { RampDirection } from '../RampToggle';
+
+interface FeeDetailsProps {
+ network: Networks;
+ feesCost: string;
+ fiatSymbol: string;
+ exchangeRate: string;
+ fromToken: OnChainTokenDetails | FiatTokenDetails;
+ toToken: OnChainTokenDetails | FiatTokenDetails;
+ partnerUrl: string;
+ direction: RampDirection;
+}
+
+export const FeeDetails: FC = ({
+ network,
+ feesCost,
+ fiatSymbol,
+ fromToken,
+ toToken,
+ exchangeRate,
+ partnerUrl,
+ direction,
+}) => {
+ const { t } = useTranslation();
+
+ const isOfframp = direction === RampDirection.OFFRAMP;
+
+ const fiatToken = (isOfframp ? toToken : fromToken) as FiatTokenDetails;
+ if (!isFiatTokenDetails(fiatToken)) {
+ throw new Error('Invalid fiat token details');
+ }
+
+ return (
+
+
+
+ {isOfframp
+ ? t('components.dialogs.OfframpSummaryDialog.offrampFee')
+ : t('components.dialogs.OfframpSummaryDialog.onrampFee')}{' '}
+ ({`${fiatToken.offrampFeesBasisPoints / 100}%`}
+ {fiatToken.offrampFeesFixedComponent ? ` + ${fiatToken.offrampFeesFixedComponent} ${fiatSymbol}` : ''})
+
+
+
+
+ {feesCost} {(toToken as OnChainTokenDetails).assetSymbol}
+
+
+
+
+
{t('components.dialogs.OfframpSummaryDialog.quote')}
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
new file mode 100644
index 000000000..b5771557f
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
@@ -0,0 +1,162 @@
+import { useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
+import { FiatTokenDetails, getAnyFiatTokenDetails, getOnChainTokenDetailsOrDefault, TokenType } from 'shared';
+import { useRampExecutionInput, useRampState } from '../../stores/offrampStore';
+import { useRampDirection } from '../../stores/rampDirectionStore';
+import { RampDirection } from '../RampToggle';
+import { useRampSummaryStore } from '../../stores/rampSummary';
+import { useRampActions } from '../../stores/offrampStore';
+import { useRampSubmission } from '../../hooks/ramp/useRampSubmission';
+import { useSep24StoreCachedAnchorUrl } from '../../stores/sep24Store';
+import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
+import { useNetwork } from '../../contexts/network';
+import { Spinner } from '../Spinner';
+
+interface UseButtonContentProps {
+ isSubmitted: boolean;
+ toToken: FiatTokenDetails;
+ submitButtonDisabled: boolean;
+}
+
+export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }: UseButtonContentProps) => {
+ const rampState = useRampState();
+ const { t } = useTranslation();
+ const rampDirection = useRampDirection();
+ const { isQuoteExpired } = useRampSummaryStore();
+
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const isOfframp = rampDirection === RampDirection.OFFRAMP;
+ const isBRCodeReady = Boolean(rampState?.ramp?.brCode);
+
+ // BRL offramp has no redirect, it is the only with type moonbeam
+ const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
+ const isAnchorWithRedirect = !isAnchorWithoutRedirect;
+
+ return useMemo(() => {
+ if (submitButtonDisabled) {
+ return {
+ text: t('components.swapSubmitButton.processing'),
+ icon: ,
+ };
+ }
+
+ if (isQuoteExpired) {
+ return {
+ text: t('components.swapSubmitButton.quoteExpired'),
+ icon: null,
+ };
+ }
+
+ if (isOfframp && rampState !== undefined) {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.processing'),
+ icon: ,
+ };
+ }
+
+ if (isOnramp && isBRCodeReady) {
+ return {
+ text: t('components.swapSubmitButton.confirmPayment'),
+ icon: null,
+ };
+ }
+
+ if (isOfframp && isAnchorWithRedirect) {
+ if (isSubmitted) {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.continueOnPartnersPage'),
+ icon: ,
+ };
+ } else {
+ return {
+ text: t('components.dialogs.OfframpSummaryDialog.continueWithPartner'),
+ icon: ,
+ };
+ }
+ }
+
+ return {
+ text: t('components.swapSubmitButton.processing'),
+ icon: ,
+ };
+ }, [
+ submitButtonDisabled,
+ isQuoteExpired,
+ isOfframp,
+ rampState,
+ isOnramp,
+ isBRCodeReady,
+ isAnchorWithRedirect,
+ t,
+ isSubmitted,
+ ]);
+};
+
+export const RampSummaryButton = () => {
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const { setRampPaymentConfirmed } = useRampActions();
+ const rampState = useRampState();
+ const { onRampConfirm } = useRampSubmission();
+ const anchorUrl = useSep24StoreCachedAnchorUrl();
+ const rampDirection = useRampDirection();
+ const isOfframp = rampDirection === RampDirection.OFFRAMP;
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const { isQuoteExpired } = useRampSummaryStore();
+ const fiatToken = useFiatToken();
+ const onChainToken = useOnChainToken();
+ const { selectedNetwork } = useNetwork();
+ const executionInput = useRampExecutionInput();
+
+ const toToken = isOnramp
+ ? getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken)
+ : getAnyFiatTokenDetails(fiatToken);
+
+ const submitButtonDisabled = useMemo(() => {
+ if (!executionInput) return true;
+ if (isQuoteExpired) return true;
+
+ if (isOfframp) {
+ if (!anchorUrl && getAnyFiatTokenDetails(fiatToken).type === TokenType.Stellar) return true;
+ if (!executionInput.brlaEvmAddress && getAnyFiatTokenDetails(fiatToken).type === 'moonbeam') return true;
+ }
+
+ const isBRCodeReady = Boolean(isOnramp && rampState?.ramp?.brCode);
+ if (isOnramp && !isBRCodeReady) return true;
+
+ return isSubmitted;
+ }, [executionInput, isQuoteExpired, isOfframp, isOnramp, rampState?.ramp?.brCode, isSubmitted, anchorUrl, fiatToken]);
+
+ const buttonContent = useButtonContent({
+ isSubmitted,
+ toToken: toToken as FiatTokenDetails,
+ submitButtonDisabled,
+ });
+
+ const onSubmit = () => {
+ setIsSubmitted(true);
+
+ if (executionInput?.quote.rampType === 'on') {
+ setRampPaymentConfirmed(true);
+ } else {
+ onRampConfirm();
+ }
+
+ if (!isOnramp && (toToken as FiatTokenDetails).type !== 'moonbeam' && anchorUrl) {
+ window.open(anchorUrl, '_blank');
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
new file mode 100644
index 000000000..3addfb592
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -0,0 +1,148 @@
+import { FC, useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import Big from 'big.js';
+import { ArrowDownIcon } from '@heroicons/react/20/solid';
+import {
+ getOnChainTokenDetailsOrDefault,
+ getAnyFiatTokenDetails,
+ OnChainTokenDetails,
+ BaseFiatTokenDetails,
+ isStellarOutputTokenDetails,
+ FiatTokenDetails,
+ Networks,
+} from 'shared';
+import { useGetAssetIcon } from '../../hooks/useGetAssetIcon';
+import { useRampState } from '../../stores/offrampStore';
+import { RampDirection } from '../RampToggle';
+import { RampExecutionInput } from '../../types/phases';
+import { useRampSummaryStore } from '../../stores/rampSummary';
+import { AssetDisplay } from './AssetDisplay';
+import { FeeDetails } from './FeeDetails';
+import { BRLOnrampDetails } from './BRLOnrampDetails';
+
+// Define onramp expiry time in minutes. This is not arbitrary, but based on the assumptions imposed by the backend.
+const ONRAMP_EXPIRY_MINUTES = 5;
+
+interface TransactionTokensDisplayProps {
+ executionInput: RampExecutionInput;
+ isOnramp: boolean;
+ selectedNetwork: Networks;
+ rampDirection: RampDirection;
+}
+
+export const TransactionTokensDisplay: FC = ({
+ executionInput,
+ isOnramp,
+ selectedNetwork,
+ rampDirection,
+}) => {
+ const { t } = useTranslation();
+ const rampState = useRampState();
+ const [timeLeft, setTimeLeft] = useState({ minutes: 0, seconds: 0 });
+ const { setIsQuoteExpired } = useRampSummaryStore();
+
+ useEffect(() => {
+ let targetTimestamp: number | null = null;
+
+ if (isOnramp) {
+ // Onramp: Use ramp creation time + expiry duration
+ const createdAt = rampState?.ramp?.createdAt;
+ if (createdAt) {
+ targetTimestamp = new Date(createdAt).getTime() + ONRAMP_EXPIRY_MINUTES * 60 * 1000;
+ }
+ } else {
+ // Offramp: Use quote expiry time directly
+ const expiresAt = executionInput.quote.expiresAt;
+ targetTimestamp = new Date(expiresAt).getTime();
+ }
+
+ if (targetTimestamp === null) {
+ // If no valid timestamp, mark as expired immediately
+ setTimeLeft({ minutes: 0, seconds: 0 });
+ setIsQuoteExpired(true);
+ return;
+ }
+
+ const intervalId = setInterval(() => {
+ const now = Date.now();
+ const diff = targetTimestamp - now;
+
+ if (diff <= 0) {
+ setTimeLeft({ minutes: 0, seconds: 0 });
+ setIsQuoteExpired(true);
+ clearInterval(intervalId);
+ return;
+ }
+
+ const minutes = Math.floor((diff / (1000 * 60)) % 60);
+ const seconds = Math.floor((diff / 1000) % 60);
+ setTimeLeft({ minutes, seconds });
+ setIsQuoteExpired(false);
+ }, 1000);
+
+ return () => clearInterval(intervalId);
+ }, [isOnramp, rampState?.ramp?.createdAt, executionInput.quote.expiresAt, setIsQuoteExpired]);
+
+ const formattedTime = `${timeLeft.minutes}:${timeLeft.seconds < 10 ? '0' : ''}${timeLeft.seconds}`;
+
+ const fromToken = isOnramp
+ ? getAnyFiatTokenDetails(executionInput.fiatToken)
+ : getOnChainTokenDetailsOrDefault(selectedNetwork, executionInput.onChainToken);
+
+ const toToken = isOnramp
+ ? getOnChainTokenDetailsOrDefault(selectedNetwork, executionInput.onChainToken)
+ : getAnyFiatTokenDetails(executionInput.fiatToken);
+
+ const fromIcon = useGetAssetIcon(
+ isOnramp ? (fromToken as BaseFiatTokenDetails).fiat.assetIcon : (fromToken as OnChainTokenDetails).networkAssetIcon,
+ );
+
+ const toIcon = useGetAssetIcon(
+ isOnramp ? (toToken as OnChainTokenDetails).networkAssetIcon : (toToken as BaseFiatTokenDetails).fiat.assetIcon,
+ );
+
+ const getPartnerUrl = (): string => {
+ const fiatToken = (isOnramp ? fromToken : toToken) as FiatTokenDetails;
+ return isStellarOutputTokenDetails(fiatToken) ? fiatToken.anchorHomepageUrl : fiatToken.partnerUrl;
+ };
+
+ const fiatSymbol = isOnramp
+ ? (fromToken as BaseFiatTokenDetails).fiat.symbol
+ : (toToken as BaseFiatTokenDetails).fiat.symbol;
+
+ return (
+
+
+
+
+
+
+
Quote expires in: {formattedTime}
+
+ );
+};
diff --git a/frontend/src/components/RampSummaryDialog/index.tsx b/frontend/src/components/RampSummaryDialog/index.tsx
new file mode 100644
index 000000000..92890fb07
--- /dev/null
+++ b/frontend/src/components/RampSummaryDialog/index.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react';
+import Big from 'big.js';
+import { useTranslation } from 'react-i18next';
+import { Dialog } from '../Dialog';
+import { useRampActions, useRampExecutionInput, useRampSummaryVisible } from '../../stores/offrampStore';
+import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
+import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
+import { useNetwork } from '../../contexts/network';
+import { RampDirection } from '../RampToggle';
+import { TransactionTokensDisplay } from './TransactionTokensDisplay';
+import { RampSummaryButton } from './RampSummaryButton';
+import { useRampDirection } from '../../stores/rampDirectionStore';
+
+export const RampSummaryDialog: FC = () => {
+ const { t } = useTranslation();
+ const { selectedNetwork } = useNetwork();
+ const { resetRampState } = useRampActions();
+ const executionInput = useRampExecutionInput();
+ const visible = useRampSummaryVisible();
+ const rampDirection = useRampDirection();
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const fiatToken = useFiatToken();
+ const onChainToken = useOnChainToken();
+ const { quote, fetchQuote } = useQuoteStore();
+
+ if (!visible) return null;
+ if (!executionInput) return null;
+
+ const onClose = () => {
+ resetRampState();
+ fetchQuote({
+ rampType: isOnramp ? 'on' : 'off',
+ inputAmount: Big(quote?.inputAmount || '0'),
+ onChainToken,
+ fiatToken,
+ selectedNetwork,
+ });
+ };
+
+ const headerText = isOnramp
+ ? t('components.dialogs.OfframpSummaryDialog.headerText.buy')
+ : t('components.dialogs.OfframpSummaryDialog.headerText.sell');
+
+ const actions = ;
+ const content = (
+
+ );
+
+ return ;
+};
From dfa8cf8d7865dea6d0626794a2837e9a090a0025 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 13:54:45 +0200
Subject: [PATCH 32/52] add rampSummary store to handle expiry logic
---
frontend/src/stores/rampSummary/index.ts | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 frontend/src/stores/rampSummary/index.ts
diff --git a/frontend/src/stores/rampSummary/index.ts b/frontend/src/stores/rampSummary/index.ts
new file mode 100644
index 000000000..bc33fbff9
--- /dev/null
+++ b/frontend/src/stores/rampSummary/index.ts
@@ -0,0 +1,11 @@
+import { create } from 'zustand';
+
+interface RampSummaryState {
+ isQuoteExpired: boolean;
+ setIsQuoteExpired: (expired: boolean) => void;
+}
+
+export const useRampSummaryStore = create((set) => ({
+ isQuoteExpired: false,
+ setIsQuoteExpired: (expired: boolean) => set({ isQuoteExpired: expired }),
+}));
From 63939d306d3ab7c9e9597d123ae2ca3367aae6bb Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 13:55:05 +0200
Subject: [PATCH 33/52] show BRL KYC form
---
frontend/src/pages/ramp-form/index.tsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/frontend/src/pages/ramp-form/index.tsx b/frontend/src/pages/ramp-form/index.tsx
index b501f9387..70fbde12f 100644
--- a/frontend/src/pages/ramp-form/index.tsx
+++ b/frontend/src/pages/ramp-form/index.tsx
@@ -1,5 +1,5 @@
import { SigningBox } from '../../components/SigningBox';
-import { OfframpSummaryDialog } from '../../components/OfframpSummaryDialog';
+import { RampSummaryDialog } from '../../components/RampSummaryDialog';
import { PIXKYCForm } from '../../components/BrlaComponents/BrlaExtendedForm';
import { Offramp } from '../../components/Ramp/Offramp';
import { motion } from 'motion/react';
@@ -18,7 +18,7 @@ export const RampForm = () => {
return (
-
+
{
>
- {/* {offrampKycStarted ? : } */}
- {activeSwapDirection === RampDirection.ONRAMP ? : }
+ {offrampKycStarted ? : activeSwapDirection === RampDirection.ONRAMP ? : }
From d5c004cbfd8acac5dcb2c66c2a6b062271eda8bd Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:12:06 +0200
Subject: [PATCH 34/52] fix translations BRLOnrampDetails
---
.../src/components/RampSummaryDialog/BRLOnrampDetails.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
index c778e2e91..92f0657b5 100644
--- a/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
+++ b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
@@ -17,17 +17,17 @@ export const BRLOnrampDetails: FC = () => {
return (
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.title')}
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.title')}
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.description')}
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.description')}
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.qrCode')}
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.qrCode')}
- {t('components.dialogs.OfframpSummaryDialog.BRLOnrampDetails.copyCode')}
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.copyCode')}
);
From 6c6afe3c135bcc0e046c19be2d27ebbb7add41eb Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:12:28 +0200
Subject: [PATCH 35/52] fix translations FeeDetails
---
frontend/src/components/RampSummaryDialog/FeeDetails.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/RampSummaryDialog/FeeDetails.tsx b/frontend/src/components/RampSummaryDialog/FeeDetails.tsx
index 79073105f..84b3685b6 100644
--- a/frontend/src/components/RampSummaryDialog/FeeDetails.tsx
+++ b/frontend/src/components/RampSummaryDialog/FeeDetails.tsx
@@ -41,8 +41,8 @@ export const FeeDetails: FC = ({
{isOfframp
- ? t('components.dialogs.OfframpSummaryDialog.offrampFee')
- : t('components.dialogs.OfframpSummaryDialog.onrampFee')}{' '}
+ ? t('components.dialogs.RampSummaryDialog.offrampFee')
+ : t('components.dialogs.RampSummaryDialog.onrampFee')}{' '}
({`${fiatToken.offrampFeesBasisPoints / 100}%`}
{fiatToken.offrampFeesFixedComponent ? ` + ${fiatToken.offrampFeesFixedComponent} ${fiatSymbol}` : ''})
@@ -54,7 +54,7 @@ export const FeeDetails: FC
= ({
-
{t('components.dialogs.OfframpSummaryDialog.quote')}
+
{t('components.dialogs.RampSummaryDialog.quote')}
= ({
-
{t('components.dialogs.OfframpSummaryDialog.partner')}
+
{t('components.dialogs.RampSummaryDialog.partner')}
{partnerUrl}
From 90d68630330818ccafd05dbbe5c69c5dcfaee3f2 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:12:42 +0200
Subject: [PATCH 36/52] fix translations TransactionTokensDisplay
---
.../components/RampSummaryDialog/TransactionTokensDisplay.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
index 3addfb592..1bee197f0 100644
--- a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -142,7 +142,9 @@ export const TransactionTokensDisplay: FC = ({
direction={rampDirection}
/>
- Quote expires in: {formattedTime}
+
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
+
);
};
From 975c8632d8e90c2002236cb3472850e9a6579a2d Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:12:56 +0200
Subject: [PATCH 37/52] fix translations RampSummaryButton
---
.../src/components/RampSummaryDialog/RampSummaryButton.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
index b5771557f..36f4d2bbc 100644
--- a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
+++ b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
@@ -50,7 +50,7 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
if (isOfframp && rampState !== undefined) {
return {
- text: t('components.dialogs.OfframpSummaryDialog.processing'),
+ text: t('components.dialogs.RampSummaryDialog.processing'),
icon: ,
};
}
@@ -65,12 +65,12 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
if (isOfframp && isAnchorWithRedirect) {
if (isSubmitted) {
return {
- text: t('components.dialogs.OfframpSummaryDialog.continueOnPartnersPage'),
+ text: t('components.dialogs.RampSummaryDialog.continueOnPartnersPage'),
icon: ,
};
} else {
return {
- text: t('components.dialogs.OfframpSummaryDialog.continueWithPartner'),
+ text: t('components.dialogs.RampSummaryDialog.continueWithPartner'),
icon: ,
};
}
From 908f8bbc463af404fc77a6c53a702a6c8b38c492 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:13:23 +0200
Subject: [PATCH 38/52] fix en translations for RampSummaryDialog
---
frontend/src/components/RampSubmitButtons/index.tsx | 6 +++---
frontend/src/components/RampSummaryDialog/index.tsx | 4 ++--
frontend/src/translations/en.json | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/frontend/src/components/RampSubmitButtons/index.tsx b/frontend/src/components/RampSubmitButtons/index.tsx
index 100ebb2bc..4a85a7196 100644
--- a/frontend/src/components/RampSubmitButtons/index.tsx
+++ b/frontend/src/components/RampSubmitButtons/index.tsx
@@ -20,7 +20,7 @@ export const RampSubmitButtons: FC = ({ toAmount }) => {
const { feeComparisonRef } = useFeeComparisonStore();
const { trackEvent } = useEventsContext();
const { getCurrentErrorMessage, initializeFailedMessage } = useRampValidation();
- const isOfframpSummaryDialogVisible = useRampSummaryVisible();
+ const isRampSummaryDialogVisible = useRampSummaryVisible();
const inputAmount = useInputAmount();
const fiatToken = useFiatToken();
const onChainToken = useOnChainToken();
@@ -45,14 +45,14 @@ export const RampSubmitButtons: FC = ({ toAmount }) => {
);
const getButtonState = (): string => {
- if (isOfframpSummaryDialogVisible) {
+ if (isRampSummaryDialogVisible) {
return t('components.swapSubmitButton.processing');
}
return t('components.swapSubmitButton.confirm');
};
const isSubmitButtonDisabled = Boolean(getCurrentErrorMessage()) || !toAmount || !!initializeFailedMessage;
- const isSubmitButtonPending = isOfframpSummaryDialogVisible;
+ const isSubmitButtonPending = isRampSummaryDialogVisible;
return (
diff --git a/frontend/src/components/RampSummaryDialog/index.tsx b/frontend/src/components/RampSummaryDialog/index.tsx
index 92890fb07..e5ca49e69 100644
--- a/frontend/src/components/RampSummaryDialog/index.tsx
+++ b/frontend/src/components/RampSummaryDialog/index.tsx
@@ -38,8 +38,8 @@ export const RampSummaryDialog: FC = () => {
};
const headerText = isOnramp
- ? t('components.dialogs.OfframpSummaryDialog.headerText.buy')
- : t('components.dialogs.OfframpSummaryDialog.headerText.sell');
+ ? t('components.dialogs.RampSummaryDialog.headerText.buy')
+ : t('components.dialogs.RampSummaryDialog.headerText.sell');
const actions =
;
const content = (
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 72a430d1e..e0a5b11d0 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -187,7 +187,7 @@
"placeholder": "Search"
},
"dialogs": {
- "OfframpSummaryDialog": {
+ "RampSummaryDialog": {
"BRLOnrampDetails": {
"title": "Pay with Pix",
"description": "Continue in your bank's app",
From 3786caf3638035f6b8fa5c897edbe62ab458d226 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:13:39 +0200
Subject: [PATCH 39/52] add pt translations for RampSummaryDialog
---
frontend/src/translations/pt.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index 7d08b0544..5e41e487c 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -187,7 +187,7 @@
"placeholder": "Pesquisar"
},
"dialogs": {
- "OfframpSummaryDialog": {
+ "RampSummaryDialog": {
"BRLOnrampDetails": {
"title": "Pagar com PIX",
"description": "Continue no app do seu banco",
From c9647cc71a12b187d6f289daafa39a7ebe17018f Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:16:31 +0200
Subject: [PATCH 40/52] hide Quote expires when is not defined
---
.../RampSummaryDialog/TransactionTokensDisplay.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
index 1bee197f0..188133b2e 100644
--- a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -143,7 +143,11 @@ export const TransactionTokensDisplay: FC = ({
/>
- {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
+ {rampState?.ramp?.createdAt && (
+ <>
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
+ >
+ )}
);
From c0b32c825e94c43cfc2034f75b1554c95a42dcd9 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 14:27:52 +0200
Subject: [PATCH 41/52] fix Expired Quote button text
---
.../RampSummaryDialog/RampSummaryButton.tsx | 12 ++++++------
.../RampSummaryDialog/TransactionTokensDisplay.tsx | 2 +-
frontend/src/translations/en.json | 2 +-
frontend/src/translations/pt.json | 2 +-
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
index 36f4d2bbc..589f29779 100644
--- a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
+++ b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
@@ -34,17 +34,17 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
const isAnchorWithRedirect = !isAnchorWithoutRedirect;
return useMemo(() => {
- if (submitButtonDisabled) {
+ if (isBRCodeReady && isQuoteExpired) {
return {
- text: t('components.swapSubmitButton.processing'),
- icon: ,
+ text: t('components.dialogs.RampSummaryDialog.quoteExpired'),
+ icon: null,
};
}
- if (isQuoteExpired) {
+ if (submitButtonDisabled) {
return {
- text: t('components.swapSubmitButton.quoteExpired'),
- icon: null,
+ text: t('components.swapSubmitButton.processing'),
+ icon: ,
};
}
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
index 188133b2e..bf9bf6e40 100644
--- a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -38,7 +38,7 @@ export const TransactionTokensDisplay: FC = ({
}) => {
const { t } = useTranslation();
const rampState = useRampState();
- const [timeLeft, setTimeLeft] = useState({ minutes: 0, seconds: 0 });
+ const [timeLeft, setTimeLeft] = useState({ minutes: ONRAMP_EXPIRY_MINUTES, seconds: 0 });
const { setIsQuoteExpired } = useRampSummaryStore();
useEffect(() => {
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index e0a5b11d0..4cf46831e 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -204,7 +204,7 @@
"onrampFee": "Onramp fee",
"continueWithPartner": "Continue with Partner",
"continueOnPartnersPage": "Continue on Partner's page",
- "quoteExpired": "Quote expired",
+ "quoteExpired": "Quote expired. Close dialog and try again.",
"processing": "Processing",
"continue": "Continue",
"quote": "Quote",
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index 5e41e487c..ba80e5992 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -204,7 +204,7 @@
"onrampFee": "Taxa de compra",
"continueWithPartner": "Continuar com Parceiro",
"continueOnPartnersPage": "Continuar na página do Parceiro",
- "quoteExpired": "Cotação expirada",
+ "quoteExpired": "Cotação expirada. Feche o diálogo e tente novamente.",
"processing": "Processando",
"continue": "Continuar",
"quote": "Cotação",
From 25120e2b789298acd679d3bbc61d987800933a75 Mon Sep 17 00:00:00 2001
From: Kacper Szarkiewicz
Date: Tue, 15 Apr 2025 15:34:54 +0200
Subject: [PATCH 42/52] handle canRegisterRamp and rename offrampStore to
rampStore
---
.../components/RampSubmitButtons/index.tsx | 2 +-
.../RampSummaryDialog/BRLOnrampDetails.tsx | 2 +-
.../RampSummaryDialog/RampSummaryButton.tsx | 56 +++++++++++--------
.../TransactionTokensDisplay.tsx | 6 +-
.../components/RampSummaryDialog/index.tsx | 2 +-
frontend/src/components/SigningBox/index.tsx | 2 +-
frontend/src/contexts/network.tsx | 2 +-
.../hooks/brla/useBRLAKYCProcess/index.tsx | 2 +-
.../offramp/useRampService/useRegisterRamp.ts | 25 +++++----
.../offramp/useRampService/useStartRamp.ts | 2 +-
.../useSEP24/useAnchorWindowHandler.ts | 2 +-
.../src/hooks/offramp/useSubmitOfframp.ts | 2 +-
frontend/src/hooks/ramp/useRampNavigation.ts | 2 +-
frontend/src/hooks/ramp/useRampSubmission.ts | 4 +-
frontend/src/hooks/useSignChallenge.ts | 2 +-
frontend/src/pages/failure/index.tsx | 12 ++--
frontend/src/pages/progress/index.tsx | 2 +-
frontend/src/pages/ramp-form/index.tsx | 2 +-
frontend/src/pages/success/index.tsx | 2 +-
.../stores/{offrampStore.ts => rampStore.ts} | 10 +++-
frontend/src/stores/rampSummary/index.ts | 11 +++-
frontend/src/translations/en.json | 3 +-
frontend/src/translations/pt.json | 3 +-
frontend/src/types/phases.ts | 2 +
24 files changed, 95 insertions(+), 65 deletions(-)
rename frontend/src/stores/{offrampStore.ts => rampStore.ts} (93%)
diff --git a/frontend/src/components/RampSubmitButtons/index.tsx b/frontend/src/components/RampSubmitButtons/index.tsx
index 4a85a7196..0ed37ac86 100644
--- a/frontend/src/components/RampSubmitButtons/index.tsx
+++ b/frontend/src/components/RampSubmitButtons/index.tsx
@@ -2,7 +2,7 @@ import { FC, useCallback } from 'react';
import Big from 'big.js';
import { useEventsContext } from '../../contexts/events';
import { useFeeComparisonStore } from '../../stores/feeComparison';
-import { useRampSummaryVisible } from '../../stores/offrampStore';
+import { useRampSummaryVisible } from '../../stores/rampStore';
import { useRampValidation } from '../../hooks/ramp/useRampValidation';
import { SwapSubmitButton } from '../buttons/SwapSubmitButton';
import { useFiatToken, useInputAmount, useOnChainToken } from '../../stores/ramp/useRampFormStore';
diff --git a/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
index 92f0657b5..8aae933d9 100644
--- a/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
+++ b/frontend/src/components/RampSummaryDialog/BRLOnrampDetails.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { QRCodeSVG } from 'qrcode.react';
import { CopyButton } from '../CopyButton';
import { useRampDirection } from '../../stores/rampDirectionStore';
-import { useRampState } from '../../stores/offrampStore';
+import { useRampState } from '../../stores/rampStore';
import { RampDirection } from '../RampToggle';
export const BRLOnrampDetails: FC = () => {
diff --git a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
index 589f29779..72695d56f 100644
--- a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
+++ b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
@@ -1,12 +1,18 @@
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
-import { FiatTokenDetails, getAnyFiatTokenDetails, getOnChainTokenDetailsOrDefault, TokenType } from 'shared';
-import { useRampExecutionInput, useRampState } from '../../stores/offrampStore';
+import {
+ FiatToken,
+ FiatTokenDetails,
+ getAnyFiatTokenDetails,
+ getOnChainTokenDetailsOrDefault,
+ TokenType,
+} from 'shared';
+import { useCanRegisterRamp, useRampExecutionInput, useRampState } from '../../stores/rampStore';
import { useRampDirection } from '../../stores/rampDirectionStore';
import { RampDirection } from '../RampToggle';
-import { useRampSummaryStore } from '../../stores/rampSummary';
-import { useRampActions } from '../../stores/offrampStore';
+import { useIsQuoteExpired, useRampSummaryStore } from '../../stores/rampSummary';
+import { useRampActions } from '../../stores/rampStore';
import { useRampSubmission } from '../../hooks/ramp/useRampSubmission';
import { useSep24StoreCachedAnchorUrl } from '../../stores/sep24Store';
import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
@@ -23,17 +29,18 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
const rampState = useRampState();
const { t } = useTranslation();
const rampDirection = useRampDirection();
- const { isQuoteExpired } = useRampSummaryStore();
+ const isQuoteExpired = useIsQuoteExpired();
+ const canRegisterRamp = useCanRegisterRamp();
- const isOnramp = rampDirection === RampDirection.ONRAMP;
- const isOfframp = rampDirection === RampDirection.OFFRAMP;
- const isBRCodeReady = Boolean(rampState?.ramp?.brCode);
+ return useMemo(() => {
+ const isOnramp = rampDirection === RampDirection.ONRAMP;
+ const isOfframp = rampDirection === RampDirection.OFFRAMP;
+ const isBRCodeReady = Boolean(rampState?.ramp?.brCode);
- // BRL offramp has no redirect, it is the only with type moonbeam
- const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
- const isAnchorWithRedirect = !isAnchorWithoutRedirect;
+ // BRL offramp has no redirect, it is the only with type moonbeam
+ const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
+ const isAnchorWithRedirect = !isAnchorWithoutRedirect;
- return useMemo(() => {
if (isBRCodeReady && isQuoteExpired) {
return {
text: t('components.dialogs.RampSummaryDialog.quoteExpired'),
@@ -48,6 +55,13 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
};
}
+ if (isOfframp && isAnchorWithoutRedirect && !canRegisterRamp) {
+ return {
+ text: t('components.dialogs.RampSummaryDialog.confirm'),
+ icon: null,
+ };
+ }
+
if (isOfframp && rampState !== undefined) {
return {
text: t('components.dialogs.RampSummaryDialog.processing'),
@@ -80,17 +94,7 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
text: t('components.swapSubmitButton.processing'),
icon: ,
};
- }, [
- submitButtonDisabled,
- isQuoteExpired,
- isOfframp,
- rampState,
- isOnramp,
- isBRCodeReady,
- isAnchorWithRedirect,
- t,
- isSubmitted,
- ]);
+ }, [submitButtonDisabled, isQuoteExpired, rampDirection, rampState, t, isSubmitted, canRegisterRamp, toToken]);
};
export const RampSummaryButton = () => {
@@ -107,6 +111,7 @@ export const RampSummaryButton = () => {
const onChainToken = useOnChainToken();
const { selectedNetwork } = useNetwork();
const executionInput = useRampExecutionInput();
+ const { setCanRegisterRamp } = useRampActions();
const toToken = isOnramp
? getOnChainTokenDetailsOrDefault(selectedNetwork, onChainToken)
@@ -136,6 +141,11 @@ export const RampSummaryButton = () => {
const onSubmit = () => {
setIsSubmitted(true);
+ // For BRL offramps, set canRegisterRamp to true
+ if (isOfframp && fiatToken === FiatToken.BRL && executionInput?.quote.rampType === 'off') {
+ setCanRegisterRamp(true);
+ }
+
if (executionInput?.quote.rampType === 'on') {
setRampPaymentConfirmed(true);
} else {
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
index bf9bf6e40..de1520eb3 100644
--- a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -12,10 +12,10 @@ import {
Networks,
} from 'shared';
import { useGetAssetIcon } from '../../hooks/useGetAssetIcon';
-import { useRampState } from '../../stores/offrampStore';
+import { useRampState } from '../../stores/rampStore';
import { RampDirection } from '../RampToggle';
import { RampExecutionInput } from '../../types/phases';
-import { useRampSummaryStore } from '../../stores/rampSummary';
+import { useRampSummaryActions } from '../../stores/rampSummary';
import { AssetDisplay } from './AssetDisplay';
import { FeeDetails } from './FeeDetails';
import { BRLOnrampDetails } from './BRLOnrampDetails';
@@ -39,7 +39,7 @@ export const TransactionTokensDisplay: FC = ({
const { t } = useTranslation();
const rampState = useRampState();
const [timeLeft, setTimeLeft] = useState({ minutes: ONRAMP_EXPIRY_MINUTES, seconds: 0 });
- const { setIsQuoteExpired } = useRampSummaryStore();
+ const { setIsQuoteExpired } = useRampSummaryActions();
useEffect(() => {
let targetTimestamp: number | null = null;
diff --git a/frontend/src/components/RampSummaryDialog/index.tsx b/frontend/src/components/RampSummaryDialog/index.tsx
index e5ca49e69..468f6a7ea 100644
--- a/frontend/src/components/RampSummaryDialog/index.tsx
+++ b/frontend/src/components/RampSummaryDialog/index.tsx
@@ -2,7 +2,7 @@ import { FC } from 'react';
import Big from 'big.js';
import { useTranslation } from 'react-i18next';
import { Dialog } from '../Dialog';
-import { useRampActions, useRampExecutionInput, useRampSummaryVisible } from '../../stores/offrampStore';
+import { useRampActions, useRampExecutionInput, useRampSummaryVisible } from '../../stores/rampStore';
import { useFiatToken, useOnChainToken } from '../../stores/ramp/useRampFormStore';
import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
import { useNetwork } from '../../contexts/network';
diff --git a/frontend/src/components/SigningBox/index.tsx b/frontend/src/components/SigningBox/index.tsx
index 4624ceb16..043752e24 100644
--- a/frontend/src/components/SigningBox/index.tsx
+++ b/frontend/src/components/SigningBox/index.tsx
@@ -7,7 +7,7 @@ import { isNetworkEVM } from 'shared';
import { useNetwork } from '../../contexts/network';
import { Spinner } from '../Spinner';
import { RampSigningPhase } from '../../types/phases';
-import { useRampSigningPhase } from '../../stores/offrampStore';
+import { useRampSigningPhase } from '../../stores/rampStore';
import { useTranslation } from 'react-i18next';
type ProgressConfig = {
diff --git a/frontend/src/contexts/network.tsx b/frontend/src/contexts/network.tsx
index 2e098c1f1..83844c8a4 100644
--- a/frontend/src/contexts/network.tsx
+++ b/frontend/src/contexts/network.tsx
@@ -2,7 +2,7 @@ import { createContext, ReactNode, useContext, useState, useEffect, useCallback
import { useAccount, useSwitchChain } from 'wagmi';
import { useLocalStorage, LocalStorageKeys } from '../hooks/useLocalStorage';
import { WALLETCONNECT_ASSETHUB_ID } from '../constants/constants';
-import { useRampActions } from '../stores/offrampStore';
+import { useRampActions } from '../stores/rampStore';
import { getNetworkId, isNetworkEVM, Networks } from 'shared';
import { useSep24Actions } from '../stores/sep24Store';
diff --git a/frontend/src/hooks/brla/useBRLAKYCProcess/index.tsx b/frontend/src/hooks/brla/useBRLAKYCProcess/index.tsx
index ed00e6f1f..ca2b17cb6 100644
--- a/frontend/src/hooks/brla/useBRLAKYCProcess/index.tsx
+++ b/frontend/src/hooks/brla/useBRLAKYCProcess/index.tsx
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
-import { useRampActions } from '../../../stores/offrampStore';
+import { useRampActions } from '../../../stores/rampStore';
import { useKycStatusQuery } from '../useKYCStatusQuery';
import { KYCFormData } from '../useKYCForm';
import { createSubaccount, KycStatus } from '../../../services/signingService';
diff --git a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
index c23cb2a0e..1cebc1122 100644
--- a/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
@@ -1,4 +1,4 @@
-import { useRampExecutionInput, useRampStore } from '../../../stores/offrampStore';
+import { useRampExecutionInput, useRampStore } from '../../../stores/rampStore';
import { useVortexAccount } from '../../useVortexAccount';
import { RampService } from '../../../services/api';
import { AccountMeta, FiatToken, getAddressForFormat, Networks, signUnsignedTransactions } from 'shared';
@@ -63,7 +63,8 @@ export const useRegisterRamp = () => {
rampRegistered,
rampState,
rampStarted,
- actions: { setRampRegistered, setRampState, setRampSigningPhase },
+ canRegisterRamp,
+ actions: { setRampRegistered, setRampState, setRampSigningPhase, setCanRegisterRamp },
} = useRampStore();
const { address } = useVortexAccount();
@@ -77,8 +78,6 @@ export const useRegisterRamp = () => {
const prepareOfframpSubmission = useSubmitOfframp();
const handleOnAnchorWindowOpen = useAnchorWindowHandler();
- // This flag is used to track if the user signaled to start the ramp process
- const [canRegisterRamp, setCanRegisterRamp] = useState(false);
// TODO if user declined signing, do something
const [userDeclinedSigning, setUserDeclinedSigning] = useState(false);
@@ -92,17 +91,17 @@ export const useRegisterRamp = () => {
if (executionInput.quote.rampType === 'off' && executionInput.fiatToken !== FiatToken.BRL) {
console.log('Registering ramp for Stellar offramps');
await handleOnAnchorWindowOpen();
- } else if (executionInput.quote.rampType === 'off' && executionInput.fiatToken === FiatToken.BRL) {
- // TODO Implement waiting for user input (the ramp summary dialog should show the 'Confirm' button and once clicked,
- // setCanRegisterRamp to true
}
- // For other ramps, we can continue registering right away
- setCanRegisterRamp(true);
+ if (executionInput.quote.rampType === 'off' && executionInput.fiatToken === FiatToken.BRL) {
+ // Waiting for user input (the ramp summary dialog should show the 'Confirm' button and once clicked,
+ // We setCanRegisterRamp to true inside of the RampSummaryButton
+ } else {
+ // For other ramps, we can continue registering right away
+ setCanRegisterRamp(true);
+ }
};
- console.log('canRegisterRamp', canRegisterRamp);
-
const { checkLock, verifyLock, releaseLock } = useProcessLock(REGISTER_KEY_LOCAL_STORAGE);
// @TODO: maybe change to useCallback
@@ -120,6 +119,10 @@ export const useRegisterRamp = () => {
console.log('processRef', processRef);
const registerRampProcess = async () => {
+ if (!canRegisterRamp) {
+ throw new Error('Cannot proceed with ramp registration, canRegisterRamp is false');
+ }
+
// Verify we still own the lock before proceeding
if (!verifyLock(processRef)) {
console.log('In registerRampProcess, lock is not valid anymore for processRef', processRef);
diff --git a/frontend/src/hooks/offramp/useRampService/useStartRamp.ts b/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
index 7f5060cd1..475ac1aea 100644
--- a/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
+++ b/frontend/src/hooks/offramp/useRampService/useStartRamp.ts
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
-import { useRampStore } from '../../../stores/offrampStore';
+import { useRampStore } from '../../../stores/rampStore';
import { RampService } from '../../../services/api';
export const useStartRamp = () => {
diff --git a/frontend/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts b/frontend/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts
index 53ed4314a..07f2a7d0e 100644
--- a/frontend/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts
+++ b/frontend/src/hooks/offramp/useSEP24/useAnchorWindowHandler.ts
@@ -8,7 +8,7 @@ import { sep24Second } from '../../../services/anchor/sep24/second';
import { useTrackSEP24Events } from './useTrackSEP24Events';
import { usePendulumNode } from '../../../contexts/polkadotNode';
-import { useRampActions, useRampStore } from '../../../stores/offrampStore';
+import { useRampActions, useRampStore } from '../../../stores/rampStore';
import { useSep24AnchorSessionParams, useSep24InitialResponse } from '../../../stores/sep24Store';
import { useVortexAccount } from '../../useVortexAccount';
import { useToastMessage } from '../../../helpers/notifications';
diff --git a/frontend/src/hooks/offramp/useSubmitOfframp.ts b/frontend/src/hooks/offramp/useSubmitOfframp.ts
index 41c4f9ae9..26fdb9663 100644
--- a/frontend/src/hooks/offramp/useSubmitOfframp.ts
+++ b/frontend/src/hooks/offramp/useSubmitOfframp.ts
@@ -10,7 +10,7 @@ import { FiatToken, getAnyFiatTokenDetails, getOnChainTokenDetailsOrDefault, get
import { fetchTomlValues } from '../../services/stellar';
import { sep24First } from '../../services/anchor/sep24/first';
import { sep10 } from '../../services/anchor/sep10';
-import { useRampActions } from '../../stores/offrampStore';
+import { useRampActions } from '../../stores/rampStore';
import { useSep24Actions } from '../../stores/sep24Store';
import { SIGNING_SERVICE_URL } from '../../constants/constants';
import { RampExecutionInput } from '../../types/phases';
diff --git a/frontend/src/hooks/ramp/useRampNavigation.ts b/frontend/src/hooks/ramp/useRampNavigation.ts
index e46dcd120..6b8ba6e7c 100644
--- a/frontend/src/hooks/ramp/useRampNavigation.ts
+++ b/frontend/src/hooks/ramp/useRampNavigation.ts
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { ReactNode } from 'react';
-import { useRampState, useRampKycStarted, useRampStarted } from '../../stores/offrampStore';
+import { useRampState, useRampKycStarted, useRampStarted } from '../../stores/rampStore';
export const useRampNavigation = (
successComponent: ReactNode,
diff --git a/frontend/src/hooks/ramp/useRampSubmission.ts b/frontend/src/hooks/ramp/useRampSubmission.ts
index acb32c7bc..baf71746e 100644
--- a/frontend/src/hooks/ramp/useRampSubmission.ts
+++ b/frontend/src/hooks/ramp/useRampSubmission.ts
@@ -4,7 +4,7 @@ import { useRampFormStore } from '../../stores/ramp/useRampFormStore';
import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
import { useVortexAccount } from '../useVortexAccount';
import { useNetwork } from '../../contexts/network';
-import { useRampActions } from '../../stores/offrampStore';
+import { useRampActions } from '../../stores/rampStore';
import { useEventsContext } from '../../contexts/events';
import {
createMoonbeamEphemeral,
@@ -141,7 +141,7 @@ export const useRampSubmission = () => {
onRampConfirm,
isExecutionPreparing: executionPreparing,
finishOfframping: () => {
- resetRampState()
+ resetRampState();
},
validateSubmissionData,
};
diff --git a/frontend/src/hooks/useSignChallenge.ts b/frontend/src/hooks/useSignChallenge.ts
index 7b4335f87..bf2f04f28 100644
--- a/frontend/src/hooks/useSignChallenge.ts
+++ b/frontend/src/hooks/useSignChallenge.ts
@@ -5,7 +5,7 @@ import { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } from '../constants/constants';
import { SIGNING_SERVICE_URL } from '../constants/constants';
import { storageKeys } from '../constants/localStorage';
import { useVortexAccount } from './useVortexAccount';
-import { useRampActions } from '../stores/offrampStore';
+import { useRampActions } from '../stores/rampStore';
import { useEffect } from 'react';
export interface SiweSignatureData {
diff --git a/frontend/src/pages/failure/index.tsx b/frontend/src/pages/failure/index.tsx
index 5232375f5..4e11f6ef8 100644
--- a/frontend/src/pages/failure/index.tsx
+++ b/frontend/src/pages/failure/index.tsx
@@ -5,7 +5,7 @@ import { Box } from '../../components/Box';
import { EmailForm } from '../../components/EmailForm';
import { config } from '../../config';
import { useRampSubmission } from '../../hooks/ramp/useRampSubmission';
-import { useRampState } from '../../stores/offrampStore';
+import { useRampState } from '../../stores/rampStore';
const ErrorIcon = () => (
@@ -24,19 +24,19 @@ export const FailurePage = () => {
{t('pages.failure.title')}
-
+
{t('pages.failure.recoverable.description')}
{t('pages.failure.recoverable.cta')}
-
+
-
+
diff --git a/frontend/src/pages/progress/index.tsx b/frontend/src/pages/progress/index.tsx
index 944a3f182..f5cd8a340 100644
--- a/frontend/src/pages/progress/index.tsx
+++ b/frontend/src/pages/progress/index.tsx
@@ -8,7 +8,7 @@ import { useEventsContext } from '../../contexts/events';
import { useNetwork } from '../../contexts/network';
import { isNetworkEVM, RampPhase } from 'shared';
import { GotQuestions } from '../../sections';
-import { useRampActions, useRampState, useRampStore } from '../../stores/offrampStore';
+import { useRampActions, useRampState, useRampStore } from '../../stores/rampStore';
import { RampService } from '../../services/api';
import { getMessageForPhase } from './phaseMessages';
import { config } from '../../config';
diff --git a/frontend/src/pages/ramp-form/index.tsx b/frontend/src/pages/ramp-form/index.tsx
index 70fbde12f..0439c5b5a 100644
--- a/frontend/src/pages/ramp-form/index.tsx
+++ b/frontend/src/pages/ramp-form/index.tsx
@@ -3,7 +3,7 @@ import { RampSummaryDialog } from '../../components/RampSummaryDialog';
import { PIXKYCForm } from '../../components/BrlaComponents/BrlaExtendedForm';
import { Offramp } from '../../components/Ramp/Offramp';
import { motion } from 'motion/react';
-import { useRampKycStarted } from '../../stores/offrampStore';
+import { useRampKycStarted } from '../../stores/rampStore';
import { PoolSelectorModal } from '../../components/InputKeys/SelectionModal';
import { useRampDirection, useRampDirectionToggle } from '../../stores/rampDirectionStore';
import { RampDirection, RampToggle } from '../../components/RampToggle';
diff --git a/frontend/src/pages/success/index.tsx b/frontend/src/pages/success/index.tsx
index c658000a7..8a813bb7b 100644
--- a/frontend/src/pages/success/index.tsx
+++ b/frontend/src/pages/success/index.tsx
@@ -6,7 +6,7 @@ import { EmailForm } from '../../components/EmailForm';
import { Rating } from '../../components/Rating';
import { FiatToken } from 'shared';
import { useRampSubmission } from '../../hooks/ramp/useRampSubmission';
-import { useRampExecutionInput } from '../../stores/offrampStore';
+import { useRampExecutionInput } from '../../stores/rampStore';
import { useRampFormStore } from '../../stores/ramp/useRampFormStore';
const Checkmark = () => (
diff --git a/frontend/src/stores/offrampStore.ts b/frontend/src/stores/rampStore.ts
similarity index 93%
rename from frontend/src/stores/offrampStore.ts
rename to frontend/src/stores/rampStore.ts
index e0d65c43c..31ce27a95 100644
--- a/frontend/src/stores/offrampStore.ts
+++ b/frontend/src/stores/rampStore.ts
@@ -30,6 +30,7 @@ export const useRampStore = create
()((set, get) => {
rampExecutionInput: undefined,
rampSummaryVisible: false,
initializeFailedMessage: undefined,
+ canRegisterRamp: false,
...loadInitialState(),
};
@@ -46,6 +47,7 @@ export const useRampStore = create()((set, get) => {
rampSigningPhase: state.rampSigningPhase,
rampExecutionInput: state.rampExecutionInput,
rampSummaryVisible: state.rampSummaryVisible,
+ canRegisterRamp: state.canRegisterRamp,
};
storageService.set(LocalStorageKeys.RAMPING_STATE, stateToSave);
};
@@ -97,7 +99,10 @@ export const useRampStore = create()((set, get) => {
set({ initializeFailedMessage: displayMessage });
saveState();
},
-
+ setCanRegisterRamp: (canRegister: boolean) => {
+ set({ canRegisterRamp: canRegister });
+ saveState();
+ },
resetRampState: () => {
clearRampingState();
@@ -112,10 +117,10 @@ export const useRampStore = create()((set, get) => {
rampExecutionInput: undefined,
rampSummaryVisible: false,
initializeFailedMessage: undefined,
+ canRegisterRamp: false,
});
// No need to save state here as we just cleared it
},
-
clearInitializeFailedMessage: () => {
set({ initializeFailedMessage: undefined });
saveState();
@@ -134,6 +139,7 @@ export const useRampKycStarted = () => useRampStore((state) => state.rampKycStar
export const useRampPaymentConfirmed = () => useRampStore((state) => state.rampPaymentConfirmed);
export const useInitializeFailedMessage = () => useRampStore((state) => state.initializeFailedMessage);
export const useRampSummaryVisible = () => useRampStore((state) => state.rampSummaryVisible);
+export const useCanRegisterRamp = () => useRampStore((state) => state.canRegisterRamp);
export const clearInitializeFailedMessage = () => useRampStore.getState().actions.clearInitializeFailedMessage();
export const useRampActions = () => useRampStore((state) => state.actions);
diff --git a/frontend/src/stores/rampSummary/index.ts b/frontend/src/stores/rampSummary/index.ts
index bc33fbff9..fc673e109 100644
--- a/frontend/src/stores/rampSummary/index.ts
+++ b/frontend/src/stores/rampSummary/index.ts
@@ -2,10 +2,17 @@ import { create } from 'zustand';
interface RampSummaryState {
isQuoteExpired: boolean;
- setIsQuoteExpired: (expired: boolean) => void;
+ actions: {
+ setIsQuoteExpired: (expired: boolean) => void;
+ };
}
export const useRampSummaryStore = create((set) => ({
isQuoteExpired: false,
- setIsQuoteExpired: (expired: boolean) => set({ isQuoteExpired: expired }),
+ actions: {
+ setIsQuoteExpired: (expired: boolean) => set({ isQuoteExpired: expired }),
+ },
}));
+
+export const useIsQuoteExpired = () => useRampSummaryStore((state) => state.isQuoteExpired);
+export const useRampSummaryActions = () => useRampSummaryStore((state) => state.actions);
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index 4cf46831e..e435cb564 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -208,7 +208,8 @@
"processing": "Processing",
"continue": "Continue",
"quote": "Quote",
- "partner": "Partner"
+ "partner": "Partner",
+ "confirm": "Confirm"
},
"selectionModal": {
"title": "Select a token",
diff --git a/frontend/src/translations/pt.json b/frontend/src/translations/pt.json
index ba80e5992..e5c6d4e22 100644
--- a/frontend/src/translations/pt.json
+++ b/frontend/src/translations/pt.json
@@ -208,7 +208,8 @@
"processing": "Processando",
"continue": "Continuar",
"quote": "Cotação",
- "partner": "Parceiro"
+ "partner": "Parceiro",
+ "confirm": "Confirmar"
},
"selectionModal": {
"title": "Selecione um token",
diff --git a/frontend/src/types/phases.ts b/frontend/src/types/phases.ts
index 327303a30..071835edd 100644
--- a/frontend/src/types/phases.ts
+++ b/frontend/src/types/phases.ts
@@ -42,6 +42,7 @@ export interface RampZustand {
rampPaymentConfirmed: boolean;
initializeFailedMessage: string | undefined;
rampSummaryVisible: boolean;
+ canRegisterRamp: boolean;
}
export interface RampActions {
@@ -57,4 +58,5 @@ export interface RampActions {
setRampSummaryVisible: (visible: boolean) => void;
clearInitializeFailedMessage: () => void;
resetRampState: () => void;
+ setCanRegisterRamp: (canRegister: boolean) => void;
}
From 73ed4d5a506efdc43203bccdd6b768ce9fdd94c2 Mon Sep 17 00:00:00 2001
From: Gianfranco
Date: Tue, 15 Apr 2025 11:25:15 -0300
Subject: [PATCH 43/52] add handler for assethub onramp
---
.gitignore | 6 +--
.../pendulum-to-assethub-phase-handler.ts | 49 +++++++++++++++++++
.../api/services/phases/meta-state-types.ts | 1 +
.../phase-processor.integration.test.ts | 15 +-----
...phase-processor.onramp.integration.test.ts | 14 +++---
.../api/services/phases/register-handlers.ts | 2 +
api/src/api/services/ramp/quote.service.ts | 12 +++++
.../transactions/onrampTransactions.ts | 7 +--
.../transactions/xcm/pendulumToAssethub.ts | 5 +-
api/src/database/migrator.ts | 2 +-
10 files changed, 83 insertions(+), 30 deletions(-)
create mode 100644 api/src/api/services/phases/handlers/pendulum-to-assethub-phase-handler.ts
diff --git a/.gitignore b/.gitignore
index 601de9aa1..1759a0e6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,7 @@ dist-ssr
.env.sentry-build-plugin
# testing artifacts
-*/failedRampStateRecovery.json
-*/lastRampState.json
-*/lastRampStateOnramp.json
+**/failedRampStateRecovery.json
+**/lastRampState.json
+**/lastRampStateOnramp.json
diff --git a/api/src/api/services/phases/handlers/pendulum-to-assethub-phase-handler.ts b/api/src/api/services/phases/handlers/pendulum-to-assethub-phase-handler.ts
new file mode 100644
index 000000000..9b31c78ab
--- /dev/null
+++ b/api/src/api/services/phases/handlers/pendulum-to-assethub-phase-handler.ts
@@ -0,0 +1,49 @@
+import { RampPhase, decodeSubmittableExtrinsic, getAddressForFormat } from 'shared';
+import { BasePhaseHandler } from '../base-phase-handler';
+import RampState from '../../../../models/rampState.model';
+
+import { submitXTokens } from '../../xcm/send';
+
+import { ApiManager } from '../../pendulum/apiManager';
+import { StateMetadata } from '../meta-state-types';
+
+export class PendulumToAssethubXCMPhaseHandler extends BasePhaseHandler {
+ public getPhaseName(): RampPhase {
+ return 'pendulumToAssethub';
+ }
+
+ protected async executePhase(state: RampState): Promise {
+ const apiManager = ApiManager.getInstance();
+ const networkName = 'pendulum';
+ const pendulumNode = await apiManager.getApi(networkName);
+
+ const { pendulumEphemeralAddress } = state.state as StateMetadata;
+
+ if (!pendulumEphemeralAddress) {
+ throw new Error('Pendulum ephemeral address is not defined in the state. This is a bug.');
+ }
+
+ try {
+ const { txData: pendulumToAssethubTransaction } = this.getPresignedTransaction(state, 'pendulumToAssethub');
+
+ const xcmExtrinsic = decodeSubmittableExtrinsic(pendulumToAssethubTransaction as string, pendulumNode.api);
+ const { hash } = await submitXTokens(
+ getAddressForFormat(pendulumEphemeralAddress, pendulumNode.ss58Format),
+ xcmExtrinsic,
+ );
+
+ state.state = {
+ ...state.state,
+ pendulumToAssethubXcmHash: hash,
+ };
+ await state.update({ state: state.state });
+
+ return this.transitionToNextPhase(state, 'complete');
+ } catch (e) {
+ console.error('Error in PendulumToAssethubPhase:', e);
+ throw e;
+ }
+ }
+}
+
+export default new PendulumToAssethubXCMPhaseHandler();
diff --git a/api/src/api/services/phases/meta-state-types.ts b/api/src/api/services/phases/meta-state-types.ts
index be3db08b3..1a8bfbe2e 100644
--- a/api/src/api/services/phases/meta-state-types.ts
+++ b/api/src/api/services/phases/meta-state-types.ts
@@ -25,4 +25,5 @@ export interface StateMetadata {
destinationAddress: string;
receiverTaxId: string;
moonbeamEphemeralAddress: string;
+ pendulumToAssethubXcmHash: string;
}
diff --git a/api/src/api/services/phases/phase-processor.integration.test.ts b/api/src/api/services/phases/phase-processor.integration.test.ts
index 7a48613e9..c95ca898e 100644
--- a/api/src/api/services/phases/phase-processor.integration.test.ts
+++ b/api/src/api/services/phases/phase-processor.integration.test.ts
@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-unresolved
-import { describe, expect, it, mock, beforeAll, afterAll } from 'bun:test';
+import { describe, it, mock } from 'bun:test';
import fs from 'node:fs';
import path from 'node:path';
import { PhaseProcessor } from './phase-processor';
@@ -7,19 +7,8 @@ import RampState from '../../../models/rampState.model';
import QuoteTicket from '../../../models/quoteTicket.model';
import { RampService } from '../ramp/ramp.service';
import { BrlaApiService } from '../brla/brlaApiService';
-import {
- AccountMeta,
- Networks,
- RampEndpoints,
- EvmToken,
- FiatToken,
- signUnsignedTransactions,
- EvmTransactionData,
- getNetworkId,
-} from 'shared';
-import { v4 as uuidv4 } from 'uuid';
+import { AccountMeta, Networks, EvmToken, FiatToken, signUnsignedTransactions, EvmTransactionData } from 'shared';
import { SubaccountData } from '../brla/types';
-import { APIError } from '../../errors/api-error';
import { QuoteService } from '../ramp/quote.service';
import { EphemeralAccount } from 'shared';
import { Keyring } from '@polkadot/api';
diff --git a/api/src/api/services/phases/phase-processor.onramp.integration.test.ts b/api/src/api/services/phases/phase-processor.onramp.integration.test.ts
index eec0f0da2..f7f294767 100644
--- a/api/src/api/services/phases/phase-processor.onramp.integration.test.ts
+++ b/api/src/api/services/phases/phase-processor.onramp.integration.test.ts
@@ -23,13 +23,13 @@ const TAX_ID = process.env.TAX_ID;
// BACKEND_TEST_STARTER_ACCOUNT = "sleep...... al"
// This is the derivation obtained using mnemonicToSeedSync(BACKEND_TEST_STARTER_ACCOUNT!) and HDKey.fromMasterSeed(seed)
const EVM_TESTING_ADDRESS = '0x30a300612ab372CC73e53ffE87fB73d62Ed68Da3';
-const EVM_DESTINATION_ADDRESS = '0x7ba99e99bc669b3508aff9cc0a898e869459f877'; // Controlled by us, so funds can arrive here during tests.
+const EVM_DESTINATION_ADDRESS = '12mkWe8Lfsk4Qx6EEocvRDpzmA6SQQHBA4Fq3b9T9cyPr7Td'; // Controlled by us, so funds can arrive here during tests.
const TEST_INPUT_AMOUNT = '1';
const TEST_INPUT_CURRENCY = FiatToken.BRL;
const TEST_OUTPUT_CURRENCY = EvmToken.USDC;
-const QUOTE_TO = 'polygon';
+const QUOTE_TO = 'assethub';
const QUOTE_FROM = 'pix';
const filePath = path.join(__dirname, 'lastRampStateOnramp.json');
@@ -210,13 +210,13 @@ describe('Onramp PhaseProcessor Integration Test', () => {
// END - MIMIC THE UI
- // const startedRamp = await rampService.startRamp({ rampId: registeredRamp.id, presignedTxs });
+ const startedRamp = await rampService.startRamp({ rampId: registeredRamp.id, presignedTxs });
- // const finalRampState = await waitForCompleteRamp(registeredRamp.id);
+ const finalRampState = await waitForCompleteRamp(registeredRamp.id);
- // // Some sanity checks.
- // expect(finalRampState.currentPhase).toBe('complete');
- // expect(finalRampState.phaseHistory.length).toBeGreaterThan(1);
+ // Some sanity checks.
+ expect(finalRampState.currentPhase).toBe('complete');
+ expect(finalRampState.phaseHistory.length).toBeGreaterThan(1);
} catch (error) {
console.error('Error during test execution:', error);
fs.writeFileSync(filePath, JSON.stringify(rampState, null, 2));
diff --git a/api/src/api/services/phases/register-handlers.ts b/api/src/api/services/phases/register-handlers.ts
index 134230d1f..356e6ce4e 100644
--- a/api/src/api/services/phases/register-handlers.ts
+++ b/api/src/api/services/phases/register-handlers.ts
@@ -15,6 +15,7 @@ import moonbeamToPendulumXcmHandler from './handlers/moonbeam-to-pendulum-xcm-ha
import fundEphemeralHandler from './handlers/fund-ephemeral-handler';
import brlaTeleportHandler from './handlers/brla-teleport-handler';
import completePhaseHandler from './handlers/complete-phase-handler';
+import pendulumToAssethubPhaseHandler from './handlers/pendulum-to-assethub-phase-handler';
/**
* Register all phase handlers
*/
@@ -37,6 +38,7 @@ export function registerPhaseHandlers(): void {
phaseRegistry.registerHandler(moonbeamToPendulumXcmHandler);
phaseRegistry.registerHandler(fundEphemeralHandler);
phaseRegistry.registerHandler(brlaTeleportHandler);
+ phaseRegistry.registerHandler(pendulumToAssethubPhaseHandler);
logger.info('Phase handlers registered');
}
diff --git a/api/src/api/services/ramp/quote.service.ts b/api/src/api/services/ramp/quote.service.ts
index 6af68f45d..9fb8c3133 100644
--- a/api/src/api/services/ramp/quote.service.ts
+++ b/api/src/api/services/ramp/quote.service.ts
@@ -224,7 +224,19 @@ export class QuoteService extends BaseRampService {
// if onramp, adjust for axlUSDC price difference.
const outputAmountMoonbeamRaw: string = amountOut.preciseQuotedAmountOut.rawBalance.toFixed(); // Store the value before the adjustment.
+<<<<<<< Updated upstream
if (rampType === 'on') {
+=======
+ if (rampType === 'on' && to !== 'assethub') {
+ const outTokenDetails = getOnChainTokenDetails(getNetworkFromDestination(to)!, outputCurrency as OnChainToken);
+ if (!outTokenDetails || !isEvmTokenDetails(outTokenDetails)) {
+ throw new APIError({
+ status: httpStatus.BAD_REQUEST,
+ message: 'Invalid token details for onramp',
+ });
+ }
+
+>>>>>>> Stashed changes
const routeParams = createOnrampRouteParams(
'0x30a300612ab372cc73e53ffe87fb73d62ed68da3', // It does not matter.
amountOut.preciseQuotedAmountOut.rawBalance.toFixed(),
diff --git a/api/src/api/services/transactions/onrampTransactions.ts b/api/src/api/services/transactions/onrampTransactions.ts
index c56003bc3..1c5226763 100644
--- a/api/src/api/services/transactions/onrampTransactions.ts
+++ b/api/src/api/services/transactions/onrampTransactions.ts
@@ -76,15 +76,12 @@ export async function prepareOnrampTransactions(
}
// For BRLA, fee is charged after minting, so we always work with the amount after anchor fees.
- const inputAmountUnits = new Big(quote.metadata.onrampInputAmountUnits)
+ const inputAmountUnits = new Big(quote.metadata.onrampInputAmountUnits);
const inputAmountRaw = multiplyByPowerOfTen(inputAmountUnits, inputTokenDetails.decimals).toFixed(0, 0);
// The output amount to be obtained on Moonbeam, differs from the amount to be obtained on destination evm chain.
const outputAmountRaw = (quote.metadata as QuoteTicketMetadata).onrampOutputAmountMoonbeamRaw;
- const outputAmount = multiplyByPowerOfTen(
- new Big(outputAmountRaw),
- -outputTokenDetails.decimals,
- );
+ const outputAmount = multiplyByPowerOfTen(new Big(outputAmountRaw), -outputTokenDetails.decimals);
const inputTokenPendulumDetails = getPendulumDetails(quote.inputCurrency);
const outputTokenPendulumDetails = getPendulumDetails(quote.outputCurrency, toNetwork);
diff --git a/api/src/api/services/transactions/xcm/pendulumToAssethub.ts b/api/src/api/services/transactions/xcm/pendulumToAssethub.ts
index 98e50c742..7f6061257 100644
--- a/api/src/api/services/transactions/xcm/pendulumToAssethub.ts
+++ b/api/src/api/services/transactions/xcm/pendulumToAssethub.ts
@@ -2,16 +2,19 @@ import { SubmittableExtrinsic } from '@polkadot/api-base/types';
import { ISubmittableResult } from '@polkadot/types/types';
import { PendulumCurrencyId } from 'shared';
import { ApiManager } from '../../pendulum/apiManager';
+import { decodeAddress } from '@polkadot/util-crypto';
+import { u8aToHex } from '@polkadot/util';
export async function createPendulumToAssethubTransfer(
destinationAddress: string,
currencyId: PendulumCurrencyId,
rawAmount: string,
): Promise> {
+ const receiverId = u8aToHex(decodeAddress(destinationAddress));
const destination = {
V3: {
parents: 1,
- interior: { X2: [{ Parachain: 1000 }, { AccountKey20: { network: undefined, key: destinationAddress } }] },
+ interior: { X2: [{ Parachain: 1000 }, { AccountId32: { network: undefined, id: receiverId } }] },
},
};
diff --git a/api/src/database/migrator.ts b/api/src/database/migrator.ts
index 1bafcb05f..a6fd344b9 100644
--- a/api/src/database/migrator.ts
+++ b/api/src/database/migrator.ts
@@ -91,7 +91,7 @@ if (require.main === module) {
process.exit(0);
} catch (error) {
- console.error("Error performing action:", error);
+ console.error('Error performing action:', error);
process.exit(1);
}
})();
From 55401b0275dff39de6cf6b29e0c4dd34275b3c6e Mon Sep 17 00:00:00 2001
From: Gianfranco
Date: Tue, 15 Apr 2025 11:33:25 -0300
Subject: [PATCH 44/52] fix broken merge
---
api/src/api/services/ramp/quote.service.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/api/src/api/services/ramp/quote.service.ts b/api/src/api/services/ramp/quote.service.ts
index 9fb8c3133..4e185daca 100644
--- a/api/src/api/services/ramp/quote.service.ts
+++ b/api/src/api/services/ramp/quote.service.ts
@@ -224,9 +224,6 @@ export class QuoteService extends BaseRampService {
// if onramp, adjust for axlUSDC price difference.
const outputAmountMoonbeamRaw: string = amountOut.preciseQuotedAmountOut.rawBalance.toFixed(); // Store the value before the adjustment.
-<<<<<<< Updated upstream
- if (rampType === 'on') {
-=======
if (rampType === 'on' && to !== 'assethub') {
const outTokenDetails = getOnChainTokenDetails(getNetworkFromDestination(to)!, outputCurrency as OnChainToken);
if (!outTokenDetails || !isEvmTokenDetails(outTokenDetails)) {
@@ -236,7 +233,6 @@ export class QuoteService extends BaseRampService {
});
}
->>>>>>> Stashed changes
const routeParams = createOnrampRouteParams(
'0x30a300612ab372cc73e53ffe87fb73d62ed68da3', // It does not matter.
amountOut.preciseQuotedAmountOut.rawBalance.toFixed(),
From 3c6cba0246ebaad30fd7534c573c05a1773d63d3 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:25:38 +0200
Subject: [PATCH 45/52] Show text for isQuoteExpired also for offramps
---
frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
index 72695d56f..e93fefb6b 100644
--- a/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
+++ b/frontend/src/components/RampSummaryDialog/RampSummaryButton.tsx
@@ -41,7 +41,7 @@ export const useButtonContent = ({ isSubmitted, toToken, submitButtonDisabled }:
const isAnchorWithoutRedirect = toToken.type === 'moonbeam';
const isAnchorWithRedirect = !isAnchorWithoutRedirect;
- if (isBRCodeReady && isQuoteExpired) {
+ if ((isOnramp && isBRCodeReady && isQuoteExpired) || (isOfframp && isQuoteExpired)) {
return {
text: t('components.dialogs.RampSummaryDialog.quoteExpired'),
icon: null,
From 5e7bc5b159f0b8fce3dddc662736f42f243241f4 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:27:52 +0200
Subject: [PATCH 46/52] Fix condition for showing quote expiry
---
.../TransactionTokensDisplay.tsx | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
index de1520eb3..5b048f6c0 100644
--- a/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
+++ b/frontend/src/components/RampSummaryDialog/TransactionTokensDisplay.tsx
@@ -39,6 +39,7 @@ export const TransactionTokensDisplay: FC = ({
const { t } = useTranslation();
const rampState = useRampState();
const [timeLeft, setTimeLeft] = useState({ minutes: ONRAMP_EXPIRY_MINUTES, seconds: 0 });
+ const [targetTimestamp, setTargetTimestamp] = useState(null);
const { setIsQuoteExpired } = useRampSummaryActions();
useEffect(() => {
@@ -56,6 +57,8 @@ export const TransactionTokensDisplay: FC = ({
targetTimestamp = new Date(expiresAt).getTime();
}
+ setTargetTimestamp(targetTimestamp);
+
if (targetTimestamp === null) {
// If no valid timestamp, mark as expired immediately
setTimeLeft({ minutes: 0, seconds: 0 });
@@ -142,13 +145,11 @@ export const TransactionTokensDisplay: FC = ({
direction={rampDirection}
/>
-
- {rampState?.ramp?.createdAt && (
- <>
- {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
- >
- )}
-
+ {targetTimestamp !== null && (
+
+ {t('components.dialogs.RampSummaryDialog.BRLOnrampDetails.timerLabel')} {formattedTime}
+
+ )}
);
};
From 8d02b366a49a5159004e744238836ad9779b32cc Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:28:09 +0200
Subject: [PATCH 47/52] Remove unused attribute from quote store
---
frontend/src/stores/ramp/useQuoteStore.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/frontend/src/stores/ramp/useQuoteStore.ts b/frontend/src/stores/ramp/useQuoteStore.ts
index fc6054685..638f9eefd 100644
--- a/frontend/src/stores/ramp/useQuoteStore.ts
+++ b/frontend/src/stores/ramp/useQuoteStore.ts
@@ -98,7 +98,6 @@ export const useQuoteStore = create((set) => ({
error: null,
outputAmount: undefined,
exchangeRate: 0,
- quoteFetchedAt: null, // Initialize timestamp
fetchQuote: async (params: QuoteParams) => {
const { inputAmount } = params;
From 9675116a13ae06b1c9036d7038b48232ae5cac1f Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:28:17 +0200
Subject: [PATCH 48/52] Adjust quote expiry button text
---
frontend/src/translations/en.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json
index e435cb564..f730f3b3a 100644
--- a/frontend/src/translations/en.json
+++ b/frontend/src/translations/en.json
@@ -204,7 +204,7 @@
"onrampFee": "Onramp fee",
"continueWithPartner": "Continue with Partner",
"continueOnPartnersPage": "Continue on Partner's page",
- "quoteExpired": "Quote expired. Close dialog and try again.",
+ "quoteExpired": "Quote expired. Please close the dialog and try again.",
"processing": "Processing",
"continue": "Continue",
"quote": "Quote",
From 7f469fa17d546cc1904a60f791cc1bd81b667e46 Mon Sep 17 00:00:00 2001
From: Gianfranco
Date: Tue, 15 Apr 2025 12:12:58 -0300
Subject: [PATCH 49/52] enable assethub onramp
---
api/src/api/services/ramp/quote.service.ts | 2 +-
frontend/src/hooks/ramp/useRampValidation.ts | 2 --
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/api/src/api/services/ramp/quote.service.ts b/api/src/api/services/ramp/quote.service.ts
index 4e185daca..957394386 100644
--- a/api/src/api/services/ramp/quote.service.ts
+++ b/api/src/api/services/ramp/quote.service.ts
@@ -191,7 +191,7 @@ export class QuoteService extends BaseRampService {
}
const outTokenDetails = toNetwork ? getOnChainTokenDetails(toNetwork, outputCurrency as OnChainToken) : undefined;
if (rampType === 'on') {
- if (!outTokenDetails || !isEvmTokenDetails(outTokenDetails)) {
+ if (!outTokenDetails) {
throw new APIError({
status: httpStatus.BAD_REQUEST,
message: 'Invalid token details for onramp',
diff --git a/frontend/src/hooks/ramp/useRampValidation.ts b/frontend/src/hooks/ramp/useRampValidation.ts
index 6bea384c4..8d7d0ebc3 100644
--- a/frontend/src/hooks/ramp/useRampValidation.ts
+++ b/frontend/src/hooks/ramp/useRampValidation.ts
@@ -182,8 +182,6 @@ export const useRampValidation = () => {
if (validationError) return validationError;
if (quoteLoading) return t('components.swap.validation.calculatingQuote')
- if (isOnramp && selectedNetwork === Networks.AssetHub)
- return t('components.swap.validation.assetHubNotSupported');
return null;
}, [
From 583a4099a82317f731b2aab75a66cca2f34ebe60 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:49:10 +0200
Subject: [PATCH 50/52] Fix wrong lock expiry check
---
api/src/api/services/phases/phase-processor.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/api/src/api/services/phases/phase-processor.ts b/api/src/api/services/phases/phase-processor.ts
index 36d1c964b..7173d99d7 100644
--- a/api/src/api/services/phases/phase-processor.ts
+++ b/api/src/api/services/phases/phase-processor.ts
@@ -78,10 +78,10 @@ export class PhaseProcessor {
private isLockExpired(state: RampState): boolean {
const lockDuration = 15 * 60 * 1000; // 15 minutes
const now = new Date();
- const lockTime = new Date(state.processingLock.lockedAt || 0);
- if (!lockTime) {
- return true; // No lock time means it's not locked
+ if (!state.processingLock?.lockedAt) {
+ return true; // No lock time means it's not locked or has expired
}
+ const lockTime = new Date(state.processingLock.lockedAt);
return now.getTime() - lockTime.getTime() > lockDuration;
}
From 8453beed641f3ef57c6bd9125586cc8bc9157058 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 16:49:26 +0200
Subject: [PATCH 51/52] Fix wrong log message
---
.../api/services/phases/handlers/subsidize-pre-swap-handler.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts b/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
index b96fd670d..d443d0bf6 100644
--- a/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
+++ b/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts
@@ -59,7 +59,7 @@ export class SubsidizePreSwapPhaseHandler extends BasePhaseHandler {
return this.transitionToNextPhase(state, 'nablaApprove');
} catch (e) {
console.error('Error in subsidizePreSwap:', e);
- throw new Error('SubsidizePreSwapPhaseHandler: Failed to subsidize post swap.');
+ throw new Error('SubsidizePreSwapPhaseHandler: Failed to subsidize pre swap.');
}
}
}
From 3fbbb3d03c44886d19e371681bb5318daf2322d5 Mon Sep 17 00:00:00 2001
From: Marcel Ebert
Date: Tue, 15 Apr 2025 17:02:43 +0200
Subject: [PATCH 52/52] Adjust styling of success page
---
frontend/src/components/EmailForm/index.tsx | 10 ++++-----
frontend/src/pages/success/index.tsx | 23 ++++++++++++++-------
2 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/frontend/src/components/EmailForm/index.tsx b/frontend/src/components/EmailForm/index.tsx
index 4d6d7fcac..7717f18ab 100644
--- a/frontend/src/components/EmailForm/index.tsx
+++ b/frontend/src/components/EmailForm/index.tsx
@@ -57,7 +57,7 @@ export const EmailForm = ({ transactionId, transactionSuccess }: EmailFormProps)
return (
<>
-
+
@@ -76,10 +76,10 @@ export const EmailForm = ({ transactionId, transactionSuccess }: EmailFormProps)
return (