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
5 changes: 5 additions & 0 deletions .changeset/brave-melons-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

🔥 remove crypto ramp and liquidation addresses
5 changes: 5 additions & 0 deletions .changeset/lucky-otters-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

✨ drop crypto currencies for new customers
Comment thread
mainqueg marked this conversation as resolved.
71 changes: 2 additions & 69 deletions server/api/ramp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ const ErrorCodes = {
EXTERNAL_ACCOUNT_NOT_FOUND: "external account not found",
EXTERNAL_ACCOUNT_NOT_SUPPORTED: "external account not supported",
INVALID_BANK_NAME: "invalid bank name",
INVALID_DEPOSIT_ADDRESS: "invalid deposit address",
NO_CREDENTIAL: "no credential",
NOT_APPROVED: "not approved",
NOT_STARTED: "not started",
NOT_SUPPORTED: "not supported",
Comment thread
mainqueg marked this conversation as resolved.
POSTAL_CODE_REQUIRED: "postal code required",
WITHDRAWAL_IN_PROGRESS: "withdrawal in progress",
};
Expand Down Expand Up @@ -122,35 +122,6 @@ export default new Hono()
externalAccountId: string(),
provider: literal("bridge"),
}),
object({
address: Address,
currency: literal("USDC"),
direction: literal("offramp"),
network: literal("BASE"),
provider: literal("bridge"),
}),
object({
address: string(),
currency: literal("USDC"),
direction: literal("offramp"),
network: literal("SOLANA"),
provider: literal("bridge"),
}),
object({
address: string(),
currency: literal("USDC"),
direction: literal("offramp"),
memo: string(),
network: literal("STELLAR"),
provider: literal("bridge"),
}),
object({
address: string(),
currency: literal("USDT"),
direction: literal("offramp"),
network: literal("TRON"),
provider: literal("bridge"),
}),
object({
currency: literal("USDT"),
direction: optional(literal("onramp")),
Expand Down Expand Up @@ -212,29 +183,6 @@ export default new Hono()
const quote = (await bridge.getQuote("USD", query.currency)) satisfies QuoteResponse;

if (query.direction === "offramp") {
if ("network" in query) {
try {
return c.json(
{
quote,
depositInfo: await bridge.getCryptoOfframpDepositDetails(
query.currency,
query.network,
query.address,
parse(Address, credential.account),
bridgeUser,
query.network === "STELLAR" ? query.memo : undefined,
),
} satisfies InferOutput<typeof RampResponse>,
200,
);
} catch (error) {
if (error instanceof Error && error.message === bridge.ErrorCodes.INVALID_DEPOSIT_ADDRESS) {
return c.json({ code: ErrorCodes.INVALID_DEPOSIT_ADDRESS }, 400);
}
throw error;
}
}
try {
return c.json(
{
Expand Down Expand Up @@ -264,22 +212,7 @@ export default new Hono()
throw error;
}
}

if ("network" in query) {
return c.json(
{
quote,
depositInfo: await bridge.getCryptoDepositDetails(
query.currency,
query.network,
credential.account,
bridgeUser,
),
} satisfies InferOutput<typeof RampResponse>,
200,
);
}

if ("network" in query) return c.json({ code: ErrorCodes.NOT_SUPPORTED }, 400);
Comment thread
mainqueg marked this conversation as resolved.
Comment thread
mainqueg marked this conversation as resolved.
return c.json(
{
quote,
Expand Down
187 changes: 4 additions & 183 deletions server/test/api/ramp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,133 +542,16 @@ describe("ramp api", () => {
});
});

it("returns deposit info with undefined quote for bridge USDT/TRON", async () => {
it("returns 400 not supported for crypto onramp", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoDepositDetails").mockResolvedValue([
{
network: "TRON" as const,
displayName: "TRON" as const,
address: "TXyz123456789",
fee: "0.0",
estimatedProcessingTime: "300",
},
]);

const response = await appClient.quote.$get(
{ query: { provider: "bridge", currency: "USDT", network: "TRON" } },
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(200);
await expect(response.json()).resolves.toStrictEqual({
depositInfo: [
{
network: "TRON",
displayName: "TRON",
address: "TXyz123456789",
fee: "0.0",
estimatedProcessingTime: "300",
},
],
});
});

it("returns deposit info with default quote for bridge USDC/SOLANA", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoDepositDetails").mockResolvedValue([
{
network: "SOLANA" as const,
displayName: "SOLANA" as const,
address: "So1anaAddress123",
fee: "0.0",
estimatedProcessingTime: "300",
},
]);

const response = await appClient.quote.$get(
{ query: { provider: "bridge", currency: "USDC", network: "SOLANA" } },
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(200);
await expect(response.json()).resolves.toStrictEqual({
quote: { buyRate: "1.0", sellRate: "1.0" },
depositInfo: [
{
network: "SOLANA",
displayName: "SOLANA",
address: "So1anaAddress123",
fee: "0.0",
estimatedProcessingTime: "300",
},
],
});
});

it("returns deposit info with default quote for bridge USDC/STELLAR", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoDepositDetails").mockResolvedValue([
{
network: "STELLAR" as const,
displayName: "STELLAR" as const,
address: "STELLAR123456",
fee: "0.0",
estimatedProcessingTime: "300",
memo: "789012",
},
]);

const response = await appClient.quote.$get(
{ query: { provider: "bridge", currency: "USDC", network: "STELLAR" } },
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(200);
await expect(response.json()).resolves.toStrictEqual({
quote: { buyRate: "1.0", sellRate: "1.0" },
depositInfo: [
{
network: "STELLAR",
displayName: "STELLAR",
address: "STELLAR123456",
fee: "0.0",
estimatedProcessingTime: "300",
memo: "789012",
},
],
});
});

it("returns deposit info with default quote for bridge USDC/BASE", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoDepositDetails").mockResolvedValue([
{
network: "BASE" as const,
displayName: "BASE" as const,
address: deposit,
fee: "0.0",
estimatedProcessingTime: "300",
},
]);

const response = await appClient.quote.$get(
{ query: { provider: "bridge", currency: "USDC", network: "BASE" } },
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(200);
await expect(response.json()).resolves.toStrictEqual({
quote: { buyRate: "1.0", sellRate: "1.0" },
depositInfo: [
{
network: "BASE",
displayName: "BASE",
address: deposit,
fee: "0.0",
estimatedProcessingTime: "300",
},
],
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toStrictEqual({ code: "not supported" });
});

describe("offramp", () => {
Expand Down Expand Up @@ -796,69 +679,6 @@ describe("ramp api", () => {
"USD",
);
});

it("returns 400 when crypto offramp to_address is invalid", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoOfframpDepositDetails").mockRejectedValue(
new Error(bridge.ErrorCodes.INVALID_DEPOSIT_ADDRESS),
);

const response = await appClient.quote.$get(
{
query: {
provider: "bridge",
currency: "USDT",
direction: "offramp",
network: "TRON",
address: "not-a-tron-address",
},
},
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(400);
await expect(response.json()).resolves.toStrictEqual({ code: "invalid deposit address" });
});

it("returns 500 when crypto offramp util throws an unexpected error", async () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue(bridgeCustomer);
vi.spyOn(bridge, "getCryptoOfframpDepositDetails").mockRejectedValue(new Error("unexpected"));

const response = await appClient.quote.$get(
{
query: {
provider: "bridge",
currency: "USDT",
direction: "offramp",
network: "TRON",
address: "TXyz",
},
},
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(500);
});

it("returns 400 when STELLAR offramp is missing the memo", async () => {
const cryptoSpy = vi.spyOn(bridge, "getCryptoOfframpDepositDetails");

const response = await appClient.quote.$get(
{
query: {
provider: "bridge",
currency: "USDC",
direction: "offramp",
network: "STELLAR",
address: "GABCDEFGHIJ",
} as never,
},
{ headers: { "test-credential-id": "ramp-bridge" } },
);

expect(response.status).toBe(400);
expect(cryptoSpy).not.toHaveBeenCalled();
});
});
});
});
Expand Down Expand Up @@ -1685,6 +1505,7 @@ const mantecaUser = {

const bridgeCustomer = {
id: "bridge-customer-123",
created_at: "2026-07-01T00:00:00.000Z",
email: "test@example.com",
status: "active" as const,
endorsements: [],
Expand Down
5 changes: 5 additions & 0 deletions server/test/hooks/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ describe("bridge hook", () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue({
id: "fallback-bridge-id",
email: "fallback@example.com",
created_at: "2026-07-01T00:00:00.000Z",
status: "active",
endorsements: [],
});
Expand Down Expand Up @@ -307,6 +308,7 @@ describe("bridge hook", () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue({
id: "conflict-bridge-id",
email: "conflict@example.com",
created_at: "2026-07-01T00:00:00.000Z",
status: "active",
endorsements: [],
});
Expand All @@ -329,6 +331,7 @@ describe("bridge hook", () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue({
id: "multi-bridge-id",
email: "multi@example.com",
created_at: "2026-07-01T00:00:00.000Z",
status: "active",
endorsements: [],
});
Expand Down Expand Up @@ -357,6 +360,7 @@ describe("bridge hook", () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue({
id: "empty-bridge-id",
email: "empty@example.com",
created_at: "2026-07-01T00:00:00.000Z",
status: "active",
endorsements: [],
});
Expand All @@ -382,6 +386,7 @@ describe("bridge hook", () => {
vi.spyOn(bridge, "getCustomer").mockResolvedValue({
id: "orphan-bridge-id",
email: "orphan@example.com",
created_at: "2026-07-01T00:00:00.000Z",
status: "active",
endorsements: [],
});
Expand Down
Loading