Skip to content

Commit

Permalink
feat: add bridge provider external url generator functions
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseRFelix committed Jun 8, 2024
1 parent d59c065 commit 8f08f21
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 22 deletions.
107 changes: 107 additions & 0 deletions packages/bridge/src/axelar/__tests__/external-urls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { NativeEVMTokenConstantAddress } from "../../ethereum";
import { getAxelarExternalUrl } from "../external-urls";

describe("getAxelarExternalUrl", () => {
it("should return the correct URL for Eth <> axlEth", async () => {
const params = {
fromChain: { chainId: 1, chainType: "evm" },
toChain: { chainId: "osmosis-1", chainType: "cosmos" },
fromAsset: {
denom: "ETH",
sourceDenom: "weth-wei",
decimals: 18,
address: NativeEVMTokenConstantAddress,
},
toAsset: {
address:
"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5",
decimals: 18,
denom: "ETH",
sourceDenom: "weth-wei",
},
env: "mainnet",
toAddress: "destination-address",
} as Parameters<typeof getAxelarExternalUrl>[0];

const url = await getAxelarExternalUrl(params);

expect(url).toBe(
"https://satellite.money/?source=Ethereum&destination=osmosis&asset_denom=eth-wei&destination_address=destination-address"
);
});

it("should throw an error if fromChain is not found", async () => {
const params = {
fromChain: { chainId: "nonexistent", chainType: "evm" },
toChain: { chainId: "chain2", chainType: "cosmos" },
fromAsset: {
address: "address1",
denom: "denom1",
sourceDenom: "sourceDenom1",
decimals: 18,
},
toAsset: {
address: "address2",
denom: "denom2",
sourceDenom: "sourceDenom2",
decimals: 18,
},
env: "mainnet",
toAddress: "destination-address",
} as Parameters<typeof getAxelarExternalUrl>[0];

await expect(getAxelarExternalUrl(params)).rejects.toThrow(
"Chain not found: nonexistent"
);
});

it("should throw an error if toChain is not found", async () => {
const params = {
fromChain: { chainId: "chain1", chainType: "evm" },
toChain: { chainId: "nonexistent", chainType: "cosmos" },
fromAsset: {
address: "address1",
denom: "denom1",
sourceDenom: "sourceDenom1",
decimals: 18,
},
toAsset: {
address: "address2",
denom: "denom2",
sourceDenom: "sourceDenom2",
decimals: 18,
},
env: "mainnet",
toAddress: "destination-address",
} as Parameters<typeof getAxelarExternalUrl>[0];

await expect(getAxelarExternalUrl(params)).rejects.toThrow(
"Chain not found: chain1"
);
});

it("should throw an error if toAsset is not found", async () => {
const params = {
fromChain: { chainId: 1, chainType: "evm" },
toChain: { chainId: "osmosis-1", chainType: "cosmos" },
fromAsset: {
address: "address1",
denom: "denom1",
sourceDenom: "sourceDenom1",
decimals: 18,
},
toAsset: {
address: "nonexistent",
denom: "denom2",
sourceDenom: "sourceDenom2",
decimals: 18,
},
env: "mainnet",
toAddress: "destination-address",
} as Parameters<typeof getAxelarExternalUrl>[0];

await expect(getAxelarExternalUrl(params)).rejects.toThrow(
"Asset not found: nonexistent"
);
});
});
60 changes: 60 additions & 0 deletions packages/bridge/src/axelar/external-urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isNil } from "@osmosis-labs/utils";

import {
BridgeProviderContext,
GetBridgeExternalUrlParams,
} from "../interface";
import { getAxelarAssets, getAxelarChains } from "./queries";

export async function getAxelarExternalUrl({
fromChain,
toChain,
toAsset,
env,
toAddress,
}: GetBridgeExternalUrlParams & {
env: BridgeProviderContext["env"];
}): Promise<string> {
const [axelarChains, axelarAssets] = await Promise.all([
getAxelarChains({ env }),
getAxelarAssets({ env }),
]);

const fromAxelarChain = axelarChains.find(
(chain) => String(chain.chain_id) === String(fromChain.chainId)
);

if (!fromAxelarChain) {
throw new Error(`Chain not found: ${fromChain.chainId}`);
}

const toAxelarChain = axelarChains.find(
(chain) => String(chain.chain_id) === String(toChain.chainId)
);

if (!toAxelarChain) {
throw new Error(`Chain not found: ${toChain.chainId}`);
}

const toAxelarAsset = axelarAssets.find((axelarAsset) => {
return (
!isNil(axelarAsset.addresses[toAxelarChain.chain_name]) &&
(axelarAsset.addresses[toAxelarChain.chain_name].ibc_denom ===
toAsset.address ||
axelarAsset.addresses[toAxelarChain.chain_name].address ===
toAsset.address)
);
});

if (!toAxelarAsset) {
throw new Error(`Asset not found: ${toAsset.address}`);
}

const url = new URL("https://satellite.money/");
url.searchParams.set("source", fromAxelarChain.chain_name);
url.searchParams.set("destination", toAxelarChain.chain_name);
url.searchParams.set("asset_denom", toAxelarAsset.id);
url.searchParams.set("destination_address", toAddress);

return url.toString();
}
107 changes: 102 additions & 5 deletions packages/bridge/src/axelar/queries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { apiClient } from "@osmosis-labs/utils";
import { CacheEntry, cachified } from "cachified";
import { LRUCache } from "lru-cache";

import { BridgeProviderContext } from "../interface";

export type TransferStep = {
id: string;
type:
Expand Down Expand Up @@ -77,14 +83,105 @@ export async function getTransferStatus(
origin = "https://api.axelarscan.io"
): Promise<TransferStatus> {
try {
const response = await fetch(
`${origin}/cross-chain/transfers-status?txHash=${sendTxHash}`
);
const data = await response.json();
const url = new URL("/cross-chain/transfers-status", origin);
url.searchParams.set("txHash", sendTxHash);

return data as TransferStatus;
return apiClient<TransferStatus>(url.toString());
} catch {
console.error("Failed to fetch transfer status for tx hash: ", sendTxHash);
return [];
}
}

interface AxelarChain {
id: string;
chain_id: number | string;
chain_name: string;
maintainer_id: string;
endpoints: {
rpc: string[];
lcd: string[];
};
native_token: {
symbol: string;
name: string;
decimals: number;
};
name: string;
short_name: string;
image: string;
color: string;
explorer: {
name: string;
url: string;
icon: string;
block_path: string;
address_path: string;
contract_path: string;
contract_0_path: string;
transaction_path: string;
asset_path: string;
};
prefix_address: string;
prefix_chain_ids: string[];
chain_type: string;
provider_params: object[];
}

const cache = new LRUCache<string, CacheEntry>({
max: 5,
});

export async function getAxelarChains({
env,
}: {
env: BridgeProviderContext["env"];
}) {
return cachified({
key: `axelar-chains`,
cache,
ttl: 1000 * 60 * 30, // 30 minutes
getFreshValue: () =>
apiClient<AxelarChain[]>(
env === "mainnet"
? "https://api.axelarscan.io/api/getChains"
: "https://testnet.api.axelarscan.io/api/getChains"
),
});
}

interface AxelarAsset {
id: string; // ID using in general purpose
denom: string;
native_chain: string; // general ID of chain that asset is native on
name: string; // display name
symbol: string;
decimals: number; // token decimals
image: string; // logo path
coingecko_id: string; // asset identifier on coingecko service
addresses: {
[chain: string]: {
address: string; // EVM token address
ibc_denom: string; // Cosmos token address (denom)
symbol: string; // symbol of asset on each chain
};
};
}

export async function getAxelarAssets({
env,
}: {
env: BridgeProviderContext["env"];
}) {
return cachified({
key: "axelar-assets",
cache,
ttl: 1000 * 60 * 30, // 30 minutes
getFreshValue: () =>
apiClient<AxelarAsset[]>(
env === "mainnet"
? "https://api.axelarscan.io/api/getAssets"
: "https://testnet.api.axelarscan.io/api/getAssets"
),
});
}
17 changes: 0 additions & 17 deletions packages/bridge/src/axelar/utils.ts

This file was deleted.

4 changes: 4 additions & 0 deletions packages/bridge/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ export const getBridgeExternalUrlSchema = z.object({
* The asset on the destination chain.
*/
toAsset: bridgeAssetSchema,
/**
* The address on the destination chain where the assets are to be received.
*/
toAddress: z.string(),
});

export type GetBridgeExternalUrlParams = z.infer<
Expand Down
57 changes: 57 additions & 0 deletions packages/bridge/src/skip/__tests__/external-urls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getSkipExternalUrl } from "../external-urls";

describe("getSkipExternalUrl", () => {
it("should generate the correct URL for given parameters", () => {
const params = {
fromChain: { chainId: "cosmoshub-4" },
toChain: { chainId: "agoric-3" },
fromAsset: { address: "uatom" },
toAsset: { address: "ubld" },
} as Parameters<typeof getSkipExternalUrl>[0];

const expectedUrl =
"https://ibc.fun/?src_chain=cosmoshub-4&src_asset=uatom&dest_chain=agoric-3&dest_asset=ubld";
const result = getSkipExternalUrl(params);

expect(result).toBe(expectedUrl);
});

it("should encode asset addresses correctly", () => {
const params = {
fromChain: { chainId: "akashnet-2" },
toChain: { chainId: "andromeda-1" },
fromAsset: {
address:
"ibc/2e5d0ac026ac1afa65a23023ba4f24bb8ddf94f118edc0bad6f625bfc557cded",
},
toAsset: {
address:
"ibc/976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97",
},
} as Parameters<typeof getSkipExternalUrl>[0];

const expectedUrl =
"https://ibc.fun/?src_chain=akashnet-2&src_asset=ibc%2F2e5d0ac026ac1afa65a23023ba4f24bb8ddf94f118edc0bad6f625bfc557cded&dest_chain=andromeda-1&dest_asset=ibc%2F976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97";
const result = getSkipExternalUrl(params);

expect(result).toBe(expectedUrl);
});

it("should handle numeric chain IDs correctly", () => {
const params = {
fromChain: { chainId: 42161 },
toChain: { chainId: "andromeda-1" },
fromAsset: { address: "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8" },
toAsset: {
address:
"ibc/976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97",
},
} as Parameters<typeof getSkipExternalUrl>[0];

const expectedUrl =
"https://ibc.fun/?src_chain=42161&src_asset=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8&dest_chain=andromeda-1&dest_asset=ibc%2F976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97";
const result = getSkipExternalUrl(params);

expect(result).toBe(expectedUrl);
});
});
File renamed without changes.
Loading

0 comments on commit 8f08f21

Please sign in to comment.