Add monerium interactive#731
Conversation
✅ Deploy Preview for pendulum-pay ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| export const getMoneriumEvmDefaultMintAddress = async (token: string): Promise<string | null> => { | ||
| // Assumption is the first linked address is the default mint address for Monerium EVM transactions. | ||
| // TODO: this needs to be confirmed. | ||
| return getFirstMoneriumLinkedAddress(token); |
There was a problem hiding this comment.
This needs to be confirmed how? By the user? I guess it's okay to assume the first account is the target account. The only problem would be if they lost access to that account some time ago and didn't unlink it. Maybe it would make sense to try and match this to the account that is currently connected on the Vortex UI?
There was a problem hiding this comment.
It was referring to the question of whether the first account linked (ordered by date) would be the one where the tokens are minted or not.
Even if we match with all linked address, like we do on the function checkAddressExists above, we need to know where the tokens will end up.
There was a problem hiding this comment.
True. And how do we know? Can we decide that somehow? Sorry for the noob question, but asking that is faster than me trying to look it up 😅
There was a problem hiding this comment.
Honestly, I guess we need to ask someone at Monerium. I couldn't figure it out from the API alone.
| const existingLock = localStorage.getItem(lockKey); | ||
| if (existingLock !== null) { | ||
| console.log(`Process for ${lockKey} already started, skipping...`); | ||
| const useSignatureTrace = (traceKey: string) => { |
There was a problem hiding this comment.
Why are we touching this logic in the first place?
| throw new Error("Cannot proceed with ramp registration, canRegisterRamp is false"); | ||
| } | ||
|
|
||
| // Verify we still own the lock before proceeding |
There was a problem hiding this comment.
Also answering here. I found that this was causing issues when using Monerium, probably because we must keep the functionality after the redirect/refresh. I agree the name changes were a bit unnecessary, but I tried to simplify the logic.
For this particular case, there was also another lock check on this line which should prevent the execution of the callback anyway. Is this intentional ? Maybe I'm not seeing the use of the second (inner) check.
There was a problem hiding this comment.
Let's keep the second (inner) check. If you look at the code, between the initial and the second check, there is a lot of async execution so we need the second check to rule out any issues with race conditions.
There was a problem hiding this comment.
Wait but , comparing from staging again, I only see a few if conditions between the two. Hook triggers, then we get the first check, and immediately after, the function registerRampProcess gets called (the rest is the definition of the function itself).
And once inside it, the second check is one of the first operations.
There was a problem hiding this comment.
But since the check of the lock uses the localStorage and not the state of the component, it would still be useful, no?
| }; | ||
|
|
||
| // Helper function to determine if EUR flow should use Monerium | ||
| async function shouldRouteToMonerium(executionInput: RampExecutionInput): Promise<boolean> { |
There was a problem hiding this comment.
Shall we make this a non-async function? I guess in the future we'd also determine this without a network call.
There was a problem hiding this comment.
Yeah the idea was to leave it up to the backend. At least originally.
| throw new Error("Cannot proceed with ramp registration, canRegisterRamp is false"); | ||
| } | ||
|
|
||
| // Verify we still own the lock before proceeding |
There was a problem hiding this comment.
But since the check of the lock uses the localStorage and not the state of the component, it would still be useful, no?
| failed: 0, | ||
| fundEphemeral: 20, | ||
| initial: 0, | ||
| moneriumOnrampMint: 60, // TODO we need to profile this value. |
There was a problem hiding this comment.
Yeah.... good question haha. We can keep it at 60 assuming it's an instant SEPA transfer. For the non-instant one, we need to communicate it differently anyway.
There was a problem hiding this comment.
Yeah this is not relevant anymore, at least not in the non-instant case.
| } from "./types"; | ||
|
|
||
| const MONERIUM_API_URL = "https://api.monerium.app"; | ||
| export const ERC20_EURE_POLYGON: `0x${string}` = "0x18ec0a6e18e5bc3784fdd3a3634b31245ab704f6"; // EUR.e on Polygon |
There was a problem hiding this comment.
nit: use Address type instead of 0x${string}
There was a problem hiding this comment.
We're still using this type instead of Address across the app right? I think the Address type from viem had some extra properties we don't really need, that's why we don't use it.
| } | ||
|
|
||
| const data: IbanDataResponse = await response.json(); | ||
| const ibanData = data.ibans.find(item => item.chain === "polygon"); |
There was a problem hiding this comment.
nit: I know that we know that it must always be Polygon, but it would be useful to leave a comment to explain why we are looking for Polygon here for future reference, wdyt?
| throw new Error("Beneficiary name, IBAN, and BIC are required to create EPC QR code data."); | ||
| } | ||
|
|
||
| const data = ["BCD", "001", "1", "SCT", bic, name, iban, `EUR${amount}`]; |
There was a problem hiding this comment.
nit: Could you please leave a comment why the QR Code data looks like it? what is the format exactly, what is the standard we're using here, wdyt?
| @@ -0,0 +1,66 @@ | |||
| export interface MoneriumAddress { | |||
There was a problem hiding this comment.
Shouldn't we move these types to a shared package so that they can be used in the front end?
There was a problem hiding this comment.
I don't think we will need them in the front-end, at least not now. As with BRLA, everything "Monerium" is processed first in the backend.
| const walletClient = evmClientManager.getWalletClient(Networks.Polygon, fundingAccount); | ||
|
|
||
| const txHash = await walletClient.sendTransaction({ | ||
| to: ephmeralAddress as `0x${string}`, |
There was a problem hiding this comment.
nit: Address instead of 0x${string}`?
| }); | ||
|
|
||
| const receipt = await polygonClient.waitForTransactionReceipt({ | ||
| hash: txHash as `0x${string}` |
There was a problem hiding this comment.
nit: Address. instead of 0x${string}`?
| tokenValueRaw: string, | ||
| swapHash: `0x${string}`, | ||
| logIndex: number | ||
| ): Promise<string> { |
There was a problem hiding this comment.
It doesn't say much what do we return here, what exactly is this string? Maybe we can return Promise<Hash>, wdyt?
| } else if (quote.inputCurrency === FiatToken.BRL) { | ||
| return this.moonbeamClient; | ||
| } else { | ||
| logger.info( |
There was a problem hiding this comment.
I like it 👍 We could also use TS Record to make the code more extensible rather than modifiable like:
Clients: Record<FiatToken, Client> {
[FiatToken.EURC]: this.polygonClient,
[FiatToken.BRL]: this.moonbeamClient
...
}
return Clients[quote.inputCurrency];
| depositQrCode: string | undefined; | ||
| // Only used in onramp, offramp - monerium | ||
| polygonEphemeralAddress: string; | ||
| ibanPaymentData: IbanPaymentData; |
There was a problem hiding this comment.
@pendulum-chain/devs This object begins to become really complex and intuitive. I think we should group it into smaller object-properties, wdyt? (in a separate PR)
import { PendulumTokenDetails, RampCurrency, StellarTokenDetails } from "@packages/shared";
import { ExtrinsicOptions } from "../transactions/nabla";
export interface CoreTransactionData {
inputAmount: string;
outputAmount: string;
inputCurrency: RampCurrency;
outputCurrency: RampCurrency;
inputTokenPendulumDetails: PendulumTokenDetails;
outputTokenPendulumDetails: PendulumTokenDetails;
outputTokenType: RampCurrency;
inputAmountBeforeSwapRaw: string;
// The final step for onramp is the squidRouterSwap or XCM transfer, for offramps it's the anchor payout
outputAmountBeforeFinalStep: { units: string; raw: string };
}
export interface AddressData {
pendulumEphemeralAddress: string;
moonbeamEphemeralAddress: string;
walletAddress: string | undefined;
destinationAddress: string;
brlaEvmAddress: string;
}
export interface TransactionHashes {
moonbeamXcmTransactionHash: string;
squidRouterReceiverHash: string;
pendulumToAssethubXcmHash?: string;
assetHubToPendulumHash: string;
squidRouterApproveHash: string;
squidRouterSwapHash: string;
squidRouterPayTxHash: string;
pendulumToMoonbeamXcmHash?: string;
}
export interface StellarData {
// Only used in offramp - eurc & ars route
stellarEphemeralAccountId: string;
stellarTarget: {
stellarTargetAccountId: string;
stellarTokenDetails: StellarTokenDetails;
};
}
export interface BrlaData {
// Only used in onramp - brla
inputAmountUnits: string;
inputAmountBeforeSwapUnits: string;
taxId: string;
pixDestination: string;
receiverTaxId: string;
}
export interface MoneriumData {
// Only used in onramp, offramp - monerium
polygonEphemeralAddress: string;
ibanPaymentData: string;
}
export interface NablaData {
nablaSoftMinimumOutputRaw: string;
nabla: {
approveExtrinsicOptions: ExtrinsicOptions;
swapExtrinsicOptions: ExtrinsicOptions;
};
}
export interface SystemState {
// Only used in offramp
squidRouterReceiverId: string;
executeSpacewalkNonce: number;
unhandledPaymentAlertSent: boolean;
depositQrCode: string | undefined;
}
export interface StateMetadata {
core: CoreTransactionData;
addresses: AddressData;
hashes: TransactionHashes;
stellar?: StellarData;
brla?: BrlaData;
monerium?: MoneriumData;
nabla: NablaData;
system: SystemState;
}
There was a problem hiding this comment.
Yes, I agree with that. Even better, we should try to define different meta-states for our different routes. Otherwise we end up with a very "sparse" state.
| @@ -1,5 +1,6 @@ | |||
| { | |||
| "dependencies": { | |||
| "big.js": "^6.2.1", | |||
There was a problem hiding this comment.
I don't think we need this in the root workspace.
There was a problem hiding this comment.
Don't we need it for the script which is in the root directory?
| const DEFAULT_SQUIDROUTER_GAS_ESTIMATE = "800000"; | ||
|
|
||
| function calculateGasFeeInUnits(feeResponse: AxelarScanStatusFees, estimatedGas: string | number): string { | ||
| console.log("fee response object", feeResponse); |
There was a problem hiding this comment.
Let's keep this one as this is useful for the script.
| } | ||
|
|
||
| private calculateGasFeeInUnits(feeResponse: AxelarScanStatusFees, estimatedGas: string | number): string { | ||
| console.log("fee response object", feeResponse); |
Adds Monerium onramp capabilities.
Main changes
registerandupdateparameters to enable the new route. (backend)Additional backend changes
SquidRouterPayPhaseHandlerphase. The fee to be payed will now be calculated based on the implementation found on theAxelarAPI.exponential backoff) forwaitForTransactionConfirmationon phase squidrouter swap. Solves cases where transaction got included and executed, yet the client couldn't confirm the receipt so quickly.Tests - Live offramps.
This PR touches much of both the UI and backend logic, including also a potential improvement on how
squidrouter-payestimates the native gas to provide. We should test all routes before merging.