Skip to content
Merged
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
110 changes: 110 additions & 0 deletions api/routes/__tests__/utxo-tx-next-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import type { NextApiRequest, NextApiResponse } from "next";
import handler from "../../../pages/api/utxo/tx/[txid]";

type MockResponse = NextApiResponse & {
body?: unknown;
statusCode: number;
};

const createResponse = (): MockResponse => {
const headers = new Map<string, string | string[]>();
const response: {
body?: unknown;
statusCode: number;
[key: string]: unknown;
} = {
statusCode: 200,
status(code: number) {
this.statusCode = code;
return this;
},
json(body: unknown) {
this.body = body;
return this;
},
send(body: unknown) {
this.body = body;
return this;
},
end() {
return this;
},
setHeader(name: string, value: string | string[]) {
headers.set(name, value);
return this;
},
getHeader(name: string) {
return headers.get(name);
},
};
return response as unknown as MockResponse;
};

const createRequest = (txid = "ab".repeat(32)) =>
({
method: "GET",
headers: {},
query: { txid },
socket: {},
} as unknown as NextApiRequest);

const originalEnvironment = { ...process.env };

afterEach(() => {
process.env = { ...originalEnvironment };
jest.restoreAllMocks();
});

describe("UTXO transaction proxy", () => {
it("returns only immutable raw transaction data", async () => {
process.env.UTXO_EXPLORER = "https://testnet-blockbook.example";
const txid = "ab".repeat(32);
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ confirmations: 5, hex: "00a1" }),
} as Response);
const response = createResponse();

await handler(createRequest(txid), response);

expect(fetchMock).toHaveBeenCalledWith(
`https://testnet-blockbook.example/api/v2/tx/${txid}`,
{ headers: { Accept: "application/json" } }
);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ hex: "00a1" });
expect(response.getHeader("Cache-Control")).toContain("immutable");
});

it("rejects malformed transaction IDs without contacting Blockbook", async () => {
process.env.UTXO_EXPLORER = "https://testnet-blockbook.example";
const fetchMock = jest.spyOn(global, "fetch");
const response = createResponse();

await handler(createRequest("not-a-txid"), response);

expect(fetchMock).not.toHaveBeenCalled();
expect(response.statusCode).toBe(400);
expect(response.body).toEqual({ message: "Invalid transaction ID" });
});

it("rejects a Blockbook response without valid transaction hex", async () => {
process.env.UTXO_EXPLORER = "https://testnet-blockbook.example";
jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ hex: "not-hex" }),
} as Response);
const response = createResponse();

await handler(createRequest(), response);

expect(response.statusCode).toBe(502);
expect(response.body).toEqual({
message: "Blockbook returned an invalid transaction",
});
});
});

9 changes: 6 additions & 3 deletions api/services/__tests__/sponsor-wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ const mockWeb3 = {
toHex: jest.fn((value) => value),
},
};
const mockExportPsbtWithPrevouts = jest.fn<any>();

jest.mock("utils/get-web3", () => ({
__esModule: true,
default: mockWeb3,
}));

jest.mock("utils/psbt-prevouts", () => ({
exportPsbtWithPrevouts: mockExportPsbtWithPrevouts,
}));

jest.mock("models/sponsor-wallet-transactions", () => {
const Model: any = jest.fn(function (this: any, data: any) {
Object.assign(this, data);
Expand Down Expand Up @@ -1073,9 +1078,7 @@ describe("SponsorWalletService", () => {
jest
.spyOn(service, "getUserInputFingerprint")
.mockReturnValue("fingerprint");
(syscoinUtils.exportPsbtToJson as jest.Mock<any>).mockReturnValue(
exported
);
mockExportPsbtWithPrevouts.mockResolvedValue(exported);
const store = jest
.spyOn(service, "storePreparedUtxoBurn")
.mockResolvedValue(undefined);
Expand Down
7 changes: 5 additions & 2 deletions api/services/sponsor-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SponsorWalletTransactions, {
} from "models/sponsor-wallet-transactions";
import { syscoin, UTXOTransaction, utils as syscoinUtils } from "syscoinjs-lib";
import web3 from "utils/get-web3";
import { exportPsbtWithPrevouts } from "utils/psbt-prevouts";
import { toSyscoinBaseUnits } from "utils/syscoin-amount";
import {
MAINNET_BLOCKBOOK_URL,
Expand Down Expand Up @@ -406,9 +407,11 @@ export class SponsorWalletService {
prepared.psbt,
reservation.key
);
const exported = syscoinUtils.exportPsbtToJson(
const exported = await exportPsbtWithPrevouts(
prepared.psbt,
prepared.assets
prepared.assets,
(txid) =>
syscoinUtils.fetchBackendRawTx(getUtxoBlockbookUrl(), txid)
);
await this.storePreparedUtxoBurn(
placeholder,
Expand Down
3 changes: 2 additions & 1 deletion components/Bridge/hooks/useMintSysx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePaliWalletV2 } from "@contexts/PaliWallet/usePaliWallet";
import { useConstants } from "@contexts/useConstants";
import { useFeatureFlags } from "./useFeatureFlags";
import { requestSponsoredUtxo } from "./sponsored-utxo";
import { exportPsbtWithPrevouts } from "utils/psbt-prevouts";

export const useMintSysx = (transfer: ITransfer) => {
const syscoinInstance = useSyscoin();
Expand Down Expand Up @@ -46,7 +47,7 @@ export const useMintSysx = (transfer: ITransfer) => {
throw new Error("Unable to mint SYSX: insufficient SYS for fees");
}

const psbt = utils.exportPsbtToJson(res.psbt, res.assets);
const psbt = await exportPsbtWithPrevouts(res.psbt, res.assets);
const { tx, error } = await sendTransaction(psbt);

if (error) {
Expand Down
3 changes: 2 additions & 1 deletion contexts/Transfer/functions/burnSysToSysx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { syscoin, utils as syscoinUtils } from "syscoinjs-lib";
import { toSyscoinBaseUnits } from "utils/syscoin-amount";
import { exportPsbtWithPrevouts } from "utils/psbt-prevouts";
import { SYSX_ASSET_GUID } from "../constants";

export const burnSysToSysx = async (
Expand Down Expand Up @@ -44,7 +45,7 @@ export const burnSysToSysx = async (
throw new Error("Unable to create the SYS burn transaction");
}
console.log("burnSysToSysx", { res });
return syscoinUtils.exportPsbtToJson(res.psbt, res.assets);
return exportPsbtWithPrevouts(res.psbt, res.assets);
};

export default burnSysToSysx;
3 changes: 2 additions & 1 deletion contexts/Transfer/functions/burnSysx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { syscoin, utils as syscoinUtils } from "syscoinjs-lib";
import { toSyscoinBaseUnits } from "utils/syscoin-amount";
import { exportPsbtWithPrevouts } from "utils/psbt-prevouts";
import { SYSX_ASSET_GUID } from "../constants";

export const burnSysx = async (
Expand Down Expand Up @@ -48,7 +49,7 @@ export const burnSysx = async (
cause: res,
});
}
return syscoinUtils.exportPsbtToJson(res.psbt, res.assets);
return exportPsbtWithPrevouts(res.psbt, res.assets);
};

export default burnSysx;
3 changes: 2 additions & 1 deletion contexts/Transfer/functions/nevmToSys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { COMMON_STATUS, ETH_TO_SYS_TRANSFER_STATUS, ITransfer } from "../types";
import { syscoin, utils } from "syscoinjs-lib";
import { SendUtxoTransaction } from "@contexts/ConnectedWallet/Provider";
import burnSysx from "./burnSysx";
import { exportPsbtWithPrevouts } from "utils/psbt-prevouts";
import { toWei } from "web3-utils";
import { captureException } from "@sentry/nextjs";
import { useErc20ManagerContract } from "components/Bridge/hooks/useErc20ManagerContract";
Expand Down Expand Up @@ -173,7 +174,7 @@ const mintSysx = async (
console.log("assetAllocationMint received", {
res,
});
const transaction = utils.exportPsbtToJson(res.psbt, res.assets);
const transaction = await exportPsbtWithPrevouts(res.psbt, res.assets);
const mintSysxTransactionReceipt = await sendUtxoTransaction(transaction);
dispatch(
addLog(
Expand Down
67 changes: 67 additions & 0 deletions pages/api/utxo/tx/[txid].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { applyApiCors } from "utils/api/cors";
import { firstConfiguredUtxoBlockbookUrl } from "utils/syscoin-urls";

const firstQueryValue = (value: string | string[] | undefined) =>
Array.isArray(value) ? value[0] : value;

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (
applyApiCors(req, res, {
allowMethods: ["GET", "OPTIONS"],
allowWildcardOrigin: true,
})
) {
return;
}

if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}

const txid = firstQueryValue(req.query.txid);
if (!txid || !/^[0-9a-fA-F]{64}$/.test(txid)) {
return res.status(400).json({ message: "Invalid transaction ID" });
}

const blockbookUrl = firstConfiguredUtxoBlockbookUrl(
process.env.UTXO_EXPLORER,
process.env.UTXO_RPC_URL,
process.env.NEXT_PUBLIC_BLOCKBOOK_API_URL
);
if (!blockbookUrl) {
return res
.status(503)
.json({ message: "UTXO Blockbook is not configured" });
}

try {
const response = await fetch(`${blockbookUrl}/api/v2/tx/${txid}`, {
headers: { Accept: "application/json" },
});
if (!response.ok) {
return res
.status(response.status)
.json({ message: "Unable to fetch transaction" });
}
const transaction = (await response.json()) as { hex?: unknown };
if (
typeof transaction.hex !== "string" ||
transaction.hex.length % 2 !== 0 ||
!/^[0-9a-fA-F]+$/.test(transaction.hex)
) {
return res
.status(502)
.json({ message: "Blockbook returned an invalid transaction" });
}
res.setHeader(
"Cache-Control",
"public, max-age=300, s-maxage=31536000, immutable"
);
return res.status(200).json({ hex: transaction.hex });
} catch {
return res.status(502).json({ message: "UTXO Blockbook is unavailable" });
}
};

export default handler;
Loading
Loading