diff --git a/apps/playground/src/components/cardano/mesh-wallet.ts b/apps/playground/src/components/cardano/mesh-wallet.ts index 971476ca9..295dd4107 100644 --- a/apps/playground/src/components/cardano/mesh-wallet.ts +++ b/apps/playground/src/components/cardano/mesh-wallet.ts @@ -2,7 +2,7 @@ import { BlockfrostProvider, MeshWallet } from "@meshsdk/core"; export function getProvider(network = "preprod") { const provider = new BlockfrostProvider( - `/api/blockfrost/${network}/`, + `https://cardano-${network}.blockfrost.io/api/v0/`, ); provider.setSubmitTxToBytes(false); return provider; diff --git a/apps/playground/src/pages/providers/blockfrost.tsx b/apps/playground/src/pages/providers/blockfrost.tsx index e8d2f78f6..a6001e954 100644 --- a/apps/playground/src/pages/providers/blockfrost.tsx +++ b/apps/playground/src/pages/providers/blockfrost.tsx @@ -43,7 +43,8 @@ const ReactPage: NextPage = () => { const blockfrostKey = useProviders((state) => state.blockfrostKey); - const provider = new BlockfrostProvider(blockfrostKey ?? ""); + // replace with your Blockfrost API key + const provider = new BlockfrostProvider(blockfrostKey ?? "preprod12345"); return ( <> diff --git a/packages/mesh-common/src/data/parser.ts b/packages/mesh-common/src/data/parser.ts index 5cbb22ade..db9cecf98 100644 --- a/packages/mesh-common/src/data/parser.ts +++ b/packages/mesh-common/src/data/parser.ts @@ -28,7 +28,8 @@ export const stringToHex = (str: string) => * @param hex The string to be checked * @returns True if the string is a hex string, false otherwise */ -export const isHexString = (hex: string) => /^[0-9A-F]*$/i.test(hex); +export const isHexString = (hex: string) => + hex.length % 2 === 0 && /^[0-9A-F]*$/i.test(hex); /** * Converting hex string to utf8 string @@ -43,9 +44,8 @@ export const hexToString = (hex: string) => * @param hex The hex or utf8 string to be converted * @returns The bytes */ -export const toBytes = (hex: string): Uint8Array => { - if (hex.length % 2 === 0 && /^[0-9A-F]*$/i.test(hex)) - return Buffer.from(hex, "hex"); +export const toBytes = (hex: string): Buffer => { + if (isHexString(hex)) return Buffer.from(hex, "hex"); return Buffer.from(hex, "utf-8"); }; @@ -56,8 +56,7 @@ export const toBytes = (hex: string): Uint8Array => { * @returns The hex string */ export const fromUTF8 = (utf8: string) => { - if (utf8.length % 2 === 0 && /^[0-9A-F]*$/i.test(utf8)) return utf8; - return bytesToHex(Buffer.from(utf8, "utf-8")); + return Buffer.from(utf8, "utf-8").toString("hex"); }; /** diff --git a/packages/mesh-common/test/data/parser.test.ts b/packages/mesh-common/test/data/parser.test.ts new file mode 100644 index 000000000..95d019237 --- /dev/null +++ b/packages/mesh-common/test/data/parser.test.ts @@ -0,0 +1,60 @@ +import { + bytesToHex, + fromUTF8, + hexToBytes, + hexToString, + isHexString, + parseAssetUnit, + stringToHex, + toBytes, + toUTF8, +} from "@meshsdk/core"; + +describe("Data parser utils", () => { + const hexStr = "626f6f6f"; + const str = "booo"; + const buf = Buffer.from(str); + const asset = + "d65287fa32b1c9880150b548ce32d503000152fe1cc3e3947eb151901370db9a"; + const assetUnit = { + policyId: "d65287fa32b1c9880150b548ce32d503000152fe1cc3e3947eb15190", + assetName: "1370db9a", + }; + + it("converts bytes array to hex string", () => { + expect(bytesToHex(buf)).toStrictEqual(hexStr); + }); + + it("converts hex string to bytes array", () => { + expect(hexToBytes(hexStr)).toStrictEqual(buf); + }); + + it("converts utf-8 string to hex string", () => { + expect(stringToHex(str)).toStrictEqual(hexStr); + }); + + it("verifies hex string correctness", () => { + expect(isHexString(hexStr)).toBeTruthy(); + }); + + it("converts hex string to utf-8 string", () => { + expect(hexToString(hexStr)).toStrictEqual(str); + }); + + it("converts hex or utf-8 string to bytes", () => { + expect(toBytes(hexStr)).toStrictEqual(buf); + expect(toBytes(str)).toStrictEqual(buf); + }); + + it("converts utf-8 string to hex string", () => { + expect(fromUTF8(str)).toStrictEqual(hexStr); + }); + + it("converts hex string to utf-8 string", () => { + expect(toUTF8(hexStr)).toStrictEqual(str); + }); + + it("parse asset unit into an object with policyId and assetName", () => { + expect(parseAssetUnit(asset)).toStrictEqual(assetUnit); + }); +}); diff --git a/packages/mesh-provider/src/blockfrost.ts b/packages/mesh-provider/src/blockfrost.ts index 0f9cfc00f..441ab51f2 100644 --- a/packages/mesh-provider/src/blockfrost.ts +++ b/packages/mesh-provider/src/blockfrost.ts @@ -35,6 +35,7 @@ import { OfflineFetcher } from "./offline/offline-fetcher"; import { BlockfrostAsset, BlockfrostUTxO } from "./types"; import { parseHttpError } from "./utils"; import { parseAssetUnit } from "./utils/parse-asset-unit"; +import { inferNetworkFromURL, isURL } from "./utils/url"; export type BlockfrostCachingOptions = { enableCaching?: boolean; @@ -78,21 +79,40 @@ export class BlockfrostProvider * @param version The version of the API. Default is 0. * @param cachingOptions Optional caching configuration */ - constructor(projectId: string, version?: number, cachingOptions?: BlockfrostCachingOptions); + constructor( + projectId: string, + version?: number, + cachingOptions?: BlockfrostCachingOptions, + ); constructor(...args: unknown[]) { let cachingOptions: BlockfrostCachingOptions | undefined; - - if ( - typeof args[0] === "string" && - (args[0].startsWith("http") || args[0].startsWith("/")) - ) { - this._axiosInstance = axios.create({ baseURL: args[0] }); - this._network = "mainnet"; + + const first = args[0]; + if (typeof first !== "string") { + throw new Error("First argument must of type string"); + } else if (first.length === 0) { + throw new Error("First argument cannot be an empty string"); + } + + if (isURL(first)) { + this._axiosInstance = axios.create({ baseURL: first }); + const n = inferNetworkFromURL(new URL(first)); + if (n === undefined) + throw new Error("Provided URL have an unrecognized network"); + this._network = n; cachingOptions = args[1] as BlockfrostCachingOptions | undefined; } else { - const projectId = args[0] as string; + const projectId = first as string; const network = projectId.slice(0, 7); + + const supported = ["mainnet", "preprod", "preview"]; + if (!supported.includes(network)) { + throw new Error( + `Unsupported type of network: ${network}. Please provide one of the following: ${supported}`, + ); + } + this._axiosInstance = axios.create({ baseURL: `https://cardano-${network}.blockfrost.io/api/v${ args[1] ?? 0 diff --git a/packages/mesh-provider/src/utils/parse-http-error.ts b/packages/mesh-provider/src/utils/parse-http-error.ts index 86d8a229e..93faecd81 100644 --- a/packages/mesh-provider/src/utils/parse-http-error.ts +++ b/packages/mesh-provider/src/utils/parse-http-error.ts @@ -8,7 +8,11 @@ export const parseHttpError = (error: unknown): string => { headers: error.response.headers, status: error.response.status, }); - } else if (error.request && !(error.request instanceof XMLHttpRequest)) { + } else if ( + typeof XMLHttpRequest !== "undefined" && + error.request && + !(error.request instanceof XMLHttpRequest) + ) { return JSON.stringify(error.request); } else { return JSON.stringify({ code: error.code, message: error.message }); diff --git a/packages/mesh-provider/src/utils/url.ts b/packages/mesh-provider/src/utils/url.ts new file mode 100644 index 000000000..a30e3fca5 --- /dev/null +++ b/packages/mesh-provider/src/utils/url.ts @@ -0,0 +1,22 @@ +import { BlockfrostSupportedNetworks } from "../blockfrost"; + +export function isURL(str: string) { + try { + new URL(str); + return true; + } catch (e) { + return false; + } +} + +export function inferNetworkFromURL( + u: URL, +): BlockfrostSupportedNetworks | undefined { + let h = u.hostname.toLowerCase(); + + if (h.includes("cardano-mainnet")) return "mainnet"; + if (h.includes("cardano-preprod")) return "preprod"; + if (h.includes("cardano-preview")) return "preview"; + + return undefined; +} diff --git a/packages/mesh-provider/test/blockfrost/constructor.test.ts b/packages/mesh-provider/test/blockfrost/constructor.test.ts new file mode 100644 index 000000000..f5432951e --- /dev/null +++ b/packages/mesh-provider/test/blockfrost/constructor.test.ts @@ -0,0 +1,13 @@ +import { BlockfrostProvider } from "@meshsdk/provider"; + +describe("Blockfrost constructor", () => { + it("fails to initiate a new instance with a wrong first argument", () => { + expect(() => new BlockfrostProvider({})).toThrow(); + expect(() => new BlockfrostProvider("")).toThrow(); + expect(() => new BlockfrostProvider("http://google.com")).toThrow(); + expect( + () => + new BlockfrostProvider("`https://cardano-testnet.blockfrost.io/api/v1"), + ).toThrow(); + }); +}); diff --git a/packages/mesh-provider/test/blockfrost/evaluator.test.ts b/packages/mesh-provider/test/blockfrost/evaluator.test.ts index a7f31e314..5a24a2fcc 100644 --- a/packages/mesh-provider/test/blockfrost/evaluator.test.ts +++ b/packages/mesh-provider/test/blockfrost/evaluator.test.ts @@ -4,7 +4,10 @@ import { BlockfrostProvider } from "@meshsdk/provider"; dotenv.config(); const apiKey = process.env.BLOCKFROST_API_KEY_PREPROD; -const provider = new BlockfrostProvider(apiKey ?? "", 0); +const provider = new BlockfrostProvider( + apiKey ?? "https://cardano-preprod.blockfrost.io/api/v1", + 0, +); const successTx = "84a70081825820859d3b4fd3a4c012b43ee1bbbc99240aec1827c3b8a74b867d10a7f4759149bc00018382583900e4cfbbc317c718f78d137b6535d8940618cc3d2ac04f1f35acf78e53a1521c2cea3cc79762d575581e47ea60b8eaa03430716cfd6140c796821a0011b0dea1581c67dd133868f14107b25772f3c5abaa1e0549f4b400b5e0e3a1136152a149000643b0546573743101a300581d7067dd133868f14107b25772f3c5abaa1e0549f4b400b5e0e3a113615201821a001ad510a1581c67dd133868f14107b25772f3c5abaa1e0549f4b400b5e0e3a1136152a149000de140546573743101028201d8185882d8799fa4446e616d6545546573743145696d6167655835697066733a2f2f516d527a6963705265757477436b4d36616f74754b6a4572464355443231334470775071364279757a4d4a617561496d656469615479706549696d6167652f6a70674b6465736372697074696f6e5348656c6c6f20776f726c64202d20434950363802ff825839003659ed2a30abb32e97589f2a01c8500ce8fc4897b868ebe42fbf4a8aa1521c2cea3cc79762d575581e47ea60b8eaa03430716cfd6140c7961a00134249021a000c830909a1581c67dd133868f14107b25772f3c5abaa1e0549f4b400b5e0e3a1136152a249000643b054657374310149000de1405465737431010b58207ae25a8a9286347cc1e0444a0de75e07432a6ed243591ef673fd837bb5235a670d82825820859d3b4fd3a4c012b43ee1bbbc99240aec1827c3b8a74b867d10a7f4759149bc00825820859d3b4fd3a4c012b43ee1bbbc99240aec1827c3b8a74b867d10a7f4759149bc050e81581ce4cfbbc317c718f78d137b6535d8940618cc3d2ac04f1f35acf78e53a206815883588101000032323232323232322232533300632323232533300a3370e9000000899b8f375c601c601000e911046d6573680014a0601000260180026018002600800429309b2b19299980319b87480000044c8c94ccc02cc03400852616375c601600260080062c60080044600a6ea80048c00cdd5000ab9a5573aaae7955cfaba157450581840100d8799f446d657368ff821a006acfc01ab2d05e00f5f6";