@funkit/fun-relay
Version:
429 lines (417 loc) • 14.1 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// index.ts
var index_exports = {};
__export(index_exports, {
FUN_RELAY_REFERRER: () => FUN_RELAY_REFERRER,
FUN_RELAY_REVENUE_WALLET: () => FUN_RELAY_REVENUE_WALLET,
LogLevel: () => import_relay_sdk2.LogLevel,
RELAY_TERMINAL_STATUSES: () => RELAY_TERMINAL_STATUSES,
convertFunToRelayTokenAddress: () => convertFunToRelayTokenAddress,
executeRelayQuote: () => executeRelayQuote,
getRelayAssetPriceInfo: () => getRelayAssetPriceInfo,
getRelayClient: () => getRelayClient,
getRelayExecutionInfo: () => getRelayExecutionInfo,
getRelayExecutionRefundState: () => getRelayExecutionRefundState,
getRelayExecutionState: () => getRelayExecutionState,
getRelayQuote: () => getRelayQuote,
initializeRelayClient: () => initializeRelayClient,
isRelayExecutionTerminalStatus: () => isRelayExecutionTerminalStatus,
parseRelayFees: () => parseRelayFees
});
module.exports = __toCommonJS(index_exports);
// src/client.ts
var import_relay_sdk = require("@reservoir0x/relay-sdk");
function initializeRelayClient({
chains,
logger,
...options
}) {
return (0, import_relay_sdk.createClient)({
...options,
baseApiUrl: import_relay_sdk.MAINNET_RELAY_API,
chains: chains.map(import_relay_sdk.convertViemChainToRelayChain),
logger: (message) => {
logger.info("[RelayClient]:", message);
}
});
}
function getRelayClient() {
const client = (0, import_relay_sdk.getClient)();
if (!client) {
throw new Error("Relay client is not defined");
}
return client;
}
// src/types.ts
var import_relay_sdk2 = require("@reservoir0x/relay-sdk");
// src/constants.ts
var FUNKIT_NATIVE_TOKEN = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
var RELAY_NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
var FUN_RELAY_REVENUE_WALLET = "0xb61562d83aEC43a050A06BED12Ac2bD8f9BFfd5E";
var FUN_RELAY_REFERRER = "funxyz";
var RELAY_QUOTE_CHECKOUT_TIME_BUFFER_MS = 25e3;
var RELAY_TERMINAL_STATUSES = [
"refund" /* REFUND */,
"failure" /* FAILURE */,
"success" /* SUCCESS */
];
// src/execution.ts
var import_relay_sdk3 = require("@reservoir0x/relay-sdk");
async function executeRelayQuote({
logger,
onConfirmed,
onError,
onProgress,
relayQuote,
walletClient
}) {
await getRelayClient().actions.execute({
quote: relayQuote,
wallet: walletClient,
onProgress: async ({
steps,
fees,
breakdown,
currentStep,
currentStepItem,
txHashes,
details,
error
}) => {
const logPrefix = "onRelayProgress";
logger.info(`${logPrefix}:params`, {
steps,
currentStep,
fees,
breakdown,
currentStepItem,
txHashes,
details,
error
});
onProgress?.(currentStep ?? null);
if (error) {
logger.info(`${logPrefix}:errorDetected`, error);
await onError(error).catch((e) => {
logger.error(`${logPrefix}:onErrorFailed`, e);
});
} else if (
// We can redirect as soon as all the required user steps are done
// https://fun-xyz.slack.com/archives/C08MQ85QB2N/p1748381169792379
txHashes && txHashes.length > 0 && txHashes.length === steps.length
) {
logger.info(`${logPrefix}:completed`, txHashes);
await onConfirmed(txHashes[0].txHash).catch((e) => {
logger.error(`${logPrefix}:onConfirmedFailed`, e);
});
}
}
});
}
async function getRelayExecutionInfo(requestId) {
const url = `${import_relay_sdk3.MAINNET_RELAY_API}/intents/status/v2?requestId=${requestId}`;
const response = await fetch(url);
if (!response.ok) {
throw Error(response.statusText);
}
const data = await response.json();
return data;
}
// src/fees.ts
async function getFunRelayFees({
fromTokenAddress,
fromChainId,
toTokenAddress,
toChainId,
estUsdValue,
clientId
}) {
const url = `https://frog.fun.xyz/api/fee?fromTokenAddress=${fromTokenAddress}&fromChainId=${fromChainId}&toTokenAddress=${toTokenAddress}&toChainId=${toChainId}&estUsdValue=${estUsdValue}&clientId=${clientId}`;
const response = await fetch(url);
if (!response.ok) {
throw Error(response.statusText);
}
const data = await response.json();
return data;
}
function parseRelayFees({
fees,
logger
}) {
const relayGasFeesUsd = Number.parseFloat(fees?.gas?.amountUsd || "0");
const funLpFeesUsd = Number.parseFloat(fees?.app?.amountUsd || "0");
const funLpFeesFromAmount = Number.parseFloat(
fees?.app?.amountFormatted || "0"
);
const funLpFeesFromAmountBaseUnit = BigInt(fees?.app?.amount || "0");
const relayLpFeesUsd = Number.parseFloat(fees?.relayer?.amountUsd || "0") + Number.parseFloat(fees?.relayerGas?.amountUsd || "0") + Number.parseFloat(fees?.relayerService?.amountUsd || "0");
const relayLpFeesFromAmount = Number.parseFloat(fees?.relayer?.amountFormatted || "0") + Number.parseFloat(fees?.relayerGas?.amountFormatted || "0") + Number.parseFloat(fees?.relayerService?.amountFormatted || "0");
const relayLpFeesFromAmountBaseUnit = BigInt(fees?.relayer?.amount || "0") + BigInt(fees?.relayerGas?.amount || "0") + BigInt(fees?.relayerService?.amount || "0");
const totalLpFeesUsd = funLpFeesUsd + relayLpFeesUsd;
const totalLpFeesFromAmount = funLpFeesFromAmount + relayLpFeesFromAmount;
const totalLpFeesFromAmountBaseUnit = funLpFeesFromAmountBaseUnit + relayLpFeesFromAmountBaseUnit;
logger.info("parseRelayFees", {
relayGasFeesUsd,
funLpFeesUsd,
funLpFeesFromAmount,
funLpFeesFromAmountBaseUnit,
relayLpFeesUsd,
relayLpFeesFromAmount,
relayLpFeesFromAmountBaseUnit,
totalLpFeesUsd,
totalLpFeesFromAmount,
totalLpFeesFromAmountBaseUnit
});
return {
relayGasFeesUsd,
totalLpFeesUsd,
totalFeesUsd: relayGasFeesUsd + totalLpFeesUsd,
totalFeesFromAmount: totalLpFeesFromAmount,
totalFeesFromAmountBaseUnit: totalLpFeesFromAmountBaseUnit
};
}
// src/price.ts
var import_relay_sdk4 = require("@reservoir0x/relay-sdk");
async function getRelayAssetPriceInfo({
chainId,
address
}) {
const url = `${import_relay_sdk4.MAINNET_RELAY_API}/currencies/token/price?chainId=${chainId}&address=${address}`;
const response = await fetch(url);
if (!response.ok) {
throw Error(response.statusText);
}
const data = await response.json();
return data;
}
// src/quote.ts
var import_viem = require("viem");
// src/currency.ts
var import_relay_sdk5 = require("@reservoir0x/relay-sdk");
async function getRelayAssetInfo({
chainId,
address
}) {
const url = `${import_relay_sdk5.MAINNET_RELAY_API}/currencies/v2`;
const body = {
chainIds: [chainId],
address
};
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (!response.ok) {
throw Error(response.statusText);
}
const data = await response.json();
return data?.[0];
}
// src/utils.ts
function convertFunToRelayTokenAddress(address) {
if (address.toLowerCase() === FUNKIT_NATIVE_TOKEN.toLowerCase()) {
return RELAY_NATIVE_TOKEN;
}
return address;
}
function getRelayExecutionRefundState(info) {
switch (info.status) {
case "refund":
return "REFUNDED" /* REFUNDED */;
default:
return void 0;
}
}
function getRelayExecutionState(info) {
switch (info.status) {
case "success":
return "COMPLETED" /* COMPLETED */;
case "failure":
case "refund":
return "CHECKOUT_ERROR" /* CHECKOUT_ERROR */;
default:
return "PENDING_RECEIVAL" /* PENDING_RECEIVAL */;
}
}
function isRelayExecutionTerminalStatus(info) {
return RELAY_TERMINAL_STATUSES.includes(info.status);
}
// src/quote.ts
async function getQuoteEstUsdValue({
tokenChainId,
tokenAddress,
tokenAmountBaseUnit,
logger
}) {
try {
const [tokenPriceRes, tokenInfoRes] = await Promise.all([
getRelayAssetPriceInfo({
chainId: tokenChainId,
address: convertFunToRelayTokenAddress(tokenAddress)
}),
getRelayAssetInfo({
chainId: tokenChainId,
address: convertFunToRelayTokenAddress(tokenAddress)
})
]);
const price = tokenPriceRes.price;
const decimals = tokenInfoRes.decimals;
const absoluteAmount = BigInt(tokenAmountBaseUnit) / BigInt(10 ** decimals);
return Number(price) * Number(absoluteAmount);
} catch (err) {
logger.error("getQuoteEstUsdValueError", {
message: `Failed to get USD value for token ${tokenAddress} on chain ${tokenChainId}: ${err instanceof Error ? err.message : JSON.stringify(err)}. Falling back to 0 USD.`
});
return 0;
}
}
async function getRelayQuote({
logger,
...params
}) {
const {
actionParams,
fromChainId,
fromTokenAddress,
options,
recipientAddress,
toChainId,
toTokenAddress,
userAddress,
clientId
} = params;
try {
const relayClient = getRelayClient();
const txs = actionParams?.map((action) => {
const data = (0, import_viem.encodeFunctionData)({
abi: action.contractAbi,
args: action.functionArgs,
functionName: action.functionName
});
return {
data,
to: action.contractAddress,
value: String(action.value ?? 0n)
};
});
const isExactIn = params.tradeType === "EXACT_INPUT";
const queryTokenAmountBaseUnit = isExactIn ? params.fromTokenAmountBaseUnit : params.toTokenAmountBaseUnit;
const queryTokenAddress = isExactIn ? fromTokenAddress : toTokenAddress;
const queryTokenChainId = isExactIn ? fromChainId : toChainId;
const estUsdValue = await getQuoteEstUsdValue({
tokenChainId: queryTokenChainId,
tokenAddress: queryTokenAddress,
tokenAmountBaseUnit: queryTokenAmountBaseUnit.toString(),
logger
});
const feeInfo = await getFunRelayFees({
fromTokenAddress,
fromChainId,
toTokenAddress,
toChainId,
estUsdValue: Number(estUsdValue) || 0,
clientId
});
const appFeeBp = feeInfo.b + feeInfo.v;
logger.info("getRelayQuoteParams", {
...params,
queryTokenAmountBaseUnit,
txs,
estUsdValue,
feeInfo,
appFeeBp
});
const relayQuote = await relayClient.actions.getQuote({
chainId: Number(fromChainId),
toChainId: Number(toChainId),
currency: convertFunToRelayTokenAddress(fromTokenAddress),
toCurrency: convertFunToRelayTokenAddress(toTokenAddress),
amount: queryTokenAmountBaseUnit.toString(),
recipient: recipientAddress,
tradeType: params.tradeType,
user: userAddress,
...txs && { txs },
options: {
referrer: clientId ? `${FUN_RELAY_REFERRER}|${clientId}` : FUN_RELAY_REFERRER,
appFees: [
{
fee: appFeeBp.toString(),
recipient: FUN_RELAY_REVENUE_WALLET
}
],
...options
}
});
logger.info("relayQuote", relayQuote);
const estSubtotalUsd = Number(
relayQuote.details?.currencyOut?.amountUsd || 0
);
const estTotalUsd = Number(relayQuote.details?.currencyIn?.amountUsd || 0);
const fromAmountString = relayQuote.details?.currencyIn?.amountFormatted || "0";
const fromAmountBaseUnitString = relayQuote.details?.currencyIn?.amount || "0";
const relayRequestId = relayQuote.steps[0]?.requestId;
if (!relayRequestId) {
throw new Error("Relay quote does not contain a requestId");
}
if (!relayQuote.details?.currencyIn || !relayQuote.details?.currencyOut) {
throw new Error("Relay quote does not contain trade details");
}
const {
relayGasFeesUsd,
totalLpFeesUsd,
totalFeesUsd,
totalFeesFromAmount,
totalFeesFromAmountBaseUnit
} = parseRelayFees({ fees: relayQuote.fees, logger });
const estSubtotalFromAmount = (Number.parseFloat(fromAmountString) - totalFeesFromAmount).toString();
const estSubtotalFromAmountBaseUnit = (BigInt(fromAmountBaseUnitString) - totalFeesFromAmountBaseUnit).toString();
return {
quoteId: relayRequestId,
estCheckoutTimeMs: (relayQuote.details.timeEstimate || 0) * 1e3 + RELAY_QUOTE_CHECKOUT_TIME_BUFFER_MS,
estFeesFromAmount: totalFeesFromAmount.toString(),
estFeesFromAmountBaseUnit: totalFeesFromAmountBaseUnit.toString(),
estFeesUsd: totalFeesUsd,
estMarketMakerGasUsd: relayGasFeesUsd,
estSubtotalFromAmount,
estSubtotalFromAmountBaseUnit,
estSubtotalUsd,
estTotalFromAmount: fromAmountString,
estTotalFromAmountBaseUnit: fromAmountBaseUnitString,
estTotalUsd,
finalToAmountBaseUnit: relayQuote.details.currencyOut.amount ?? "0",
// TODO: Or do we want 'minimumAmount'?
fromTokenAddress,
lpFeePercentage: 0,
// TODO: ?
lpFeeUsd: totalLpFeesUsd,
metadata: {
fromAmountBaseUnit: isExactIn ? queryTokenAmountBaseUnit.toString() : relayQuote.details.currencyIn.amount ?? "0",
relayQuote,
toAmountBaseUnit: isExactIn ? relayQuote.details.currencyOut.amount ?? "0" : queryTokenAmountBaseUnit.toString()
}
};
} catch (err) {
throw new Error(
`An error occurred trying to generate a relay quote: ${err instanceof Error ? err.message : JSON.stringify(err)}`
);
}
}
//# sourceMappingURL=index.js.map