Skip to content
23 changes: 12 additions & 11 deletions api/src/api/services/moonbeam/balance.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { createPublicClient, http } from 'viem';
import { moonbeam, polygon } from 'viem/chains';
import { moonbeam } from 'viem/chains';
import erc20ABI from '../../../contracts/ERC20';
import Big from 'big.js';
import { ApiManager } from '../pendulum/apiManager';
import { getFundingData } from '../pendulum/pendulum.service';
import { KeyringPair } from '@polkadot/keyring/types';
import { Keyring } from '@polkadot/api';
import { MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS, MOONBEAM_FUNDING_PRIVATE_KEY } from '../../../constants/constants';
import { MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS, MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM, MOONBEAM_FUNDING_PRIVATE_KEY } from '../../../constants/constants';
import { multiplyByPowerOfTen } from '../pendulum/helpers';
import { privateKeyToAccount } from 'viem/accounts';
import { createMoonbeamClientsAndConfig } from './createServices';
import logger from '../../../config/logger';
import { Networks } from 'shared';

export function checkMoonbeamBalancePeriodically(
export function checkEvmBalancePeriodically(
tokenAddress: string,
brlaEvmAddress: string,
amountDesiredRaw: string,
intervalMs: number,
timeoutMs: number,
chain: any
) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const intervalId = setInterval(async () => {
try {
const publicClient = createPublicClient({
chain: moonbeam,
chain,
transport: http(),
});

Expand Down Expand Up @@ -54,11 +53,13 @@ export function checkMoonbeamBalancePeriodically(
});
}

export const fundMoonbeamEphemeralAccount = async (ephemeralAddress: string) => {
export const fundMoonbeamEphemeralAccount = async (ephemeralAddress: string, destinationNetwork: Networks) => {
try {
const apiManager = ApiManager.getInstance();
const apiData = await apiManager.getApi('moonbeam');
const { walletClient, fundingAmountRaw, publicClient } = getMoonbeamFundingData(apiData.decimals);

const largeFunding = destinationNetwork === Networks.Ethereum;
const { walletClient, fundingAmountRaw, publicClient } = getMoonbeamFundingData(apiData.decimals, largeFunding);

const txHash = await walletClient.sendTransaction({
to: ephemeralAddress as `0x${string}`,
Expand All @@ -75,12 +76,12 @@ export const fundMoonbeamEphemeralAccount = async (ephemeralAddress: string) =>
}
};

export function getMoonbeamFundingData(decimals: number): {
export function getMoonbeamFundingData(decimals: number, largeFunding: boolean = false): {
fundingAmountRaw: string;
walletClient: ReturnType<typeof createMoonbeamClientsAndConfig>['walletClient'];
publicClient: ReturnType<typeof createMoonbeamClientsAndConfig>['publicClient'];
} {
const fundingAmountUnits = Big(MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS);
const fundingAmountUnits = largeFunding ? Big(MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM) : Big(MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS);
const fundingAmountRaw = multiplyByPowerOfTen(fundingAmountUnits, decimals).toFixed();

const moonbeamExecutorAccount = privateKeyToAccount(MOONBEAM_FUNDING_PRIVATE_KEY as `0x${string}`);
Expand Down
41 changes: 38 additions & 3 deletions api/src/api/services/pendulum/apiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,32 @@ export class ApiManager {
createCall: (api: ApiPromise) => SubmittableExtrinsic<'promise', ISubmittableResult>,
senderKeypair: KeyringPair,
networkName: SubstrateApiNetwork,
): Promise<any> {
): Promise<{hash: string}> {
const apiInstance = await this.getApi(networkName);
const call = createCall(apiInstance.api);

try {
const nonce = await this.getNonce(senderKeypair, networkName);
logger.info(`Sending transaction on ${networkName} with nonce ${nonce}`);
return call.signAndSend(senderKeypair, { nonce });

return new Promise((resolve, reject) => {
call.signAndSend(senderKeypair, { nonce }, (submissionResult: ISubmittableResult) => {
const { status, events, dispatchError } = submissionResult;

if (dispatchError) {
reject(new Error(`Transaction failed: ${dispatchError}`));
}

if (submissionResult.isError){
reject(new Error(`Transaction was not included: ${submissionResult.dispatchError}`));
}

if (submissionResult.isFinalized){
const hash = status.asFinalized.toString();
resolve({hash})
}
});
});
} 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')) {
Expand All @@ -197,7 +215,24 @@ export class ApiManager {
try {
await this.populateApi(networkName);
const nonce = await this.getNonce(senderKeypair, networkName);
return call.signAndSend(senderKeypair, { nonce });
return new Promise((resolve, reject) => {
call.signAndSend(senderKeypair, { nonce }, (submissionResult: ISubmittableResult) => {
const { status, events, dispatchError } = submissionResult;

if (dispatchError) {
reject(new Error(`Transaction failed: ${dispatchError}`));
}

if (submissionResult.isError){
reject(new Error(`Transaction was not included: ${submissionResult.dispatchError}`));
}

if (submissionResult.isFinalized){
const hash = status.asFinalized.toString();
resolve({hash})
}
});
});
} catch (retryError) {
throw retryError;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import RampState from '../../../../models/rampState.model';
import { StateMetadata } from '../meta-state-types';
import { BasePhaseHandler } from '../base-phase-handler';
import { BrlaApiService } from '../../brla/brlaApiService';
import { checkMoonbeamBalancePeriodically } from '../../moonbeam/balance';
import { checkEvmBalancePeriodically } from '../../moonbeam/balance';
import logger from '../../../../config/logger';
import { polygon } from 'viem/chains';

export class BrlaPayoutOnMoonbeamPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
Expand Down Expand Up @@ -38,12 +39,13 @@ export class BrlaPayoutOnMoonbeamPhaseHandler extends BasePhaseHandler {
const maxWaitingTimeMs = 5 * 60 * 1000; // 5 minutes

try {
await checkMoonbeamBalancePeriodically(
await checkEvmBalancePeriodically(
tokenDetails.polygonErc20Address,
brlaEvmAddress,
outputAmountBeforeFees.raw,
pollingTimeMs,
maxWaitingTimeMs,
polygon
);
} catch (balanceCheckError) {
if (balanceCheckError instanceof Error) {
Expand Down
6 changes: 4 additions & 2 deletions api/src/api/services/phases/handlers/brla-teleport-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import RampState from '../../../../models/rampState.model';
import { StateMetadata } from '../meta-state-types';
import { BasePhaseHandler } from '../base-phase-handler';
import { BrlaApiService } from '../../brla/brlaApiService';
import { checkMoonbeamBalancePeriodically } from '../../moonbeam/balance';
import { checkEvmBalancePeriodically } from '../../moonbeam/balance';
import { BrlaTeleportService } from '../../brla/brlaTeleportService';
import logger from '../../../../config/logger';
import { moonbeam } from 'viem/chains';

export class BrlaTeleportPhaseHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
Expand Down Expand Up @@ -51,12 +52,13 @@ export class BrlaTeleportPhaseHandler extends BasePhaseHandler {

const tokenDetails = getAnyFiatTokenDetailsMoonbeam(FiatToken.BRL);

await checkMoonbeamBalancePeriodically(
await checkEvmBalancePeriodically(
tokenDetails.moonbeamErc20Address,
moonbeamEphemeralAddress,
inputAmountBeforeSwapRaw, // TODO verify this is okay, regarding decimals.
pollingTimeMs,
maxWaitingTimeMs,
moonbeam
);
} catch (balanceCheckError) {
if (balanceCheckError instanceof Error) {
Expand Down
11 changes: 9 additions & 2 deletions api/src/api/services/phases/handlers/fund-ephemeral-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FiatToken, RampPhase } from 'shared';
import { FiatToken, getNetworkFromDestination, Networks, RampPhase } from 'shared';
import { BasePhaseHandler } from '../base-phase-handler';
import RampState from '../../../../models/rampState.model';
import { API, ApiManager } from '../../pendulum/apiManager';
Expand Down Expand Up @@ -54,7 +54,14 @@ export class FundEphemeralPhaseHandler extends BasePhaseHandler {

if (state.type === 'on' && !isMoonbeamFunded) {
logger.info('Funding moonbeam ephemeral...');
await fundMoonbeamEphemeralAccount(moonbeamEphemeralAddress);

const destinationNetwork = getNetworkFromDestination(state.to);
// For onramp case, "to" is always a network.
if (!destinationNetwork) {
throw new Error('FundEphemeralPhaseHandler: Invalid destination network.');
}

await fundMoonbeamEphemeralAccount(moonbeamEphemeralAddress, destinationNetwork);
}
} catch (e) {
console.error('Error in FundEphemeralPhaseHandler:', e);
Expand Down
15 changes: 14 additions & 1 deletion api/src/api/services/ramp/quote.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getOnChainTokenDetails,
OnChainToken,
isEvmTokenDetails,
Networks,
} from 'shared';
import { BaseRampService } from './base.service';
import QuoteTicket, { QuoteTicketMetadata } from '../../../models/quoteTicket.model';
Expand All @@ -20,7 +21,8 @@ import { ApiManager } from '../pendulum/apiManager';
import { calculateTotalReceive, calculateTotalReceiveOnramp } from '../../helpers/quote';
import { createOnrampRouteParams, getRoute } from '../transactions/squidrouter/route';
import { parseContractBalanceResponse, stringifyBigWithSignificantDecimals } from '../../helpers/contracts';

import { MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS, MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM } from '../../../constants/constants';
import { multiplyByPowerOfTen } from '../../services/pendulum/helpers';
/**
* Trims trailing zeros from a decimal string, keeping at least two decimal places.
* @param decimalString - The decimal string to format
Expand Down Expand Up @@ -245,6 +247,17 @@ export class QuoteService extends BaseRampService {
const { route } = routeResult.data;
const { toAmountMin } = route.estimate;

// Check against our moonbeam funding amounts.
const squidrouterSwapValue = multiplyByPowerOfTen(Big(route.transactionRequest.value), -18);
const fundingAmountUnits = getNetworkFromDestination(to) === Networks.Ethereum ? Big(MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM) : Big(MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS);
// Leave 2 glmr for other operations of the ephemeral.
if (squidrouterSwapValue.gte(fundingAmountUnits.minus(2))) {
throw new APIError({
status: httpStatus.SERVICE_UNAVAILABLE,
message: 'Cannot service this route at the moment. Please try again later.',
});
}

amountOut.preciseQuotedAmountOut = parseContractBalanceResponse(
outTokenDetails!.pendulumDecimals,
BigInt(toAmountMin),
Expand Down
4 changes: 3 additions & 1 deletion api/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const SUBSIDY_MINIMUM_RATIO_FUND_UNITS = '5'; // 5 Subsidies considering maximum
const MOONBEAM_RECEIVER_CONTRACT_ADDRESS = '0x2AB52086e8edaB28193172209407FF9df1103CDc';
const STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS = '2.5'; // Amount to send to the new stellar ephemeral account created
const PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS = '0.1'; // Amount to send to the new pendulum ephemeral account created
const MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS = '3'; // Amount to send to the new moonbeam ephemeral account created
const MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS = '30'; // Amount to send to the new moonbeam ephemeral account created
const MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM = '80'; // Amount to send to the new moonbeam ephemeral account created when onramping to Ethereum
const BRLA_BASE_URL = 'https://api.brla.digital:5567/v1/business';
const DEFAULT_POLLING_INTERVAL = 3000;
const GLMR_FUNDING_AMOUNT_RAW = '50000000000000000';
Expand All @@ -32,6 +33,7 @@ const MOONBEAM_FUNDING_PRIVATE_KEY = MOONBEAM_EXECUTOR_PRIVATE_KEY;
const { BACKEND_TEST_STARTER_ACCOUNT } = process.env;

export {
MOONBEAM_EPHEMERAL_STARTING_BALANCE_UNITS_ETHEREUM,
ASSETHUB_XCM_FEE_USDC_UNITS,
SEQUENCE_TIME_WINDOW_IN_SECONDS,
BACKEND_TEST_STARTER_ACCOUNT,
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,6 @@ export const useRegisterRamp = () => {
// Check if we can proceed with the registration process
const lockResult = checkLock();
if (!lockResult.canProceed) {
// Alternatively release locks if process was cancelled
// This will NOT clean the localStorage right after the process is cancelled
// but rather on the first checkLock() call after confirmation.
if (!rampStarted) {
releaseSigningLock();
releaseLock();
}
return;
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export enum LocalStorageKeys {
FIRED_INITIALIZATION_EVENTS = 'FIRED_INITIALIZATION_EVENTS',
TERMS_AND_CONDITIONS = 'TERMS_AND_CONDITIONS',
RAMPING_STATE = 'RAMPING_STATE',
REGISTER_KEY_LOCAL_STORAGE = 'rampRegisterKey',
START_KEY_LOCAL_STORAGE = 'rampStartKey',
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/stores/rampStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface RampStore extends RampZustand {

const clearRampingState = () => {
storageService.remove(LocalStorageKeys.RAMPING_STATE);
storageService.remove(LocalStorageKeys.REGISTER_KEY_LOCAL_STORAGE);
storageService.remove(LocalStorageKeys.START_KEY_LOCAL_STORAGE);
};

// Load initial state from localStorage
Expand Down
Loading