Skip to content

Commit

Permalink
[Chainport #1] Add trpc endpoints (#214)
Browse files Browse the repository at this point in the history
* [Chainport] Add trpc endpoints

* Remove ethers dependency
  • Loading branch information
dgca authored Jun 24, 2024
1 parent 8d64d91 commit f735968
Show file tree
Hide file tree
Showing 9 changed files with 556 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CreateTransactionResponse, RawTransactionSerde } from "@ironfish/sdk";

import {
BuildTransactionRequestParamsInputs,
buildTransactionRequestParams,
} from "./utils/buildTransactionRequestParams";
import { manager } from "../manager";

export async function handleGetChainportBridgeTransactionEstimatedFees({
fromAccount,
txDetails,
}: BuildTransactionRequestParamsInputs) {
const ironfish = await manager.getIronfish();
const rpcClient = await ironfish.rpcClient();
const estimatedFees = await rpcClient.chain.estimateFeeRates();

const results: CreateTransactionResponse[] = await Promise.all(
[
estimatedFees.content.slow,
estimatedFees.content.average,
estimatedFees.content.fast,
].map((feeRate) => {
const transactionRequestParams = buildTransactionRequestParams({
fromAccount,
txDetails,
feeRate,
});

return rpcClient.wallet
.createTransaction(transactionRequestParams)
.then((result) => result.content);
}),
);

const fees: bigint[] = results.map(
(result) =>
RawTransactionSerde.deserialize(Buffer.from(result.transaction, "hex"))
.fee,
);

return {
slow: parseInt(fees[0].toString()),
average: parseInt(fees[1].toString()),
fast: parseInt(fees[2].toString()),
};
}
20 changes: 20 additions & 0 deletions main/api/chainport/handleGetChainportTransactionStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from "axios";

import { getChainportEndpoints } from "./utils/getChainportEndpoints";
import { ChainportTransactionStatus } from "../../../shared/chainport";

export async function handleGetChainportTransactionStatus({
txHash,
baseNetworkId,
}: {
txHash: string;
baseNetworkId: number;
}) {
const endpoints = await getChainportEndpoints();
const url = `${endpoints.baseUrl}/api/port?base_tx_hash=${txHash}&base_network_id=${baseNetworkId}`;

const response = await axios(url);
const data = response.data as ChainportTransactionStatus;

return data;
}
39 changes: 39 additions & 0 deletions main/api/chainport/handleSendChainportBridgeTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { RawTransactionSerde } from "@ironfish/sdk";
import { z } from "zod";

import {
buildTransactionRequestParams,
buildTransactionRequestParamsInputs,
} from "./utils/buildTransactionRequestParams";
import { manager } from "../manager";

export const handleSendChainportBridgeTransactionInput =
buildTransactionRequestParamsInputs.extend({
fee: z.number(),
});

export async function handleSendChainportBridgeTransaction({
fromAccount,
txDetails,
fee,
}: z.infer<typeof handleSendChainportBridgeTransactionInput>) {
const ironfish = await manager.getIronfish();
const rpcClient = await ironfish.rpcClient();

const params = buildTransactionRequestParams({
fromAccount,
txDetails,
fee,
});

const createResponse = await rpcClient.wallet.createTransaction(params);
const bytes = Buffer.from(createResponse.content.transaction, "hex");
const rawTx = RawTransactionSerde.deserialize(bytes);

const postResponse = await rpcClient.wallet.postTransaction({
transaction: RawTransactionSerde.serialize(rawTx).toString("hex"),
account: fromAccount,
});

return postResponse.content;
}
185 changes: 185 additions & 0 deletions main/api/chainport/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import axios from "axios";
import { z } from "zod";

import { handleGetChainportBridgeTransactionEstimatedFees } from "./handleGetChainportBridgeTransactionEstimatedFees";
import { handleGetChainportTransactionStatus } from "./handleGetChainportTransactionStatus";
import {
handleSendChainportBridgeTransaction,
handleSendChainportBridgeTransactionInput,
} from "./handleSendChainportBridgeTransaction";
import { buildTransactionRequestParamsInputs } from "./utils/buildTransactionRequestParams";
import { decodeChainportMemo } from "./utils/decodeChainportMemo";
import { getChainportEndpoints } from "./utils/getChainportEndpoints";
import {
ChainportBridgeTransaction,
ChainportTargetNetwork,
ChainportToken,
assertMetadataApiResponse,
assertTokensApiResponse,
} from "../../../shared/chainport";
import { logger } from "../ironfish/logger";
import { t } from "../trpc";

export const chainportRouter = t.router({
getChainportTokens: t.procedure.query(async () => {
const { tokensEndpoint, metadataEndpoint } = await getChainportEndpoints();

try {
const [tokensResponse, metadataResponse] = await Promise.all([
axios.get(tokensEndpoint),
axios.get(metadataEndpoint),
]);

const tokensData = assertTokensApiResponse(tokensResponse.data);
const chainportMeta = assertMetadataApiResponse(metadataResponse.data);

const chainportTokens = tokensData.verified_tokens.map((token) => {
const targetNetworks: Array<ChainportTargetNetwork> =
token.target_networks
.map((networkId) => {
const networkDetails = chainportMeta.cp_network_ids[networkId];
if (!networkDetails) {
throw new Error(`Unknown network id: ${networkId}`);
}
return {
chainportNetworkId: networkId,
label: networkDetails.label,
networkIcon: networkDetails.network_icon,
chainId: networkDetails.chain_id,
value: networkDetails.chain_id
? networkDetails.chain_id.toString()
: "",
};
})
.filter((item) => {
return item.value !== null;
});

return {
chainportId: token.id,
ironfishId: token.web3_address,
symbol: token.symbol,
name: token.name,
decimals: token.decimals,
targetNetworks,
};
});

const tokenEntries = chainportTokens.map<[string, ChainportToken]>(
(token) => [token.ironfishId, token],
);
const chainportTokensMap: Record<string, ChainportToken> =
Object.fromEntries(tokenEntries);

const networksEntries = chainportTokens.flatMap((token) =>
token.targetNetworks.map<[string, ChainportTargetNetwork]>(
(network) => [network.value, network],
),
);
const chainportNetworksMap: Record<string, ChainportTargetNetwork> =
Object.fromEntries(networksEntries);
return {
chainportTokens,
chainportTokensMap,
chainportNetworksMap,
};
} catch (err) {
logger.error(`Failed to fetch Chainport tokens data.
${err}
`);
throw err;
}
}),
getChainportBridgeTransactionDetails: t.procedure
.input(
z.object({
amount: z.string(),
assetId: z.string(),
to: z.string(),
selectedNetwork: z.string(),
}),
)
.query(async (opts) => {
const endpoints = await getChainportEndpoints();

const { amount, assetId, to, selectedNetwork } = opts.input;

const url = `${
endpoints.baseUrl
}/ironfish/metadata?raw_amount=${amount.toString()}&asset_id=${assetId}&target_network_id=${selectedNetwork}&target_web3_address=${to}`;

const response = await fetch(url);
const data:
| ChainportBridgeTransaction
| {
error: {
code: string;
description: string;
};
} = await response.json();

if ("error" in data) {
throw new Error(data.error.description);
}

if (!response.ok) {
logger.error(`Failed to fetch chainport bridge transaction details.
${data}`);
throw new Error("A network error occured, please try again");
}

return data;
}),
getChainportBridgeTransactionEstimatedFees: t.procedure
.input(buildTransactionRequestParamsInputs)
.query(async (opts) => {
const result = await handleGetChainportBridgeTransactionEstimatedFees(
opts.input,
);
return result;
}),
sendChainportBridgeTransaction: t.procedure
.input(handleSendChainportBridgeTransactionInput)
.mutation(async (opts) => {
const result = await handleSendChainportBridgeTransaction(opts.input);
return result;
}),
getChainportTransactionStatus: t.procedure
.input(
z.object({
transactionHash: z.string(),
baseNetworkId: z.number(),
}),
)
.query(async (opts) => {
const result = handleGetChainportTransactionStatus({
txHash: opts.input.transactionHash,
baseNetworkId: opts.input.baseNetworkId,
});
return result;
}),
getChainportMeta: t.procedure.query(async () => {
const { metadataEndpoint } = await getChainportEndpoints();
const response = await axios.get(metadataEndpoint);
const data = assertMetadataApiResponse(response.data);
return data;
}),
decodeMemo: t.procedure
.input(
z.object({
memo: z.string(),
}),
)
.query(async (opts) => {
try {
const result = decodeChainportMemo(
Buffer.from(opts.input.memo).toString(),
);
return result;
} catch (_err) {
return null;
}
}),
});
55 changes: 55 additions & 0 deletions main/api/chainport/utils/buildTransactionRequestParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { CreateTransactionRequest, CurrencyUtils } from "@ironfish/sdk";
import { z } from "zod";

export const buildTransactionRequestParamsInputs = z.object({
fromAccount: z.string(),
txDetails: z.object({
bridge_output: z.object({
publicAddress: z.string(),
amount: z.string(),
memoHex: z.string(),
assetId: z.string(),
}),
gas_fee_output: z.object({
publicAddress: z.string(),
amount: z.string(),
memo: z.string(),
}),
}),
fee: z.number().optional(),
feeRate: z.string().optional(),
});

export type BuildTransactionRequestParamsInputs = z.infer<
typeof buildTransactionRequestParamsInputs
>;

export function buildTransactionRequestParams({
fromAccount,
txDetails,
fee,
feeRate,
}: BuildTransactionRequestParamsInputs) {
const params: CreateTransactionRequest = {
account: fromAccount,
outputs: [
{
publicAddress: txDetails.bridge_output.publicAddress,
amount: txDetails.bridge_output.amount,
memoHex: txDetails.bridge_output.memoHex,
assetId: txDetails.bridge_output.assetId,
},
{
publicAddress: txDetails.gas_fee_output.publicAddress,
amount: txDetails.gas_fee_output.amount,
memo: txDetails.gas_fee_output.memo,
},
],
fee: fee ? CurrencyUtils.encode(BigInt(fee)) : null,
feeRate: feeRate ?? null,
expiration: undefined,
confirmations: undefined,
};

return params;
}
33 changes: 33 additions & 0 deletions main/api/chainport/utils/decodeChainportMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function decodeNumberFrom10Bits(bits: string) {
return parseInt("0" + bits.slice(1, 10), 2);
}

function decodeCharFrom6Bits(bits: string) {
const num = parseInt(bits, 2);
if (num < 10) {
return num.toString();
}
return String.fromCharCode(num - 10 + "a".charCodeAt(0));
}

export function decodeChainportMemo(
encodedHex: string,
): [number, string, boolean] {
const hexInteger = BigInt("0x" + encodedHex);
const encodedString = hexInteger.toString(2);
const padded = encodedString.padStart(250, "0");
const networkId = decodeNumberFrom10Bits(padded);

const toIronfish = padded[0] === "1";
const addressCharacters = [];

for (let i = 10; i < padded.length; i += 6) {
const j = i + 6;
const charBits = padded.slice(i, j);
addressCharacters.push(decodeCharFrom6Bits(charBits));
}

const address = "0x" + addressCharacters.join("");

return [networkId, address.toLowerCase(), toIronfish];
}
Loading

0 comments on commit f735968

Please sign in to comment.