Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
104e49e
create transactions for BRL onramp on base
gianfra-t Apr 6, 2026
95bdfd9
add quote engine for brl onramp on base
gianfra-t Apr 7, 2026
fb397cc
adjust phases for base brl flow
gianfra-t Apr 7, 2026
cb54d5e
handle subsidy, fee distribution on BRL Base
gianfra-t Apr 7, 2026
0bebbfc
create offramp transactions for brla base flow, first iteration.
gianfra-t Apr 8, 2026
fac170f
implement quote logic for brla on base.
gianfra-t Apr 8, 2026
d66365a
route new brla base offramp flow
gianfra-t Apr 8, 2026
b95e42e
fix avenia kyc machine guard bug
gianfra-t Apr 9, 2026
25f8627
use new pix evm quote strategy
gianfra-t Apr 9, 2026
e71a10b
extra validations, fixing gas issues
gianfra-t Apr 10, 2026
0eaa559
add quote engine phase for subsidy merge on evm
gianfra-t Apr 10, 2026
bfd2f8e
patch on offramp brla flow on base
gianfra-t Apr 10, 2026
142c849
patch more incorrect flows
gianfra-t Apr 13, 2026
127902a
more patches after onramp testing
gianfra-t Apr 13, 2026
688c2e4
use proper squidrouter funding value
gianfra-t Apr 13, 2026
0d7b006
adjust brla offramp on base, new phases
gianfra-t Apr 15, 2026
2857835
add final destination transaction to avenia deposit evm address on base
gianfra-t Apr 15, 2026
7b2e339
update ramp journey
gianfra-t Apr 15, 2026
04bcfd7
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
b72857c
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
6513473
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
a7cb50f
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
a0232a9
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
0bdedcb
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
dddd1fc
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
6f4cfca
Potential fix for pull request finding 'Unused variable, import, func…
gianfra-t Apr 15, 2026
9e97b6d
code quality fix
gianfra-t Apr 15, 2026
3d3e2f0
reset scroll to top on route and widget screen transitions
Sharqiewicz Apr 17, 2026
ea963ba
Merge branch 'staging' into speedy-brl-flow
ebma Apr 28, 2026
5ffe05b
Replace sepolia with base contracts and address PR review comments
ebma Apr 28, 2026
778fcaf
Improve error logging by serializing route parameters in error messages
ebma Apr 28, 2026
66a900b
Update off-ramp flow to use brlaPayoutOnBase instead of brlaPayoutBase
ebma Apr 28, 2026
a4a3bc3
Update phaseMessages to use brlaPayoutOnBase and add EVM-specific mes…
ebma Apr 28, 2026
b37c397
Sort imports
ebma Apr 28, 2026
e4d6fa9
Fix type issues
ebma Apr 28, 2026
a9418ac
Amend type issues
ebma Apr 29, 2026
b916eb3
Amend PR comments
ebma Apr 29, 2026
d5594ce
fix: address PR review comments - presigned tx, nabla addresses, PIX …
Copilot Apr 29, 2026
544f70a
Implement EVM fee distribution with Multicall3 support for partner sp…
ebma Apr 29, 2026
86caa8d
Rename 026-add-payout-address-evm-to-partners.ts
ebma Apr 29, 2026
2d79041
remove typeDocument from Mexico KYC form
Sharqiewicz Apr 30, 2026
2967a61
update Colombia KYC Form
Sharqiewicz Apr 30, 2026
d4a0f65
fix(ColKycFormScreen): use ColKycFormValues type and fix phone normal…
Copilot Apr 30, 2026
34df162
Fix bug with race condition of progress page sometimes rendering old …
ebma May 4, 2026
8a78be5
Fix bug with invalid nabla contract call name
ebma May 4, 2026
6ea53d9
Add check for already funded ephemeral address to optimize mint flow
ebma May 4, 2026
2c034f9
Add debug log
ebma May 4, 2026
a584555
Merge branch 'staging' into speedy-brl-flow
ebma May 4, 2026
1689691
Enhance error handling for pixKey and receiverTaxId validation with d…
ebma May 4, 2026
db9f7d7
Add Mexico KYB Alfredpay types
Sharqiewicz May 5, 2026
7a9cce2
add Mexico KYB Alfredpay Related Person endpoints
Sharqiewicz May 5, 2026
210fff8
refactor Mexico KYB Flow
Sharqiewicz May 5, 2026
ece2006
Require website in KYB mexico form
Sharqiewicz May 5, 2026
781daa3
update translations
Sharqiewicz May 5, 2026
9fd9543
Merge branch 'feat/alfredpay-isExternal-us-checking' of github.com:pe…
Sharqiewicz May 5, 2026
32be165
release deposit qr only after pre-signed transactions are validated a…
gianfra-t May 5, 2026
11645f2
Add proper support for passing public and private key from Vortex SDK
ebma May 5, 2026
02cb25c
Bump package versions
ebma May 5, 2026
08143c7
Fix bug with moneriumWalletAddress not set when registering ramp
ebma May 6, 2026
ff0b82f
Increase route queue interval to 1500ms and implement retry logic for…
ebma May 6, 2026
7313cc3
Fix cap for delay not applied
ebma May 6, 2026
c041b5e
Throw error if moneriumWalletAddress is undefined
ebma May 6, 2026
24ff68c
Merge pull request #1136 from pendulum-chain/fix-monerium-onramp-miss…
ebma May 6, 2026
3fe0d48
Merge branch 'staging' into speedy-brl-flow
ebma May 6, 2026
4838e3c
Implement partitioning of unsigned transactions and filter for offram…
ebma May 6, 2026
069eb26
Improve error parsing of SDK
ebma May 6, 2026
2899bff
Add publishConfig to package.json files
ebma May 6, 2026
fbaf4bb
Add deadlineMinutes parameter to transaction functions for configurab…
ebma May 6, 2026
5b60f34
Optimize speed of some phases
ebma May 7, 2026
204b75c
Enhance funding logic by incorporating presigned transaction value fo…
ebma May 7, 2026
d69aa2a
Update waitUntilTrue function to include timeout for asset arrival ch…
ebma May 7, 2026
f7905dc
Update waitUntilTrue function to include timeout for asset arrival ch…
ebma May 7, 2026
4b0017a
Fix issues with routes that don't require squidrouter transfer
ebma May 7, 2026
bf438e9
Simplify balance check in isDestinationEvmEphemeralFunded function
ebma May 7, 2026
f3dbb7e
Rename payout_address to payout_address_substrate for clarity and upd…
ebma May 7, 2026
423a38c
Reorder fee distribution transaction to ensure it precedes Nabla swap…
ebma May 7, 2026
b518fce
Add fee balance precondition checks for EVM and Substrate transactions
ebma May 7, 2026
c107a27
country-aware nationality placeholder in KYB form
Sharqiewicz May 8, 2026
b0fabd2
serve real 404s for unknown routes via Netlify redirects
Sharqiewicz May 8, 2026
7905cb1
add robots.txt and sitemap.xml
Sharqiewicz May 8, 2026
e74c8eb
fix type issue
Sharqiewicz May 8, 2026
d5cfe58
address PR comments
Sharqiewicz May 8, 2026
72e1c4c
Merge pull request #1120 from pendulum-chain/1067-bug-scroll-position…
ebma May 8, 2026
751d12f
address PR comments
Sharqiewicz May 11, 2026
d2aded1
Merge pull request #1131 from pendulum-chain/feat/alfredpay-isExterna…
ebma May 11, 2026
4d10c09
Merge pull request #1118 from pendulum-chain/speedy-brl-flow
ebma May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion apps/api/src/api/controllers/alfredpay.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,34 @@ export class AlfredpayController {
}
}

static async findKybCustomerAndBusiness(req: Request, res: Response) {
try {
const { country } = req.query as { country: string };
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
where: { country: country as AlfredPayCountry, type: AlfredpayCustomerType.BUSINESS, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay business customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
const details = await alfredpayService.getKybBusinessDetails(alfredPayCustomer.alfredPayId);

const minimized = details.map(business => ({
relatedPersons: (business.relatedPersons ?? []).map(person => ({ idRelatedPerson: person.idRelatedPerson }))
}));

res.json(minimized);
} catch (error) {
logger.error("Error finding Alfredpay KYB customer and business:", error);
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
}

static async submitKybFile(req: Request, res: Response) {
try {
const { country, submissionId, fileType } = req.body as { country: string; submissionId: string; fileType: string };
Expand Down Expand Up @@ -582,6 +610,7 @@ export class AlfredpayController {
}

static async submitKybRelatedPersonFile(req: Request, res: Response) {
let pennyCustomerId: string | undefined;
try {
const { country, relatedPersonId, fileType } = req.body as {
country: string;
Expand All @@ -602,6 +631,8 @@ export class AlfredpayController {
return res.status(404).json({ error: "Alfredpay business customer not found" });
}

pennyCustomerId = alfredPayCustomer.alfredPayId;

const fileBlob = new File([new Uint8Array(req.file.buffer)], req.file.originalname, { type: req.file.mimetype });
const alfredpayService = AlfredpayApiService.getInstance();
await alfredpayService.submitKybRelatedPersonFiles(
Expand All @@ -613,7 +644,11 @@ export class AlfredpayController {

res.json({ success: true });
} catch (error) {
logger.error("Error submitting KYB related person file:", error);
const body = req.body as { country?: string; relatedPersonId?: string; fileType?: string };
const errSummary = error instanceof Error ? error.message : String(error);
logger.error(
`[submitKybRelatedPersonFile] ${errSummary} | customerIdPenny=${pennyCustomerId ?? "n/a"} relatedPersonId=${body.relatedPersonId ?? "n/a"} userId=${req.userId ?? "n/a"}`
);
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/controllers/brla.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export const newKyc = async (
* @throws 500 - For any server-side errors during processing
*/
export const initiateKybLevel1 = async (
req: Request<unknown, unknown, unknown, { subAccountId?: string }>,
req: Request<unknown, { redirectUrl: string }, unknown, { subAccountId?: string }>,
res: Response<KybLevel1Response | BrlaErrorResponse>
): Promise<void> => {
try {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/api/routes/v1/alfredpay.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ router.post("/sendKycSubmission", requireAuth, validateResultCountry, AlfredpayC
// Business API-based KYB
router.post("/submitKybInformation", requireAuth, validateResultCountry, AlfredpayController.submitKybInformation);
router.post("/submitKybFile", requireAuth, upload.single("file"), validateResultCountry, AlfredpayController.submitKybFile);
router.get("/findKybCustomerAndBusiness", requireAuth, validateResultCountry, AlfredpayController.findKybCustomerAndBusiness);
router.post(
"/submitKybRelatedPersonFile",
requireAuth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
BrlaApiService,
BrlaCurrency,
checkEvmBalancePeriodically,
FiatToken,
getAnyFiatTokenDetailsMoonbeam,
EvmAddress,
EvmToken,
evmTokenConfig,
getEvmTokenBalance,
Networks,
RampPhase,
waitUntilTrueWithTimeout
Expand All @@ -28,7 +30,7 @@ import { StateMetadata } from "../meta-state-types";
const PAYMENT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const EVM_BALANCE_CHECK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes

// Phase description: wait for the tokens to arrive at the Moonbeam ephemeral address.
// Phase description: wait for the tokens to arrive at the Base ephemeral address.
// If the timeout is reached, we assume the user has NOT made the payment and we cancel the ramp.
export class BrlaOnrampMintHandler extends BasePhaseHandler {
public getPhaseName(): RampPhase {
Expand Down Expand Up @@ -63,6 +65,24 @@ export class BrlaOnrampMintHandler extends BasePhaseHandler {
});
}

const tokenDetails = evmTokenConfig[Networks.Base][EvmToken.BRLA];
if (!tokenDetails) {
throw new Error("BRLA token details not found for Base network");
}

const expectedAmountReceived = quote.metadata.aveniaTransfer.outputAmountRaw;

// Recovery shortcut: a previous run may have already minted on Avenia and
// transferred to the ephemeral. If the ephemeral already holds the expected
// amount, skip the Avenia balance wait and the (idempotent-but-wasteful)
// pay-out ticket creation.
if (await this.ephemeralAlreadyFunded(tokenDetails.erc20AddressSourceChain, evmEphemeralAddress, expectedAmountReceived)) {
logger.info(
`BrlaOnrampMintHandler: Ephemeral ${evmEphemeralAddress} already holds the expected ${expectedAmountReceived} BRLA. Skipping mint flow.`
);
return this.transitionToNextPhase(state, "fundEphemeral");
}

const brlaApiService = BrlaApiService.getInstance();
try {
logger.info(
Expand Down Expand Up @@ -106,7 +126,7 @@ export class BrlaOnrampMintHandler extends BasePhaseHandler {
inputPaymentMethod: AveniaPaymentMethod.INTERNAL,
inputThirdParty: false,
outputCurrency: BrlaCurrency.BRLA,
outputPaymentMethod: AveniaPaymentMethod.MOONBEAM,
outputPaymentMethod: AveniaPaymentMethod.BASE,
outputThirdParty: false,
subAccountId: taxIdRecord.subAccountId
});
Expand All @@ -118,29 +138,26 @@ export class BrlaOnrampMintHandler extends BasePhaseHandler {
quoteToken: aveniaQuote.quoteToken,
ticketBlockchainOutput: {
walletAddress: state.state.evmEphemeralAddress,
walletChain: AveniaPaymentMethod.MOONBEAM
walletChain: AveniaPaymentMethod.BASE
}
},
taxIdRecord.subAccountId
);

const expectedAmountReceived = quote.metadata.aveniaTransfer?.outputAmountRaw;

logger.info(
`BrlaOnrampMintHandler: Created Avenia transfer ticket with id ${aveniaTicket.id} to transfer ${quote.metadata.aveniaTransfer.outputAmountDecimal} BRLA to Moonbeam address ${state.state.evmEphemeralAddress}`
`BrlaOnrampMintHandler: Created Avenia transfer ticket with id ${aveniaTicket.id} to transfer ${quote.metadata.aveniaTransfer.outputAmountDecimal} BRLA to Base address ${state.state.evmEphemeralAddress}`
);

try {
const pollingTimeMs = 1000;
const tokenDetails = getAnyFiatTokenDetailsMoonbeam(FiatToken.BRL);

await checkEvmBalancePeriodically(
tokenDetails.moonbeamErc20Address,
tokenDetails.erc20AddressSourceChain,
evmEphemeralAddress,
expectedAmountReceived,
pollingTimeMs,
EVM_BALANCE_CHECK_TIMEOUT_MS,
Networks.Moonbeam
Networks.Base
);
} catch (error) {
if (!(error instanceof BalanceCheckError)) throw error;
Expand All @@ -153,12 +170,34 @@ export class BrlaOnrampMintHandler extends BasePhaseHandler {

throw isCheckTimeout
? this.createRecoverableError(`BrlaOnrampMintHandler: phase timeout reached with error: ${error}`)
: new Error(`Error checking Moonbeam balance: ${error}`);
: new Error(`Error checking Base balance: ${error}`);
}

return this.transitionToNextPhase(state, "fundEphemeral");
}

private async ephemeralAlreadyFunded(
tokenAddress: string,
ownerAddress: string,
expectedAmountRaw: string
): Promise<boolean> {
try {
const balance = await getEvmTokenBalance({
chain: Networks.Base,
ownerAddress: ownerAddress as EvmAddress,
tokenAddress: tokenAddress as EvmAddress
});
return balance.gte(new Big(expectedAmountRaw));
} catch (error) {
// Treat read failures as "not funded" so we fall through to the regular
// flow rather than aborting the phase on a transient RPC error.
logger.warn(
`BrlaOnrampMintHandler: ephemeral balance pre-check failed for ${ownerAddress}, falling back to Avenia flow: ${error}`
);
return false;
}
}

protected isPaymentTimeoutReached(state: RampState): boolean {
const thisPhaseEntry = state.phaseHistory.find(phaseHistoryEntry => phaseHistoryEntry.phase === this.getPhaseName());
if (!thisPhaseEntry) {
Expand Down
Loading
Loading