Skip to content

Fix Monerium flow after API change#1157

Merged
ebma merged 13 commits into
stagingfrom
fix-modified-monerium-api
May 21, 2026
Merged

Fix Monerium flow after API change#1157
ebma merged 13 commits into
stagingfrom
fix-modified-monerium-api

Conversation

@gianfra-t
Copy link
Copy Markdown
Contributor

No description provided.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 21, 2026

Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit e96b57a
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6a0f3508f084eb00087ffbe0
😎 Deploy Preview https://deploy-preview-1157--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 21, 2026

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit e96b57a
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6a0f35086795400008a0fcc9
😎 Deploy Preview https://deploy-preview-1157--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Monerium onramp flow to align with upstream API/behavior changes, improving correctness and safety around permit signing/validation, self-transfer execution, and EVM token resolution/logging.

Changes:

  • Add richer Monerium permit payloads (signed context) on the frontend and enforce server-side permit validation (EIP-712 recovery + strict field checks).
  • Harden the Monerium self-transfer phase with preflight inspection/simulation and persistence of the self-transfer tx hash to avoid resends.
  • Improve EVM operational behavior: redact RPC URLs in logs and resolve EVM token details by contract address (including Monerium EURe Polygon v1/v2).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/shared/src/tokens/utils/helpers.ts Adds getEvmTokenDetailsByAddress and a Monerium EURe address fallback to resolve tokens by contract address.
packages/shared/src/services/evm/clientManager.ts Redacts RPC URLs in logs and tightens readContractWithRetry typings/casting for viem.
packages/shared/src/endpoints/monerium.ts Extends Monerium endpoint types: IBAN now includes name; permit signature now supports an optional signed context.
apps/frontend/src/helpers/crypto.ts Returns PermitSignature with a context payload when signing ERC-2612 permits.
apps/api/src/api/services/transactions/onramp/common/monerium.ts Increases Monerium self-transfer gas limit via a named constant.
apps/api/src/api/services/ramp/ramp.service.ts Uses ibanData.name for EPC QR/IBAN payment data and validates Monerium onramp permits against quote/state expectations.
apps/api/src/api/services/ramp/monerium-self-transfer.ts New helper to parse/inspect a presigned transferFrom raw transaction before broadcast.
apps/api/src/api/services/ramp/monerium-permit.ts New helper to validate permit context + signature recovery and decide if sending the permit is necessary.
apps/api/src/api/services/phases/meta-state-types.ts Adds moneriumOnrampSelfTransferHash to persisted phase metadata.
apps/api/src/api/services/phases/handlers/squid-router-phase-handler.ts Uses address-based token resolution and skips squid routing for same-chain same-token passthrough.
apps/api/src/api/services/phases/handlers/monerium-onramp-self-transfer-handler.ts Adds permit preflight/optional send, self-transfer preflight validation, and stores transfer hash for idempotency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +78 to +87
if (network === Networks.Polygon && MONERIUM_EURE_POLYGON_ADDRESSES.has(normalizedTokenAddress)) {
return {
assetSymbol: ERC20_EURE_POLYGON_TOKEN_NAME,
decimals: ERC20_EURE_POLYGON_DECIMALS,
erc20AddressSourceChain: tokenAddress,
isNative: false,
network: Networks.Polygon,
pendulumRepresentative: PENDULUM_USDC_AXL,
type: TokenType.Evm
};
Comment on lines +291 to +293
if (currentNonce > transfer.signedNonce) {
throw new Error(
`[${rampId}] Self-transfer signed nonce ${transfer.signedNonce} has already been consumed by ${transfer.signer} (current nonce ${currentNonce}). Do not resend this raw transaction; regenerate the presigned self-transfer transaction or inspect the previous nonce-${transfer.signedNonce} transaction.`
Comment on lines +53 to +87
export function validateMoneriumOnrampPermit(permit: PermitSignature, expectation: MoneriumPermitExpectation): void {
const context = getPermitContext(permit);
const expectedChainId = getNetworkId(expectation.network);

assertEqual("owner", context.owner, expectation.expectedOwner);
assertEqual("spender", context.spender, expectation.expectedSpender);
assertEqual("valueRaw", context.valueRaw, expectation.expectedValueRaw);
assertEqual("tokenAddress", context.tokenAddress, expectation.expectedTokenAddress);
assertEqual("tokenName", context.tokenName, expectation.expectedTokenName);
assertEqual("tokenVersion", context.tokenVersion, expectation.expectedTokenVersion ?? "1");
assertEqual("chainId", context.chainId, expectedChainId);
assertEqual("deadline", context.deadline, permit.deadline);

const recoveredSigner = verifyTypedData(
{
chainId: context.chainId,
name: context.tokenName,
verifyingContract: context.tokenAddress,
version: context.tokenVersion
},
PERMIT_TYPES,
{
deadline: context.deadline,
nonce: context.nonce,
owner: context.owner,
spender: context.spender,
value: context.valueRaw
},
EvmSignature.from({ r: permit.r, s: permit.s, v: permit.v }).serialized
);

if (recoveredSigner.toLowerCase() !== expectation.expectedOwner.toLowerCase()) {
throwBadPermit(`Monerium permit signature was produced by ${recoveredSigner}, expected ${expectation.expectedOwner}`);
}
}
Comment on lines +40 to +86
export async function inspectMoneriumSelfTransferTransaction(
txData: string,
expectation: MoneriumSelfTransferExpectation
): Promise<MoneriumSelfTransferInspection> {
const serializedTransaction = txData as RecoverableSerializedTransaction;
const parsedTx = parseTransaction(serializedTransaction);
const signer = await recoverTransactionAddress({ serializedTransaction });
const tokenAddress = expectation.expectedTokenAddress ?? ERC20_EURE_POLYGON_V2;
const signedNonce = parsedTx.nonce;

if (signedNonce === undefined) {
throw new Error(`[${expectation.rampId}] Self-transfer signed transaction is missing a nonce`);
}

if (signer.toLowerCase() !== expectation.expectedSigner.toLowerCase()) {
throw new Error(
`[${expectation.rampId}] Self-transfer signer ${signer} does not match expected EVM ephemeral ${expectation.expectedSigner}`
);
}

if (parsedTx.to?.toLowerCase() !== tokenAddress.toLowerCase()) {
throw new Error(`[${expectation.rampId}] Self-transfer token ${parsedTx.to} does not match expected ${tokenAddress}`);
}

const decodedTransfer = decodeFunctionData({
abi: moneriumTransferFromAbi,
data: parsedTx.data ?? "0x"
});
const [owner, recipient, amountRaw] = decodedTransfer.args;
const expectedAmount = BigInt(expectation.expectedAmountRaw);

if (owner.toLowerCase() !== expectation.expectedOwner.toLowerCase()) {
throw new Error(
`[${expectation.rampId}] Self-transfer owner ${owner} does not match expected ${expectation.expectedOwner}`
);
}
if (recipient.toLowerCase() !== expectation.expectedRecipient.toLowerCase()) {
throw new Error(
`[${expectation.rampId}] Self-transfer recipient ${recipient} does not match expected ${expectation.expectedRecipient}`
);
}
if (amountRaw !== expectedAmount) {
throw new Error(
`[${expectation.rampId}] Self-transfer amount ${amountRaw.toString()} does not match expected ${expectation.expectedAmountRaw}`
);
}

- Strict nonce equality check with directional error messages to surface
  both stale and gap-ahead signed nonces on the EVM ephemeral.
- Validate chainId of the signed self-transfer against Polygon to reject
  transactions signed for the wrong network before broadcast.
- Reject Monerium permits with a missing signed context explicitly
  instead of silently coercing undefined to a string mismatch.
- Document the 30s post-broadcast settlement wait.
- Add unit tests for permit validation, self-transfer inspection,
  squid-router same-chain passthrough, monerium tx builders, shared
  EVM client manager redaction, and EURE token helper fallbacks.
Copy link
Copy Markdown
Member

@ebma ebma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent Code Review

Reviewed all 11 modified files against staging. Below: my own findings + verdicts on the Copilot review (#pullrequestreview-4338359773). Accepted improvements have been pushed as commit 99710cd60.

Verdicts on Copilot's Comments

1. Extract "EURe" symbol to a constantSkip. The literal has a single consumer (squidRouterPhaseHandler balance check) and no UI/display surface. Adding a shared constant for a single internal comparison would be over-engineering (CLAUDE.md "No Over-Engineering").

2. Nonce comparison treats gap-ahead as okayAccepted. The original current > signed silently proceeded when the current nonce was greater than signed (treating it as already-included) but did not flag the inverse case (signed nonce ahead of current). On an ephemeral account, a gap-ahead signed nonce stalls the mempool indefinitely. Changed to strict !== with directional error messages.

3. Missing chainId validation on signed self-transferAccepted. A self-transfer presigned on the wrong chain would otherwise pass inspection. Added optional expectedChainId to MoneriumSelfTransferExpectation and pass getNetworkId(Networks.Polygon) from the handler. Tests added for both positive and negative cases.

4. assertEqual undefined coercion in permit contextAccepted. Previously String(undefined) === "undefined" would produce a confusing "undefined does not match expected X" error. Now throws an explicit "missing from signed context" error. Test added for the entirely-missing-context branch.

My Independent Findings

Accepted & applied (in commit 99710cd60):

  • Same as Copilot #2, #3, #4 above
  • Added explanatory comment for the 30s setTimeout post-broadcast wait (CLAUDE.md: always comment setTimeout rationale)

Considered but skipped:

  • Same-chain passthrough balance gatesquidRouterPhaseHandler returns early without an explicit balance check, but the downstream destinationTransfer phase enforces this. Adding a duplicate check would be over-engineering.
  • Infura URL redaction in error messages — Already covered by sanitizeRpcErrorMessage in clientManager.ts.
  • Address/name casing nits — Cosmetic only.

Test Coverage Added

Tests committed for the affected areas:

  • monerium-permit.test.ts — value/version mismatch, missing context, nonce drift, allowance-sufficient skip
  • monerium-self-transfer.test.ts — amount mismatch, chainId positive + negative
  • squid-router-phase-handler.test.ts, monerium.test.ts, clientManager.test.ts, helpers.test.ts

Verification

  • 10/10 monerium tests pass (bun test)
  • bun typecheck shows no new errors introduced by these edits (4 pre-existing branch errors remain; pre-existing on 06ca1a5a9 before any of my changes)
  • bun lint:fix clean on touched files

Note on mykobo-eur-offramp.integration.test.ts

This untracked file references several exports that don't exist in @vortexfi/shared (MykoboApiService, MykoboCurrency, etc.) and RampPhase values that aren't defined. It appears to be WIP for a separate feature. Not committed as part of this PR — recommend handling separately.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment on lines +12 to +16
export function redactRpcUrlForLogs(rpcUrl: string): string {
if (!rpcUrl) {
return "<default>";
}

Comment on lines +5 to +8
it("redacts provider API keys from RPC URLs", () => {
expect(redactRpcUrlForLogs("https://polygon-mainnet.g.alchemy.com/v2/dUzb7oLgJ3f9T72vWR-Iw7X38wct7h62")).toBe(
"https://polygon-mainnet.g.alchemy.com/v2/[redacted]"
);
assertEqual("tokenVersion", context.tokenVersion, expectation.expectedTokenVersion ?? "1");
assertEqual("chainId", context.chainId, expectedChainId);
assertEqual("deadline", context.deadline, permit.deadline);

Copilot AI requested a review from ebma May 21, 2026 16:21
Copilot finished work on behalf of ebma May 21, 2026 16:22
@ebma ebma merged commit 741d03a into staging May 21, 2026
7 checks passed
@ebma ebma deleted the fix-modified-monerium-api branch May 21, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants