Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,60 @@ console.log("Please do the pix transfer using the following code: ", depositQrCo
const startedRamp = await sdk.startRamp(rampProcess.id);
```

### Alfredpay (USD / MXN / COP) onramp

```typescript
import { VortexSdk, FiatToken, EvmToken, Networks, RampDirection } from "@vortexfi/sdk";

const sdk = new VortexSdk({ apiBaseUrl: "http://localhost:3000" });

const quote = await sdk.createQuote({
inputAmount: "100",
inputCurrency: FiatToken.COP,
outputCurrency: EvmToken.USDC,
rampType: RampDirection.BUY,
to: Networks.Polygon
});

const { rampProcess } = await sdk.registerRamp(quote, {
destinationAddress: "0x1234567890123456789012345678901234567890",
fiatAccountId: "<the user's Alfredpay fiat account id>",
walletAddress: "0x1234567890123456789012345678901234567890"
});

// Inspect off-chain fiat payment instructions before starting.
const startedRamp = await sdk.startRamp(rampProcess.id);
console.log("Pay via:", startedRamp.achPaymentData);
```

### Alfredpay (USD / MXN / COP) offramp

```typescript
const quote = await sdk.createQuote({
from: Networks.Polygon,
inputAmount: "10",
inputCurrency: EvmToken.USDC,
outputCurrency: FiatToken.MXN,
rampType: RampDirection.SELL
});

const { rampProcess, unsignedTransactions } = await sdk.registerRamp(quote, {
fiatAccountId: "<the user's Alfredpay fiat account id>",
walletAddress: "0x1234567890123456789012345678901234567890"
});

// Sign and submit the user-side EVM transactions (squidRouter approve/swap, etc.)
// then push the resulting hashes back to Vortex:
const updated = await sdk.updateRamp(quote, rampProcess.id, {
squidRouterApproveHash: "0x...",
squidRouterSwapHash: "0x..."
});

const startedRamp = await sdk.startRamp(updated.id);
```

> `fiatAccountId` is opaque to the SDK. Consumers create or look up the user's Alfredpay fiat account out-of-band (via the Vortex backend) and pass the ID in.

## Core Features
- **Ephemerals abstracted**: No need to keep track of the ephemeral accounts used in the ramp process. If `storeEphemeralKeys` is enabled, keys are stored in a JSON file in Node.js.
- **Stateless Design**: No internal state management - you control persistence of the rampId for status checking
Expand Down
38 changes: 34 additions & 4 deletions packages/sdk/src/VortexSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import {
createStellarEphemeral,
EphemeralAccount,
EphemeralAccountType,
isAlfredpayToken,
QuoteResponse,
RampDirection,
RampProcess,
signUnsignedTransactions,
UnsignedTx
} from "@vortexfi/shared";
import { TransactionSigningError } from "./errors";
import { AlfredpayHandler } from "./handlers/AlfredpayHandler";
import { BrlHandler } from "./handlers/BrlHandler";
import { ApiService } from "./services/ApiService";
import { NetworkManager } from "./services/NetworkManager";
import { storeEphemeralKeys } from "./storage";
import type {
AlfredpayOfframpAdditionalData,
AlfredpayOfframpUpdateAdditionalData,
AlfredpayOnrampAdditionalData,
BrlOfframpAdditionalData,
BrlOfframpUpdateAdditionalData,
BrlOnrampAdditionalData,
Expand All @@ -32,6 +37,7 @@ export class VortexSdk {
private publicKey: string | undefined;
private networkManager: NetworkManager;
private brlHandler: BrlHandler;
private alfredpayHandler: AlfredpayHandler;
private initializationPromise: Promise<void>;
private storeEphemeralKeys: boolean;

Expand All @@ -48,6 +54,13 @@ export class VortexSdk {
this.signTransactions.bind(this)
);

this.alfredpayHandler = new AlfredpayHandler(
this.apiService,
this,
this.generateEphemerals.bind(this),
this.signTransactions.bind(this)
);

this.initializationPromise = this.networkManager.waitForInitialization();
}

Expand Down Expand Up @@ -86,7 +99,13 @@ export class VortexSdk {
let unsignedTransactions: UnsignedTx[] = [];

if (quote.rampType === RampDirection.BUY) {
if (quote.from === "pix") {
if (isAlfredpayToken(quote.inputCurrency)) {
rampProcess = await this.alfredpayHandler.registerAlfredpayOnramp(
quote.id,
additionalData as AlfredpayOnrampAdditionalData
);
unsignedTransactions = [];
} else if (quote.from === "pix") {
rampProcess = await this.brlHandler.registerBrlOnramp(quote.id, additionalData as BrlOnrampAdditionalData);
unsignedTransactions = [];
} else if (quote.from === "sepa") {
Expand All @@ -95,7 +114,11 @@ export class VortexSdk {
throw new Error(`Unsupported onramp from: ${quote.from}`);
}
} else if (quote.rampType === RampDirection.SELL) {
if (quote.to === "pix") {
if (isAlfredpayToken(quote.outputCurrency)) {
const offrampData = additionalData as AlfredpayOfframpAdditionalData;
rampProcess = await this.alfredpayHandler.registerAlfredpayOfframp(quote.id, offrampData);
unsignedTransactions = await this.getUserTransactions(rampProcess, offrampData.walletAddress);
} else if (quote.to === "pix") {
rampProcess = await this.brlHandler.registerBrlOfframp(quote.id, additionalData as BrlOfframpAdditionalData);
const userAddress = (additionalData as BrlOfframpAdditionalData).walletAddress;
unsignedTransactions = await this.getUserTransactions(rampProcess, userAddress);
Expand All @@ -117,13 +140,20 @@ export class VortexSdk {
additionalUpdateData: UpdateRampAdditionalData<Q>
): Promise<RampProcess> {
if (quote.rampType === RampDirection.BUY) {
if (quote.from === "pix") {
if (isAlfredpayToken(quote.inputCurrency)) {
throw new Error("Alfredpay onramp does not require any further data");
} else if (quote.from === "pix") {
throw new Error("Brl onramp does not require any further data");
} else if (quote.from === "sepa") {
throw new Error("Euro onramp handler not implemented yet");
}
} else if (quote.rampType === RampDirection.SELL) {
if (quote.to === "pix") {
if (isAlfredpayToken(quote.outputCurrency)) {
return this.alfredpayHandler.updateAlfredpayOfframp(
rampId,
additionalUpdateData as AlfredpayOfframpUpdateAdditionalData
);
} else if (quote.to === "pix") {
return this.brlHandler.updateBrlOfframp(rampId, additionalUpdateData as BrlOfframpUpdateAdditionalData);
} else if (quote.to === "sepa") {
throw new Error("Euro offramp handler not implemented yet");
Expand Down
33 changes: 33 additions & 0 deletions packages/sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,36 @@ export class InvalidPixKeyError extends BrlOfframpError {
}
}

// Alfredpay Onramp specific errors
export class AlfredpayOnrampError extends RegisterRampError {
constructor(message: string, status = 400) {
super(message, status);
this.name = "AlfredpayOnrampError";
}
}

export class MissingAlfredpayOnrampParametersError extends AlfredpayOnrampError {
constructor() {
super("Parameters destinationAddress and fiatAccountId are required for Alfredpay onramp", 400);
this.name = "MissingAlfredpayOnrampParametersError";
}
}

// Alfredpay Offramp specific errors
export class AlfredpayOfframpError extends RegisterRampError {
constructor(message: string, status = 400) {
super(message, status);
this.name = "AlfredpayOfframpError";
}
}

export class MissingAlfredpayOfframpParametersError extends AlfredpayOfframpError {
constructor() {
super("Parameters fiatAccountId and walletAddress are required for Alfredpay offramp", 400);
this.name = "MissingAlfredpayOfframpParametersError";
}
}

// Monerium specific errors
export class MoneriumError extends RegisterRampError {
constructor(message: string, status = 400) {
Expand Down Expand Up @@ -362,6 +392,9 @@ export function parseAPIError(response: any): VortexSdkError {
if (errorMessage === "Invalid pixKey or receiverTaxId") {
return new InvalidPixKeyError();
}
if (errorMessage === "Parameter destinationAddress is required for Alfredpay onramp") {
return new MissingAlfredpayOnrampParametersError();
}
if (errorMessage === "Parameters moneriumAuthToken and destinationAddress are required for Monerium onramp") {
return new MissingMoneriumOnrampParametersError();
}
Expand Down
151 changes: 151 additions & 0 deletions packages/sdk/src/handlers/AlfredpayHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
AccountMeta,
EphemeralAccount,
EphemeralAccountType,
RampProcess,
RegisterRampRequest,
UnsignedTx,
UpdateRampRequest
} from "@vortexfi/shared";
import { MissingAlfredpayOfframpParametersError, MissingAlfredpayOnrampParametersError } from "../errors";
import type { ApiService } from "../services/ApiService";
import type {
AlfredpayOfframpAdditionalData,
AlfredpayOfframpUpdateAdditionalData,
AlfredpayOnrampAdditionalData,
RampHandler,
VortexSdkContext
} from "../types";

export class AlfredpayHandler implements RampHandler {
private apiService: ApiService;
private context: VortexSdkContext;
private generateEphemerals: () => Promise<{
ephemerals: { [key in EphemeralAccountType]?: EphemeralAccount };
accountMetas: AccountMeta[];
}>;
private signTransactions: (
unsignedTxs: UnsignedTx[],
ephemerals: {
stellarEphemeral?: EphemeralAccount;
substrateEphemeral?: EphemeralAccount;
evmEphemeral?: EphemeralAccount;
}
) => Promise<any[]>;

constructor(
apiService: ApiService,
context: VortexSdkContext,
generateEphemerals: () => Promise<{
ephemerals: { [key in EphemeralAccountType]?: EphemeralAccount };
accountMetas: AccountMeta[];
}>,
signTransactions: (
unsignedTxs: UnsignedTx[],
ephemerals: {
stellarEphemeral?: EphemeralAccount;
substrateEphemeral?: EphemeralAccount;
evmEphemeral?: EphemeralAccount;
}
) => Promise<any[]>
) {
this.apiService = apiService;
this.context = context;
this.generateEphemerals = generateEphemerals;
this.signTransactions = signTransactions;
}

async registerAlfredpayOnramp(quoteId: string, additionalData: AlfredpayOnrampAdditionalData): Promise<RampProcess> {
if (!additionalData.destinationAddress || !additionalData.fiatAccountId) {
throw new MissingAlfredpayOnrampParametersError();
}

const { ephemerals, accountMetas } = await this.generateEphemerals();

const registerRequest: RegisterRampRequest = {
additionalData: {
destinationAddress: additionalData.destinationAddress,
fiatAccountId: additionalData.fiatAccountId,
sessionId: additionalData.sessionId,
walletAddress: additionalData.walletAddress
},
quoteId,
signingAccounts: accountMetas
};

const rampProcess = await this.apiService.registerRamp(registerRequest);

await this.context.storeEphemerals(ephemerals, rampProcess.id);

const signedTxs = await this.signTransactions(rampProcess.unsignedTxs || [], {
evmEphemeral: ephemerals.EVM,
stellarEphemeral: ephemerals.Stellar,
substrateEphemeral: ephemerals.Substrate
});

const updateRequest: UpdateRampRequest = {
additionalData: {},
presignedTxs: signedTxs,
rampId: rampProcess.id
};

return this.apiService.updateRamp(updateRequest);
}

async registerAlfredpayOfframp(quoteId: string, additionalData: AlfredpayOfframpAdditionalData): Promise<RampProcess> {
if (!additionalData.fiatAccountId || !additionalData.walletAddress) {
throw new MissingAlfredpayOfframpParametersError();
}

const { ephemerals, accountMetas } = await this.generateEphemerals();

const registerRequest: RegisterRampRequest = {
additionalData: {
fiatAccountId: additionalData.fiatAccountId,
sessionId: additionalData.sessionId,
walletAddress: additionalData.walletAddress
},
quoteId,
signingAccounts: accountMetas
};

const rampProcess = await this.apiService.registerRamp(registerRequest);

await this.context.storeEphemerals(ephemerals, rampProcess.id);

const signedTxs = await this.signTransactions(rampProcess.unsignedTxs || [], {
evmEphemeral: ephemerals.EVM,
stellarEphemeral: ephemerals.Stellar,
substrateEphemeral: ephemerals.Substrate
});

const updateRequest: UpdateRampRequest = {
additionalData: {},
presignedTxs: signedTxs,
rampId: rampProcess.id
};

return this.apiService.updateRamp(updateRequest);
}

async updateAlfredpayOfframp(rampId: string, additionalData: AlfredpayOfframpUpdateAdditionalData): Promise<RampProcess> {
const rampProcess = await this.apiService.getRampStatus(rampId);
if (rampProcess.currentPhase !== "initial") {
throw new Error(
`Invalid ramp id. Ramp must be on initial phase to be updated. Current phase: ${rampProcess.currentPhase}`
);
}

const updateRequest: UpdateRampRequest = {
additionalData: {
assethubToPendulumHash: additionalData.assethubToPendulumHash,
squidRouterApproveHash: additionalData.squidRouterApproveHash,
squidRouterSwapHash: additionalData.squidRouterSwapHash
},
presignedTxs: [],
rampId: rampProcess.id
};

return this.apiService.updateRamp(updateRequest);
}
}
Loading
Loading