diff --git a/src/helper/horizon-rpc.ts b/src/helper/horizon-rpc.ts index 221b4be..91b7a4f 100644 --- a/src/helper/horizon-rpc.ts +++ b/src/helper/horizon-rpc.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import { AssetType, Horizon } from "stellar-sdk"; +import { AssetType, Horizon, TransactionBuilder } from "stellar-sdk"; export const BASE_RESERVE = 0.5; export const BASE_RESERVE_MIN_COUNT = 2; @@ -229,3 +229,34 @@ export const fetchAccountHistory = async ( throw new Error(JSON.stringify(error)); } }; + +export const submitTransaction = async ( + signedXDR: string, + networkUrl: string, + networkPassphrase: string +): Promise<{ + data: Horizon.HorizonApi.SubmitTransactionResponse | null; + error: unknown; +}> => { + const tx = TransactionBuilder.fromXDR(signedXDR, networkPassphrase); + const server = new Horizon.Server(networkUrl); + + try { + const data = await server.submitTransaction(tx); + return { + data, + error: null, + }; + } catch (e: any) { + if (e.response.status === 504) { + // in case of 504, keep retrying this tx until submission succeeds or we get a different error + // https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/timeout + // https://developers.stellar.org/docs/encyclopedia/error-handling + return await submitTransaction(signedXDR, networkUrl, networkPassphrase); + } + return { + data: null, + error: e, + }; + } +}; diff --git a/src/route/index.ts b/src/route/index.ts index 3e8c554..d8e1e2a 100644 --- a/src/route/index.ts +++ b/src/route/index.ts @@ -12,6 +12,16 @@ import { isNetwork, NetworkNames, } from "../helper/validate"; +import { submitTransaction } from "../helper/horizon-rpc"; +import { + Memo, + MemoType, + Operation, + SorobanRpc, + Transaction, + TransactionBuilder, +} from "stellar-sdk"; +import { simulateTx } from "../helper/soroban-rpc"; const API_VERSION = "v1"; @@ -150,6 +160,58 @@ export function initApiServer( }, }); + instance.route({ + method: "GET", + url: "/token-details/:contractId", + schema: { + params: { + ["contractId"]: { + type: "string", + validator: (qStr: string) => isContractId(qStr), + }, + }, + querystring: { + ["pub_key"]: { + type: "string", + validator: (qStr: string) => isPubKey(qStr), + }, + ["network"]: { + type: "string", + validator: (qStr: string) => isNetwork(qStr), + }, + ["soroban_url"]: { + type: "string", + }, + }, + }, + handler: async ( + request: FastifyRequest<{ + Params: { ["contractId"]: string }; + Querystring: { + ["contract_ids"]: string; + ["pub_key"]: string; + ["network"]: NetworkNames; + ["soroban_url"]?: string; + }; + }>, + reply + ) => { + const contractId = request.params["contractId"]; + const { network, pub_key, soroban_url } = request.query; + try { + const data = await mercuryClient.tokenDetails( + pub_key, + contractId, + network, + soroban_url + ); + reply.code(200).send(data); + } catch (error) { + reply.code(400).send(error); + } + }, + }); + instance.route({ method: "POST", url: "/subscription/token", @@ -279,6 +341,86 @@ export function initApiServer( }, }); + instance.route({ + method: "POST", + url: "/submit-tx", + schema: { + body: { + type: "object", + properties: { + signed_xdr: { type: "string" }, + network_url: { type: "string" }, + network_passphrase: { type: "string" }, + }, + }, + }, + handler: async ( + request: FastifyRequest<{ + Body: { + signed_xdr: string; + network_url: string; + network_passphrase: string; + }; + }>, + reply + ) => { + const { signed_xdr, network_url, network_passphrase } = request.body; + const { data, error } = await submitTransaction( + signed_xdr, + network_url, + network_passphrase + ); + if (error) { + reply.code(400).send(error); + } else { + reply.code(200).send(data); + } + }, + }); + + instance.route({ + method: "POST", + url: "/simulate-tx", + schema: { + body: { + type: "object", + properties: { + signed_xdr: { type: "string" }, + network_url: { type: "string" }, + network_passphrase: { type: "string" }, + }, + }, + }, + handler: async ( + request: FastifyRequest<{ + Body: { + signed_xdr: string; + network_url: string; + network_passphrase: string; + }; + }>, + reply + ) => { + const { signed_xdr, network_url, network_passphrase } = request.body; + + try { + const tx = TransactionBuilder.fromXDR( + signed_xdr, + network_passphrase + ); + const server = new SorobanRpc.Server(network_url); + + const data = await simulateTx( + tx as Transaction, Operation[]>, + server + ); + reply.code(200).send(data); + } catch (error) { + reply.code(400).send(error); + } + }, + }); + next(); }, { prefix: `/api/${API_VERSION}` } diff --git a/src/service/mercury/index.ts b/src/service/mercury/index.ts index 32945a2..6a9a833 100644 --- a/src/service/mercury/index.ts +++ b/src/service/mercury/index.ts @@ -332,7 +332,8 @@ export class MercuryClient { tokenDetails = async ( pubKey: string, contractId: string, - network: NetworkNames + network: NetworkNames, + customRpcUrl?: string ): Promise< { name: string; symbol: string; decimals: number } | undefined > => { @@ -345,7 +346,7 @@ export class MercuryClient { return JSON.parse(tokenDetails); } } - const server = await getServer(network); + const server = await getServer(network, customRpcUrl); // we need a builder per operation, 1 op per tx in Soroban const decimalsBuilder = await getTxBuilder(pubKey, network, server); const decimals = await getTokenDecimals(