Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/frt 1607 #1104

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions .changeset/lucky-ladybugs-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@swapkit/helpers": patch
"@swapkit/api": patch
"@swapkit/sdk": patch
---

add support to use backend for external providers i.e covalent (evm) & blockchair (utxo)
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { describe, expect, test } from "bun:test";
import { ChainId } from "@swapkit/helpers";
import { swaptkitExternalProvidersApi } from "../endpoints";

describe("should return the correct response for balance", () => {
test("LTC", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Litecoin,
isDev: true,
});

const response = await api.getBalance("ltc1q29hm7wn047c8vgqeth8d97nn7k5xrtgrek9dzg");

expect(response).toBeArray();

const balance = response?.[0];

expect(balance?.chain).toBe("LTC");
expect(balance?.decimal).toBe(8);
expect(balance?.identifier).toBe("LTC.LTC");
expect(Number(balance?.value)).toBeNumber();
});

test("ETH", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Ethereum,
isDev: true,
});

const response = await api.getBalance("0x7FF6907C874a9442EAF9c3f86F24d50391aC555f");

expect(response).toBeArray();

response.forEach((balance) => {
expect(balance).toHaveProperty("chain", "ETH");
expect(balance).toHaveProperty("decimal");
expect(balance.decimal).toBeNumber();
expect(balance).toHaveProperty("symbol");
expect(balance.symbol).toBeString();
expect(balance).toHaveProperty("value");
expect(Number(balance.value)).toBeNumber();
});
});
});

describe("should return the correct response for raw tx", () => {
test("BTC", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Bitcoin,
isDev: true,
});

const response = await api.getRawTx(
"07ac034fa366f271d638352cdb0a96b5830e2d72d80378b85be95497bc85612d",
);

// FIXME: api returns string, request client tries to parse it to json object, causing a test failure here.
expect(response).toBe(
"0100000000010170fb9124d649025893dfdbd9ac1d65e9572091890519ad9187af8807d99e35c90100000000fdffffff0220933300000000002200207bb8f5802ae446be4e5f7dde387ca1624faee09a8643a8c16ff87df4235e25845b1d70000000000022002022660edb22aaf0caec4ff17cf2e0117029ba61369f49de8d0bedbeef1c9e21e30400483045022100f87a16352df7177cdfb1a779f71fd54c07b6fc84db97cdf9463434ba0e934e7402201db3889709063e27ff9cb62d13cae3f6b918b0aec9054337bfe66e8d8c0641be01473044022100b02d245f2381cce6b0d58e8b39ba7cae8eca1033d7857ea1078ddffee7a3f3b1021f7e4952fd3c44b44f9457a2732b8237c4786e3bd66483d2c4b7d12cc2fc2a9a016952210374289bccda370609b0df0633e5078dc2b9a693e5c20c2c1c41de8fbf074b07a221029ee74e69b22d5f8c1ceffc3c842d2393dc7313c87f1d96f25bfa0c982739473221036c6cf3d37907b4d7f140b9db4f06df85d584729d7854eb1940afa8ec17d4a6a153ae00000000",
);
});
});

describe("should return the correct response for scan utxos", () => {
test("BTC", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Bitcoin,
isDev: true,
});

const response = await api.scanUTXOs("bc1q3lp7z8x0h9uuzu9keu9esnzz59yuehufljxqk6");

expect(response).toBeArray();
response.forEach((utxo) => {
expect(utxo).toHaveProperty("address");
expect(utxo).toHaveProperty("hash");
expect(utxo).toHaveProperty("index");
expect(utxo).toHaveProperty("value");
expect(utxo).toHaveProperty("witnessUtxo");

expect(utxo.address).toBeString();
expect(utxo.address.startsWith("bc1")).toBeTrue();

expect(utxo.hash).toBeString();
expect(utxo.hash).toHaveLength(64);

expect(utxo.index).toBeNumber();
expect(utxo.value).toBeNumber();

expect(utxo.witnessUtxo).toHaveProperty("value");
expect(utxo.witnessUtxo).toHaveProperty("script");
expect(utxo.witnessUtxo.value).toBe(utxo.value);
expect(Array.isArray(utxo.witnessUtxo.script)).toBeTrue();
expect(utxo.witnessUtxo.script.length).toBeGreaterThan(0);
});
});

test("BTC with tx hex", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Bitcoin,
isDev: true,
});

const response = await api.scanUTXOs("bc1q3lp7z8x0h9uuzu9keu9esnzz59yuehufljxqk6", true);

expect(response).toBeArray();
response.forEach((utxo) => {
expect(utxo).toHaveProperty("txHex");
});
});
});

describe("should return the correct response for address details", () => {
test("BTC address details", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Bitcoin,
isDev: true,
});

const response = await api.getAddressData("bc1q3lp7z8x0h9uuzu9keu9esnzz59yuehufljxqk6");

expect(response.address).toBeObject();
const { address } = response;

expect(address.type).toBe("witness_v0_scripthash");
expect(address.script_hex).toBe("00148fc3e11ccfb979c170b6cf0b984c42a149ccdf89");
expect(address.balance).toBeNumber();
expect(address.balance_usd).toBeNumber();
expect(address.received).toBeNumber();
expect(address.spent).toBeNumber();
expect(address.output_count).toBeNumber();

// Test timestamps
expect(address.first_seen_receiving).toBeString();
expect(address.last_seen_receiving).toBeString();
expect(address.first_seen_spending).toBeString();
expect(address.last_seen_spending).toBeString();

// Test transaction list
expect(response.transactions).toBeArray();
expect(response.transactions.length).toBeGreaterThan(0);
response.transactions.forEach((txHash) => {
expect(txHash).toBeString();
expect(txHash).toHaveLength(64);
});

expect(response.utxo).toBeArray();

response.utxo.forEach((utxo) => {
expect(utxo).toHaveProperty("block_id");
expect(utxo).toHaveProperty("transaction_hash");
expect(utxo).toHaveProperty("index");
expect(utxo).toHaveProperty("value");

expect(utxo.block_id).toBeNumber();
expect(utxo.transaction_hash).toHaveLength(64);
expect(utxo.index).toBeNumber();
expect(utxo.value).toBeNumber();
expect(utxo.value).toBeGreaterThan(0);
});
});
});

describe("should return the correct response for suggested tx fee", () => {
test("BTC", async () => {
const api = swaptkitExternalProvidersApi({
apiKey: "55b9bbcd-9ef0-4a52-8b9a-a5aff43db4a3",
chainId: ChainId.Bitcoin,
isDev: true,
});

const response = await api.getSuggestedTxFee();
expect(response).toBeNumber();
});
});
76 changes: 76 additions & 0 deletions packages/swapkit/api/src/external-providers/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { type ChainId, ChainIdToChain, RequestClient } from "@swapkit/helpers";
import type {
SwapkitApiAddressDataResponse,
SwapkitApiBalanceResponse,
SwapkitApiRawTxResponse,
SwapkitApiScanUTXOResponse,
SwapkitApiSuggestedTxFeeResponse,
} from "./types";

const baseUrl = "https://api.swapkit.dev/external-providers";
const baseUrlDev = "https://dev-api.swapkit.dev/external-providers";

function getBaseUrl(isDev?: boolean) {
return isDev ? baseUrlDev : baseUrl;
}

export const swaptkitExternalProvidersApi = ({
apiKey,
chainId,
isDev,
}: {
apiKey: string;
chainId: ChainId;
isDev?: boolean;
}) => ({
getBalance: async (address: string) => {
const chain = ChainIdToChain[chainId];
const url = `${getBaseUrl(isDev)}/balance?chain=${chain}&address=${address}`;

const data = await RequestClient.get<SwapkitApiBalanceResponse>(url, {
headers: { "x-api-key": apiKey },
});

return (data || []).map(({ value, decimal, chain, symbol, identifier }) => ({
value,
decimal,
chain,
symbol,
identifier,
}));
},
getRawTx: async (hash: string) => {
const chain = ChainIdToChain[chainId];
const url = `${getBaseUrl(isDev)}/tx?chain=${chain}&hash=${hash}`;
const data = await RequestClient.get<SwapkitApiRawTxResponse>(url, {
headers: { "x-api-key": apiKey },
});
return data;
},
scanUTXOs: async (address: string, fetchTxHex = false) => {
const chain = ChainIdToChain[chainId];
const url = `${getBaseUrl(isDev)}/scanUTXO?chain=${chain}&address=${address}&fetchTxHex=${fetchTxHex}`;
const data = await RequestClient.get<SwapkitApiScanUTXOResponse>(url, {
headers: { "x-api-key": apiKey },
});
return data;
},
getAddressData: async (address: string) => {
const chain = ChainIdToChain[chainId];
const url = `${getBaseUrl(isDev)}/address?chain=${chain}&address=${address}`;
const data = await RequestClient.get<SwapkitApiAddressDataResponse>(url, {
headers: { "x-api-key": apiKey },
});
return data;
},
getSuggestedTxFee: async () => {
const chain = ChainIdToChain[chainId];
const url = `${getBaseUrl(isDev)}/suggestedFee?chain=${chain}`;
const data = await RequestClient.get<SwapkitApiSuggestedTxFeeResponse>(url, {
headers: { "x-api-key": apiKey },
});
return data;
},
});

export type SwaptkitExternalProvidersApiType = ReturnType<typeof swaptkitExternalProvidersApi>;
59 changes: 59 additions & 0 deletions packages/swapkit/api/src/external-providers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export type SwapkitApiBalanceResponse = {
chain: string;
decimal: number;
ticker: string;
symbol: string;
value: string;
identifier: string;
}[];

export type SwapkitApiRawTxResponse = string;

export type SwapkitApiScanUTXOResponse = ScanUTXO[];

export interface ScanUTXO {
address: string;
hash: string;
index: number;
value: number;
witnessUtxo: WitnessUtxo;
}

export interface WitnessUtxo {
value: number;
script: number[];
}

export interface SwapkitApiAddressDataResponse {
address: Address;
transactions: string[];
utxo: Utxo[];
}

export interface Address {
type: string;
script_hex: string;
balance: number;
balance_usd: number;
received: number;
received_usd: number;
spent: number;
spent_usd: number;
output_count: number;
unspent_output_count: number;
first_seen_receiving: string;
last_seen_receiving: string;
first_seen_spending: string;
last_seen_spending: string;
transaction_count: number;
scripthash_type: any;
}

export interface Utxo {
block_id: number;
transaction_hash: string;
index: number;
value: number;
}

export type SwapkitApiSuggestedTxFeeResponse = number;
3 changes: 3 additions & 0 deletions packages/swapkit/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { swaptkitExternalProvidersApi } from "./external-providers/endpoints";
import * as microgardEndpoints from "./microgard/endpoints";
import { mayachainMidgard, thorchainMidgard } from "./midgard/endpoints";
import * as swapkitApiEndpoints from "./swapkitApi/endpoints";
Expand All @@ -8,6 +9,7 @@ export * from "./microgard/types";
export * from "./thorswapStatic/types";
export * from "./thornode/types";
export * from "./swapkitApi/types";
export * from "./external-providers/types";

export const SwapKitApi = {
...microgardEndpoints,
Expand All @@ -16,4 +18,5 @@ export const SwapKitApi = {
...thorswapStaticEndpoints,
thorchainMidgard,
mayachainMidgard,
swaptkitExternalProvidersApi,
};
15 changes: 13 additions & 2 deletions packages/swapkit/helpers/src/modules/requestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ let clientConfig: Options = {};
export const defaultRequestHeaders =
typeof window !== "undefined"
? ({} as Record<string, string>)
: { referrer: "https://sk.thorswap.net", referer: "https://sk.thorswap.net" };
: {
referrer: "https://sk.thorswap.net",
referer: "https://sk.thorswap.net",
};

export function setRequestClientConfig({ apiKey, ...config }: Options) {
clientConfig = { ...config, apiKey };
Expand Down Expand Up @@ -45,7 +48,15 @@ async function fetchWithConfig(url: string, options: Options) {
body: bodyToSend,
headers,
});
const body = await response.json();

const contentType = response.headers.get("content-type");
let body;

if (contentType?.includes("application/json")) {
body = await response.json();
} else if (contentType?.includes("text/plain")) {
body = await response.text();
}

if (options.responseHandler) return options.responseHandler(body);

Expand Down
Loading
Loading