@lifi/composer-sdk
Version:
Public Composer SDK for building and submitting flows
231 lines • 7.98 kB
JavaScript
import {
checkCompatibility,
COMPOSE_VERSION_HEADER,
COMPOSER_SDK_VERSION_HEADER,
parseVersion
} from "@lifi/compose-spec";
import { ComposeError, errorFromHttpResponse } from "./errors.js";
import {
parseCompilePartialEnvelope,
parseSimulateResult
} from "./responseSchemas.js";
const SDK_VERSION = true ? "0.6.0" : "dev";
const SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);
const bigintReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
const isNonNullObject = (v) => typeof v === "object" && v !== null;
const INTEGER_STRING = /^\d+$/;
const decodeAmount = (value, path, url) => {
if (typeof value !== "string" || !INTEGER_STRING.test(value)) {
throw new ComposeError(
"UNKNOWN_ERROR",
`Malformed compose response: ${path} is ${value === void 0 ? "absent" : "not an integer string"}`,
{ url }
);
}
return BigInt(value);
};
const malformed = (path, what, url) => new ComposeError(
"UNKNOWN_ERROR",
`Malformed compose response: ${path} ${what}`,
{ url }
);
const decodeAmountLeaf = (amount, field, path, url) => {
const leaf = amount[field];
if (leaf === void 0) return;
if (!isNonNullObject(leaf)) {
throw malformed(`${path}.${field}`, "is not an object", url);
}
leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);
};
const decodeOutputs = (data, url) => {
const outputs = data.outputs;
if (!isNonNullObject(outputs)) {
throw malformed(
"outputs",
outputs === void 0 ? "is absent" : "is not an object",
url
);
}
for (const key of Object.keys(outputs)) {
const entry = outputs[key];
if (!isNonNullObject(entry)) {
throw malformed(`outputs.${key}`, "is not an object", url);
}
const amount = entry.amount;
if (!isNonNullObject(amount)) {
throw malformed(
`outputs.${key}.amount`,
amount === void 0 ? "is absent" : "is not an object",
url
);
}
decodeAmountLeaf(amount, "estimate", `outputs.${key}.amount`, url);
decodeAmountLeaf(amount, "minimum", `outputs.${key}.amount`, url);
}
};
const decodeWireAmounts = (data, url) => {
if (!isNonNullObject(data)) return;
decodeOutputs(data, url);
};
const parseBody = async (res, url) => {
const body = await res.json().catch((_) => null);
if (!isNonNullObject(body) || !("data" in body)) {
throw new ComposeError("UNKNOWN_ERROR", "Unexpected response format", {
url
});
}
return body.data;
};
const parseCompileSuccessBody = async (res, url) => {
const data = await parseBody(res, url);
decodeWireAmounts(data, url);
return { ...data, status: "success" };
};
const parsePartialBody = async (res, url) => {
const body = await res.json().catch((_) => null);
const envelope = parseCompilePartialEnvelope(body);
if (envelope === null) {
throw new ComposeError(
"UNKNOWN_ERROR",
"Unexpected partial response format",
{ url }
);
}
const data = envelope.data;
decodeWireAmounts(data, url);
return { ...data, status: "partial", error: envelope.error };
};
const parseSimulateBody = async (res, url) => {
const body = await res.json().catch((_) => null);
const result = parseSimulateResult(body);
if (result === null) {
throw new ComposeError(
"UNKNOWN_ERROR",
"Unexpected simulate response format",
{ url }
);
}
return result;
};
const assertServerVersion = (res, url) => {
const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);
if (serverVersion === null || SDK_VERSION_TRIPLE === void 0) return;
const serverTriple = parseVersion(serverVersion);
if (serverTriple === void 0) return;
const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);
if (compatibility === "compatible") return;
const versions = { sdkVersion: SDK_VERSION, serverVersion };
if (compatibility === "sdk_outdated") {
throw new ComposeError(
"VALIDATION_ERROR",
`composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,
{
status: res.status,
url,
kind: "sdk_outdated",
sdkOutdated: {
...versions,
minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`
}
}
);
}
throw new ComposeError(
"VALIDATION_ERROR",
`composer-sdk ${SDK_VERSION} targets a newer compose contract than the server at ${url} serves (${serverVersion}); pin @lifi/composer-sdk and @lifi/compose-spec to ${serverVersion} or wait for the server rollout`,
{
status: res.status,
url,
kind: "server_outdated",
serverOutdated: versions
}
);
};
const requestInit = (baseHeaders, init) => init.method === "GET" ? { method: "GET", headers: { ...baseHeaders } } : {
method: "POST",
headers: { ...baseHeaders, "Content-Type": "application/json" },
body: JSON.stringify(init.body, bigintReplacer)
};
const send = async ({ fetchFn, baseHeaders }, url, init) => {
try {
return await fetchFn(url, requestInit(baseHeaders, init));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new ComposeError("NETWORK_ERROR", message, { cause: err });
}
};
const postCompile = async (transport, url, request) => {
const res = await send(transport, url, { method: "POST", body: request });
assertServerVersion(res, url);
if (res.status === 206) {
return await parsePartialBody(res, url);
}
if (!res.ok) {
const body = await res.text();
throw errorFromHttpResponse(res.status, body, url);
}
return await parseCompileSuccessBody(res, url);
};
const getJson = async (transport, url) => {
const res = await send(transport, url, { method: "GET" });
if (!res.ok) {
const body = await res.text();
throw errorFromHttpResponse(res.status, body, url);
}
return await parseBody(res, url);
};
const createComposeClient = (options) => {
if (!options.baseUrl || !/^https?:\/\//i.test(options.baseUrl)) {
throw new ComposeError(
"VALIDATION_ERROR",
`Invalid baseUrl: expected an HTTP(S) URL, got "${options.baseUrl}"`
);
}
const trimmedApiKey = options.apiKey?.trim() || void 0;
if (!trimmedApiKey) {
throw new ComposeError(
"VALIDATION_ERROR",
"apiKey is required: pass a LI.FI API key to createComposeSdk()."
);
}
const fetchFn = options.fetch ?? globalThis.fetch;
const base = options.baseUrl.replace(/\/$/, "");
const baseHeaders = {
Accept: "application/json",
[COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,
"x-lifi-api-key": trimmedApiKey
};
const transport = { fetchFn, baseHeaders };
const getManifest = async () => getJson(transport, `${base}/compose/manifest`);
const getChains = async () => getJson(transport, `${base}/chains`);
const compile = async (request) => postCompile(transport, `${base}/compose`, request);
const route = async (request) => postCompile(transport, `${base}/compose/route`, request);
const getZapPacks = async (options2) => {
const params = new URLSearchParams();
if (options2?.protocols !== void 0) {
const raw = options2.protocols;
const list = typeof raw === "string" ? raw : raw.join(",");
params.set("protocols", list);
}
const qs = params.toString();
return getJson(
transport,
`${base}/compose/zap-packs${qs ? `?${qs}` : ""}`
);
};
const simulate = async (request) => {
const url = `${base}/simulate`;
const res = await send(transport, url, { method: "POST", body: request });
if (res.status === 200 || res.status === 422) {
return await parseSimulateBody(res, url);
}
const body = await res.text();
throw errorFromHttpResponse(res.status, body, url);
};
return { getManifest, getChains, compile, route, getZapPacks, simulate };
};
export {
bigintReplacer,
createComposeClient
};
//# sourceMappingURL=client.js.map