liquidops
Version:
LiquidOps is an over-collateralised lending and borrowing protocol built on Arweave's L2 AO.
1,439 lines (1,410 loc) • 45.3 kB
JavaScript
// src/ao/utils/tokenAddressData.ts
var controllerAddress = "SmmMv0rJwfIDVM3RvY2-P729JFYwhdGSeGo2deynbfY";
var redstoneOracleAddress = "R5rRjBFS90qIGaohtzd1IoyPwZD0qJZ25QXkP7_p5a0";
var APRAgentAddress = "D3AlSUAtbWKcozsrvckRuCY6TVkAY1rWtLYGoGf6KIA";
var tokenData = {
QAR: {
name: "Quantum Arweave",
icon: "8VLMb0c9NATl4iczfwpMDe1Eh8kFWIUpSlIkcGfDFzM",
ticker: "QAR",
address: "NG-0lVX882MG5nhARrSzyprEK6ejonHpdUmaaMPsHE8",
oTicker: "oQAR",
oAddress: "fODpFVOb5weX9Yc-26AA82m2MhmT7N9L0TkynOsruK0",
controllerAddress,
cleanTicker: "qAR",
denomination: BigInt(12),
collateralEnabled: true,
baseDenomination: BigInt(12)
},
WUSDC: {
name: "Wrapped USD Circle",
icon: "iNYk0bDqUiH0eLT2rbYjYAI5i126R4ye8iAZb55IaIM",
ticker: "WUSDC",
address: "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ",
oTicker: "oWUSDC",
oAddress: "4MW7uLFtttSLWM-yWEqV9TGD6fSIDrqa4lbTgYL2qHg",
controllerAddress,
cleanTicker: "wUSDC",
denomination: BigInt(12),
collateralEnabled: true,
baseDenomination: BigInt(6)
}
};
var tokens = Object.fromEntries(
Object.entries(tokenData).map(([ticker, data]) => [ticker, data.address])
);
var oTokens = Object.fromEntries(
Object.entries(tokenData).map(([_, data]) => [data.oTicker, data.oAddress])
);
var collateralEnabledTickers = Object.keys(tokenData).filter(
(ticker) => tokenData[ticker].collateralEnabled
);
var collateralEnabledOTickers = collateralEnabledTickers.map(
(ticker) => tokenData[ticker].oTicker
);
// src/ao/utils/tokenInput.ts
function tokenInput(token) {
const tokenEntry = Object.entries(tokens).find(
([ticker, address]) => ticker === token || address === token
);
if (tokenEntry) {
const [ticker, tokenAddress] = tokenEntry;
return {
tokenAddress,
oTokenAddress: oTokens[`o${ticker}`],
controllerAddress: tokenData[ticker].controllerAddress,
ticker
};
}
if (Object.values(oTokens).some((address) => address === token) || Object.keys(oTokens).includes(token)) {
throw new Error("Token input cannot be an oToken ticker or address.");
}
throw new Error("Token input is not supported.");
}
// src/arweave/getTags.ts
import { arGql } from "ar-gql";
async function getTags({
aoUtils,
tags,
owner,
cursor
}) {
var _a, _b;
try {
const gqlEndpoint = aoUtils.configs.GRAPHQL_URL || "https://arweave-search.goldsky.com/graphql";
const gql = arGql({ endpointUrl: gqlEndpoint });
const recipientTag = tags.find((tag) => tag.name === "recipient");
const recipientValue = recipientTag ? Array.isArray(recipientTag.values) ? recipientTag.values[0] : recipientTag.values : void 0;
const filteredTags = tags.filter((tag) => tag.name !== "recipient");
const formattedTags = filteredTags.map((tag) => ({
name: tag.name,
values: Array.isArray(tag.values) ? tag.values : [tag.values]
}));
const query = `
query GetTransactions(
$tags: [TagFilter!],
$cursor: String
${owner ? ", $owner: String!" : ""}
${recipientValue ? ", $recipients: [String!]" : ""}
) {
transactions(
tags: $tags
${owner ? "owners: [$owner]" : ""}
${recipientValue ? "recipients: $recipients" : ""}
first: 100
after: $cursor
sort: HEIGHT_DESC
) {
edges {
cursor
node {
id
tags {
name
value
}
block {
timestamp
}
}
}
pageInfo {
hasNextPage
}
}
}
`;
const variables = {
tags: formattedTags,
cursor: cursor || "",
...owner && { owner },
...recipientValue && { recipients: [recipientValue] }
};
const response = await gql.run(query, variables);
if (((_b = (_a = response.errors) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) === "internal server error") {
throw new Error("GraphQL endpoint internal server error.");
}
return response.data.transactions;
} catch (error) {
throw new Error(`Failed to retrieve Arweave GraphQL data: ${error}`);
}
}
// src/ao/messaging/validationUtils.ts
async function retryOperation(operation, maxRetries = 10, retryInterval = 600) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) return "pending";
await new Promise((resolve) => setTimeout(resolve, retryInterval));
}
}
return "pending";
}
async function validateTransaction(aoUtils, transferID, targetProcessID, config, maxRetries = 10, retryInterval = 600) {
const result = await retryOperation(
async () => {
const { Messages } = await aoUtils.result({
message: transferID,
process: targetProcessID
});
if (!Array.isArray(Messages) || Messages.length !== config.expectedTxCount) {
throw new Error("Invalid transaction response");
}
const hasRequiredActions = config.requiredNotices.every(
(notice) => Messages.some(
(msg) => {
var _a;
return (_a = msg.Tags) == null ? void 0 : _a.some(
(tag) => tag.name === "Action" && tag.value === notice
);
}
)
);
if (hasRequiredActions) return true;
return false;
},
maxRetries,
retryInterval
);
return result === "pending" ? "pending" : !!result;
}
async function findTransactionIds(aoUtils, transferID, processId, maxRetries = 10, retryInterval = 6e3) {
const result = await retryOperation(
async () => {
const transactionsFound = await getTags({
aoUtils,
tags: [
{ name: "Pushed-For", values: transferID },
{ name: "From-Process", values: processId }
],
cursor: ""
});
if (!(transactionsFound == null ? void 0 : transactionsFound.edges) || transactionsFound.edges.length !== 2) {
throw new Error("No transactions found or invalid count");
}
const debitTx = transactionsFound.edges.find(
(edge) => edge.node.tags.some(
(tag) => tag.name === "Action" && tag.value === "Debit-Notice"
)
);
const creditTx = transactionsFound.edges.find(
(edge) => edge.node.tags.some(
(tag) => tag.name === "Action" && tag.value === "Credit-Notice"
)
);
if (!debitTx || !creditTx) {
throw new Error(
"Missing required Debit-Notice or Credit-Notice transactions"
);
}
return {
debitID: debitTx.node.id,
creditID: creditTx.node.id
};
},
maxRetries,
retryInterval
);
if (result === "pending") {
throw new Error("Operation timed out");
}
return result;
}
// src/functions/borrow/borrow.ts
var BORROW_CONFIG = {
action: "Borrow",
expectedTxCount: 2,
confirmationTag: "Borrow-Confirmation",
requiredNotices: ["Transfer", "Borrow-Confirmation"],
requiresCreditDebit: true
};
async function borrow(aoUtils, { token, quantity }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { oTokenAddress, tokenAddress } = tokenInput(token);
const transferID = await aoUtils.message({
process: oTokenAddress,
tags: [
{ name: "Action", value: "Borrow" },
{ name: "Quantity", value: quantity.toString() },
{ name: "Protocol-Name", value: "LiquidOps" },
{ name: "Analytics-Tag", value: "Borrow" },
{ name: "timestamp", value: JSON.stringify(Date.now()) },
{ name: "token", value: tokenAddress }
],
signer: aoUtils.signer
});
const transferResult = await validateTransaction(
aoUtils,
transferID,
oTokenAddress,
BORROW_CONFIG
);
if (transferResult === "pending") {
return {
status: "pending",
transferID,
response: "Transaction pending."
};
}
if (!transferResult) {
throw new Error("Transaction validation failed");
}
const transferTxnId = await findExtraBorrowTransfer(
aoUtils,
transferID,
oTokenAddress
);
if (transferTxnId === "pending") {
return {
status: "pending",
transferID,
response: "Transfer transaction pending."
};
}
const transactionIds = await findTransactionIds(
aoUtils,
transferID,
tokenAddress
);
return {
status: true,
...transactionIds,
transferID
};
} catch (error) {
throw new Error("Error in borrow function: " + error);
}
}
async function findExtraBorrowTransfer(aoUtils, transferID, oTokenAddress, maxRetries = 10, retryInterval = 6e3) {
var _a, _b, _c;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const transferTxns = await getTags({
aoUtils,
tags: [
{ name: "Pushed-For", values: transferID },
{ name: "Action", values: "Transfer" },
{ name: "From-Process", values: oTokenAddress }
],
cursor: ""
});
if (!((_c = (_b = (_a = transferTxns == null ? void 0 : transferTxns.edges) == null ? void 0 : _a[0]) == null ? void 0 : _b.node) == null ? void 0 : _c.id)) {
throw new Error("Could not find transfer transaction");
}
return transferTxns.edges[0].node.id;
} catch (error) {
if (attempt === maxRetries) return "pending";
await new Promise((resolve) => setTimeout(resolve, retryInterval));
}
}
return "pending";
}
// src/functions/borrow/repay.ts
var REPAY_CONFIG = {
action: "Repay",
expectedTxCount: 2,
confirmationTag: "Repay-Confirmation",
requiredNotices: ["Debit-Notice", "Credit-Notice"],
requiresCreditDebit: true
};
async function repay(aoUtils, { token, quantity, onBehalfOf }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { tokenAddress, oTokenAddress } = tokenInput(token);
const transferID = await aoUtils.message({
process: tokenAddress,
tags: [
{ name: "Action", value: "Transfer" },
{ name: "Quantity", value: quantity.toString() },
{ name: "Recipient", value: oTokenAddress },
{ name: "X-Action", value: "Repay" },
{ name: "Protocol-Name", value: "LiquidOps" },
...onBehalfOf ? [{ name: "X-On-Behalf", value: onBehalfOf }] : [],
{ name: "Analytics-Tag", value: "Repay" },
{ name: "timestamp", value: JSON.stringify(Date.now()) },
{ name: "token", value: tokenAddress }
],
signer: aoUtils.signer
});
const transferResult = await validateTransaction(
aoUtils,
transferID,
tokenAddress,
REPAY_CONFIG
);
if (transferResult === "pending") {
return {
status: "pending",
transferID,
response: "Transaction pending."
};
}
if (!transferResult) {
throw new Error("Transaction validation failed");
}
const transactionIds = await findTransactionIds(
aoUtils,
transferID,
tokenAddress
);
return {
status: true,
...transactionIds,
transferID
};
} catch (error) {
throw new Error("Error in repay function: " + error);
}
}
// src/functions/getTransactions/getTransactions.ts
async function getTransactions(aoUtils, { token, action, walletAddress, cursor = "" }) {
var _a;
try {
if (!token || !action || !walletAddress) {
throw new Error("Please specify a token, action and walletAddress.");
}
const tags = [{ name: "Protocol-Name", values: ["LiquidOps"] }];
const { oTokenAddress, tokenAddress } = tokenInput(token);
if (action === "borrow") {
tags.push({ name: "recipient", values: [oTokenAddress] });
tags.push({ name: "Action", values: ["Borrow"] });
tags.push({ name: "Analytics-Tag", values: ["Borrow"] });
} else if (action === "repay") {
tags.push({ name: "recipient", values: [tokenAddress] });
tags.push({ name: "Action", values: ["Transfer"] });
tags.push({ name: "Recipient", values: [oTokenAddress] });
tags.push({ name: "X-Action", values: ["Repay"] });
tags.push({ name: "Analytics-Tag", values: ["Repay"] });
} else if (action === "lend") {
tags.push({ name: "recipient", values: [tokenAddress] });
tags.push({ name: "Action", values: ["Transfer"] });
tags.push({ name: "Recipient", values: [oTokenAddress] });
tags.push({ name: "X-Action", values: ["Mint"] });
tags.push({ name: "Analytics-Tag", values: ["Lend"] });
} else if (action === "unLend") {
tags.push({ name: "recipient", values: [oTokenAddress] });
tags.push({ name: "Action", values: ["Redeem"] });
tags.push({ name: "Analytics-Tag", values: ["UnLend"] });
} else {
throw new Error("Please specify an action.");
}
const queryArweave = await getTags({
aoUtils,
tags,
owner: walletAddress,
cursor
});
return {
transactions: processTransactions(queryArweave.edges),
pageInfo: {
hasNextPage: queryArweave.pageInfo.hasNextPage,
cursor: (_a = queryArweave.edges[queryArweave.edges.length - 1]) == null ? void 0 : _a.cursor
}
};
} catch (error) {
throw new Error("Error in getTransactions function:" + error);
}
}
function processTransactions(transactions) {
return transactions.map(({ node }) => {
var _a, _b;
const processedTransaction = {
id: node.id,
tags: {},
block: {
timestamp: (_b = (_a = node == null ? void 0 : node.block) == null ? void 0 : _a.timestamp) != null ? _b : 0
}
};
node.tags.forEach((tag) => {
processedTransaction.tags[tag.name] = tag.value;
});
return processedTransaction;
});
}
// src/functions/lend/lend.ts
var LEND_CONFIG = {
action: "Mint",
expectedTxCount: 2,
confirmationTag: "Mint-Confirmation",
requiredNotices: ["Debit-Notice", "Credit-Notice"],
requiresCreditDebit: true
};
async function lend(aoUtils, { token, quantity }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { tokenAddress, oTokenAddress } = tokenInput(token);
const transferID = await aoUtils.message({
process: tokenAddress,
tags: [
{ name: "Action", value: "Transfer" },
{ name: "Quantity", value: quantity.toString() },
{ name: "Recipient", value: oTokenAddress },
{ name: "X-Action", value: "Mint" },
{ name: "Protocol-Name", value: "LiquidOps" },
{ name: "Analytics-Tag", value: "Lend" },
{ name: "timestamp", value: JSON.stringify(Date.now()) },
{ name: "token", value: tokenAddress }
],
signer: aoUtils.signer
});
const transferResult = await validateTransaction(
aoUtils,
transferID,
tokenAddress,
LEND_CONFIG
);
if (transferResult === "pending") {
return {
status: "pending",
transferID,
response: "Transaction pending."
};
}
if (!transferResult) {
throw new Error("Transaction validation failed");
}
const transactionIds = await findTransactionIds(
aoUtils,
transferID,
tokenAddress
);
return {
status: true,
...transactionIds,
transferID
};
} catch (error) {
throw new Error("Error in lend function: " + error);
}
}
// src/functions/lend/unLend.ts
var UNLEND_CONFIG = {
action: "Redeem",
expectedTxCount: 1,
confirmationTag: "Redeem-Confirmation",
requiredNotices: ["Redeem-Confirmation"],
requiresCreditDebit: false
};
async function unLend(aoUtils, { token, quantity }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { oTokenAddress, tokenAddress } = tokenInput(token);
const transferID = await aoUtils.message({
process: oTokenAddress,
tags: [
{ name: "Action", value: "Redeem" },
{ name: "Quantity", value: quantity.toString() },
{ name: "Protocol-Name", value: "LiquidOps" },
{ name: "Analytics-Tag", value: "UnLend" },
{ name: "Analytics-Tag", value: "UnLend" },
{ name: "timestamp", value: JSON.stringify(Date.now()) },
{ name: "token", value: tokenAddress }
],
signer: aoUtils.signer
});
const transferResult = await validateTransaction(
aoUtils,
transferID,
oTokenAddress,
UNLEND_CONFIG
);
if (transferResult === "pending") {
return {
status: "pending",
transferID,
response: "Transaction pending."
};
}
return {
status: true,
transferID
};
} catch (error) {
throw new Error("Error in unLend function: " + error);
}
}
// src/functions/liquidations/getDiscountedQuantity.ts
function getTokenValue(from, to, prices) {
const fromData = {
price: prices[from.token === "QAR" ? "AR" : from.token].v,
scale: BigInt(10) ** tokenData[from.token].denomination
};
const fromScaledPrice = BigInt(
Math.round(fromData.price * Number(fromData.scale))
);
const toData = {
price: prices[to === "QAR" ? "AR" : to].v,
scale: BigInt(10) ** tokenData[to].denomination
};
const toScaledPrice = BigInt(Math.round(toData.price * Number(toData.scale)));
const fromValUSD = from.quantity * fromScaledPrice / fromData.scale;
return fromValUSD * toData.scale / toScaledPrice;
}
function getDiscountedQuantity({
liquidated,
rewardToken,
qualifyingPosition,
priceData,
validateMax = false
}, precisionFactor) {
if (!Number.isInteger(precisionFactor)) {
throw new Error("The precision factor has to be an integer");
}
const dept = qualifyingPosition.debts.find(
(d) => d.ticker === liquidated.token
);
const reward = qualifyingPosition.collaterals.find(
(c) => c.ticker === rewardToken
);
if (!dept || !reward) {
throw new Error(
"Liquidated token or reward token is not in the user's position"
);
}
if (validateMax && dept.quantity < liquidated.quantity) {
throw new Error("Not enough tokens to liquidate");
}
let marketValue = getTokenValue(liquidated, rewardToken, priceData);
if (qualifyingPosition.discount > BigInt(0)) {
const precise100 = BigInt(100 * precisionFactor);
marketValue = marketValue * (precise100 + qualifyingPosition.discount) / precise100;
}
if (validateMax && reward.quantity < marketValue) {
throw new Error("Not enough tokens to receive as reward");
}
return marketValue;
}
// src/ao/messaging/getData.ts
import { dryrun } from "@permaweb/aoconnect";
async function getData(messageTags) {
const convertedMessageTags = Object.entries(messageTags).map(
([name, value]) => ({
name,
value
})
);
convertedMessageTags.push({ name: "Protocol-Name", value: "LiquidOps" });
const targetProcessID = messageTags["Target"];
try {
const { Messages, Spawns, Output, Error: Error2 } = await dryrun({
process: targetProcessID,
data: "",
tags: convertedMessageTags
});
return {
Messages,
Spawns,
Output,
Error: Error2
};
} catch (error) {
throw new Error(`Error sending ao dryrun: ${error}`);
}
}
// src/functions/protocolData/getAllPositions.ts
async function getAllPositions({
token
}) {
try {
if (!token) {
throw new Error("Please specify a token.");
}
const { oTokenAddress } = tokenInput(token);
const res = await getData({
Target: oTokenAddress,
Action: "Positions"
});
const allPositions = JSON.parse(res.Messages[0].Data);
const transformedPositions = {};
for (const walletAddress in allPositions) {
const originalPosition = allPositions[walletAddress];
transformedPositions[walletAddress] = {
borrowBalance: BigInt(originalPosition["Borrow-Balance"]),
capacity: BigInt(originalPosition.Capacity),
collateralization: BigInt(originalPosition["Collateralization"]),
liquidationLimit: BigInt(originalPosition["Liquidation-Limit"])
};
}
return transformedPositions;
} catch (error) {
throw new Error(`Error in getAllPositions function: ${error}`);
}
}
// src/ao/utils/dryRunAwait.ts
async function dryRunAwait(seconds) {
const miliseconds = seconds * 1e3;
await new Promise((resolve) => setTimeout(resolve, miliseconds));
}
// src/functions/liquidations/getLiquidations.ts
function convertTicker(ticker) {
if (ticker === "QAR") return "AR";
if (ticker === "WUSDC") return "USDC";
return ticker;
}
async function getLiquidations(precisionFactor) {
try {
if (!Number.isInteger(precisionFactor)) {
throw new Error("The precision factor has to be an integer");
}
const tokensList = Object.keys(tokens);
const redstonePriceFeedRes = await getData({
Target: redstoneOracleAddress,
Action: "v2.Request-Latest-Data",
Tickers: JSON.stringify(collateralEnabledTickers.map(convertTicker))
});
await dryRunAwait(1);
const positionsList = [];
for (const token of tokensList) {
const positions = await getAllPositions({ token });
positionsList.push({
token,
positions
});
await dryRunAwait(1);
}
const auctionsRes = await getData({
Target: controllerAddress,
Action: "Get-Auctions"
});
await dryRunAwait(1);
const prices = JSON.parse(
redstonePriceFeedRes.Messages[0].Data
);
const auctions = JSON.parse(
auctionsRes.Messages[0].Data
);
const auctionTags = Object.fromEntries(
auctionsRes.Messages[0].Tags.map((tag) => [tag.name, tag.value])
);
const maxDiscount = parseFloat(auctionTags["Initial-Discount"] || "0");
const discountInterval = parseInt(auctionTags["Discount-Interval"] || "0");
const globalPositions = /* @__PURE__ */ new Map();
let highestDenomination = BigInt(0);
for (const { token, positions: localPositions } of positionsList) {
const tokenPrice = prices[convertTicker(token)].v;
const tokenDenomination = tokenData[token].denomination;
if (highestDenomination < tokenDenomination)
highestDenomination = tokenDenomination;
const scale = BigInt(10) ** highestDenomination;
const priceScaled = BigInt(Math.round(tokenPrice * Number(scale)));
const scaleDifference = BigInt(10) ** (highestDenomination - tokenDenomination);
for (const [walletAddress, position] of Object.entries(
localPositions
)) {
const posValueUSD = {
borrowBalanceUSD: position.borrowBalance * scaleDifference * priceScaled / scale,
capacityUSD: position.capacity * scaleDifference * priceScaled / scale,
collateralizationUSD: position.collateralization * scaleDifference * priceScaled / scale,
liquidationLimitUSD: position.liquidationLimit * scaleDifference * priceScaled / scale
};
if (!globalPositions.has(walletAddress)) {
globalPositions.set(walletAddress, {
...posValueUSD,
tokenPositions: { [token]: position }
});
} else {
const globalPos = globalPositions.get(walletAddress);
globalPos.borrowBalanceUSD += posValueUSD.borrowBalanceUSD;
globalPos.capacityUSD += posValueUSD.capacityUSD;
globalPos.collateralizationUSD += posValueUSD.collateralizationUSD;
globalPos.liquidationLimitUSD += posValueUSD.liquidationLimitUSD;
globalPos.tokenPositions[token] = position;
}
}
}
const res = /* @__PURE__ */ new Map();
for (const [walletAddress, position] of globalPositions) {
if (position.borrowBalanceUSD <= position.liquidationLimitUSD) continue;
const currentTime = Date.now();
let timeSinceDiscovery = currentTime - (auctions[walletAddress] || currentTime);
if (timeSinceDiscovery > discountInterval) {
timeSinceDiscovery = discountInterval;
}
const discount = BigInt(
Math.max(
Math.floor(
(discountInterval - timeSinceDiscovery) * maxDiscount * precisionFactor / discountInterval
),
0
)
);
const qualifyingPos = {
debts: [],
collaterals: [],
discount
};
for (const [token, localPosition] of Object.entries(
position.tokenPositions
)) {
if (localPosition.borrowBalance > BigInt(0)) {
qualifyingPos.debts.push({
ticker: token,
quantity: localPosition.borrowBalance
});
}
if (localPosition.collateralization > BigInt(0)) {
qualifyingPos.collaterals.push({
ticker: token,
quantity: localPosition.collateralization
});
}
}
res.set(walletAddress, qualifyingPos);
}
return {
liquidations: res,
usdDenomination: highestDenomination,
prices
};
} catch (error) {
throw new Error(`Error in getLiquidations function: ${error}`);
}
}
// src/functions/liquidations/liquidate.ts
var LIQUIDATE_CONFIG = {
action: "Liquidate",
expectedTxCount: 2,
confirmationTag: "Liquidate-Confirmation",
requiredNotices: ["Debit-Notice", "Credit-Notice"],
requiresCreditDebit: true
};
async function liquidate(aoUtils, {
token,
rewardToken,
targetUserAddress,
quantity,
minExpectedQuantity
}) {
try {
if (!token || !rewardToken || !targetUserAddress || !quantity) {
throw new Error(
"Please specify token, reward token, target address, and quantity."
);
}
const { tokenAddress: repayTokenAddress, controllerAddress: controllerAddress2 } = tokenInput(token);
const { tokenAddress: rewardTokenAddress } = tokenInput(rewardToken);
const transferID = await aoUtils.message({
process: repayTokenAddress,
tags: [
{ name: "Action", value: "Transfer" },
{ name: "Quantity", value: quantity.toString() },
{ name: "Recipient", value: controllerAddress2 },
{ name: "X-Target", value: targetUserAddress },
{ name: "X-Reward-Token", value: rewardTokenAddress },
{ name: "X-Action", value: "Liquidate" },
...minExpectedQuantity && [
{
name: "X-Min-Expected-Quantity",
value: minExpectedQuantity.toString()
}
] || [],
{ name: "Protocol-Name", value: "LiquidOps" }
],
signer: aoUtils.signer
});
const transferResult = await validateTransaction(
aoUtils,
transferID,
repayTokenAddress,
LIQUIDATE_CONFIG
);
if (transferResult === "pending") {
return {
status: "pending",
transferID,
response: "Transaction pending."
};
}
if (!transferResult) {
throw new Error("Transaction validation failed");
}
const transactionIds = await findTransactionIds(
aoUtils,
transferID,
repayTokenAddress
);
return {
status: true,
...transactionIds,
transferID
};
} catch (error) {
throw new Error("Error in liquidate function: " + error);
}
}
// src/functions/oTokenData/getBalances.ts
async function getBalances({
token
}) {
try {
if (!token) {
throw new Error("Please specify a token.");
}
const { oTokenAddress } = tokenInput(token);
const res = await getData({
Target: oTokenAddress,
Action: "Balances"
});
if (!res.Messages || !res.Messages[0] || !res.Messages[0].Data) {
throw new Error("Invalid response format from getData");
}
const balance = JSON.parse(res.Messages[0].Data);
const key = Object.keys(balance)[0];
const value = Object.values(balance)[0];
if (!key || !value) {
throw new Error("Invalid balance data format");
}
return { [key]: BigInt(value) };
} catch (error) {
throw new Error(`Error in getBalances function: ${error}`);
}
}
// src/functions/oTokenData/getBorrowAPR.ts
async function getBorrowAPR({
token
}) {
try {
if (!token) {
throw new Error("Please specify a token.");
}
const { oTokenAddress } = tokenInput(token);
const checkDataRes = await getData({
Target: oTokenAddress,
Action: "Get-APR"
});
const tags = checkDataRes.Messages[0].Tags;
const aprResponse = {
"Annual-Percentage-Rate": "",
"Rate-Multiplier": ""
};
tags.forEach((tag) => {
if (tag.name === "Annual-Percentage-Rate" || tag.name === "Rate-Multiplier") {
aprResponse[tag.name] = tag.value;
}
});
const apr = parseFloat(aprResponse["Annual-Percentage-Rate"]);
const rateMultiplier = parseFloat(aprResponse["Rate-Multiplier"]);
return apr / rateMultiplier;
} catch (error) {
throw new Error("Error in getBorrowAPR function: " + error);
}
}
// src/functions/oTokenData/getExchangeRate.ts
async function getExchangeRate({
token,
quantity
}) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { oTokenAddress } = tokenInput(token);
const message = await getData({
Target: oTokenAddress,
Action: "Exchange-Rate-Current",
...quantity && { Quantity: quantity.toString() }
});
const valueTag = message.Messages[0].Tags.find(
(tag) => tag.name === "Value"
);
return BigInt(valueTag.value);
} catch (error) {
throw new Error("Error in getExchangeRate function: " + error);
}
}
// src/functions/oTokenData/getPosition.ts
async function getPosition({
token,
recipient
}) {
try {
if (!token || !recipient) {
throw new Error("Please specify a token and recipient.");
}
const { oTokenAddress } = tokenInput(token);
const res = await getData({
Target: oTokenAddress,
Action: "Position",
...recipient && { Recipient: recipient }
});
const tagsObject = Object.fromEntries(
res.Messages[0].Tags.map((tag) => [tag.name, tag.value])
);
return {
capacity: tagsObject["Capacity"],
borrowBalance: tagsObject["Borrow-Balance"],
collateralTicker: tagsObject["Collateral-Ticker"] === "AR" ? "qAR" : tagsObject["Collateral-Ticker"],
collateralDenomination: tagsObject["Collateral-Denomination"],
collateralization: tagsObject["Collateralization"],
liquidationLimit: tagsObject["Liquidation-Limit"]
};
} catch (error) {
throw new Error("Error in getPosition function: " + error);
}
}
// src/functions/oTokenData/getGlobalPosition.ts
async function getGlobalPosition({
walletAddress
}) {
try {
if (!walletAddress) {
throw new Error("Please specify a wallet address.");
}
const tokensList = Object.keys(tokens);
const redstonePriceFeedRes = await getData({
Target: redstoneOracleAddress,
Action: "v2.Request-Latest-Data",
Tickers: JSON.stringify(
collateralEnabledTickers.map((ticker) => {
if (ticker === "QAR") return "AR";
if (ticker === "WUSDC") return "USDC";
return ticker;
})
)
});
const prices = JSON.parse(
redstonePriceFeedRes.Messages[0].Data
);
const positionsPromises = tokensList.map(async (token) => {
try {
const position = await getPosition({
token,
recipient: walletAddress
});
return {
token,
position
};
} catch (error) {
return {
token,
position: null
};
}
});
const positionsResults = await Promise.all(positionsPromises);
const globalPosition = {
borrowBalanceUSD: BigInt(0),
capacityUSD: BigInt(0),
collateralizationUSD: BigInt(0),
liquidationLimitUSD: BigInt(0),
usdDenomination: BigInt(0),
tokenPositions: {}
};
let highestDenomination = BigInt(0);
for (const { token, position } of positionsResults) {
if (!position) continue;
const tokenPosition = {
borrowBalance: BigInt(position.borrowBalance || 0),
capacity: BigInt(position.capacity || 0),
collateralization: BigInt(position.collateralization || 0),
liquidationLimit: BigInt(position.liquidationLimit || 0),
ticker: token
};
globalPosition.tokenPositions[token] = tokenPosition;
const tokenPrice = prices[token === "QAR" ? "AR" : token === "WUSDC" ? "USDC" : token].v;
const tokenDenomination = tokenData[token].baseDenomination;
if (highestDenomination < tokenDenomination)
highestDenomination = tokenDenomination;
const scale = BigInt(10) ** highestDenomination;
const priceScaled = BigInt(Math.round(tokenPrice * Number(scale)));
const scaleDifference = BigInt(10) ** (highestDenomination - tokenDenomination);
const borrowBalanceUSD = tokenPosition.borrowBalance * scaleDifference * priceScaled / scale;
const capacityUSD = tokenPosition.capacity * scaleDifference * priceScaled / scale;
const collateralizationUSD = tokenPosition.collateralization * scaleDifference * priceScaled / scale;
const liquidationLimitUSD = tokenPosition.liquidationLimit * scaleDifference * priceScaled / scale;
globalPosition.borrowBalanceUSD += borrowBalanceUSD;
globalPosition.capacityUSD += capacityUSD;
globalPosition.collateralizationUSD += collateralizationUSD;
globalPosition.liquidationLimitUSD += liquidationLimitUSD;
}
globalPosition.usdDenomination = highestDenomination;
return {
globalPosition,
prices
};
} catch (error) {
throw new Error(`Error in getGlobalPosition function: ${error}`);
}
}
// src/functions/oTokenData/getInfo.ts
async function getInfo({ token }) {
try {
if (!token) {
throw new Error("Please specify a token.");
}
const { oTokenAddress } = tokenInput(token);
const res = await getData({
Target: oTokenAddress,
Action: "Info"
});
const tagsObject = Object.fromEntries(
res.Messages[0].Tags.map((tag) => [tag.name, tag.value])
);
return {
collateralDenomination: tagsObject["Collateral-Denomination"],
liquidationThreshold: tagsObject["Liquidation-Threshold"],
totalSupply: tagsObject["Total-Supply"],
totalBorrows: tagsObject["Total-Borrows"],
valueLimit: tagsObject["Value-Limit"],
name: tagsObject["Name"],
collateralFactor: tagsObject["Collateral-Factor"],
totalReserves: tagsObject["Total-Reserves"],
cash: tagsObject["Cash"],
oracle: tagsObject["Oracle"],
logo: tagsObject["Logo"],
reserveFactor: tagsObject["Reserve-Factor"],
denomination: tagsObject["Denomination"],
collateralId: tagsObject["Collateral-Id"],
ticker: tagsObject["Ticker"]
};
} catch (error) {
throw new Error("Error in getInfo function: " + error);
}
}
// src/functions/oTokenData/getSupplyAPR.ts
import { Quantity } from "ao-tokens";
async function getSupplyAPR({
token,
getInfoRes,
getBorrowAPRRes
}) {
try {
if (!token) {
throw new Error("Please specify a token.");
}
if (!getBorrowAPRRes) {
getBorrowAPRRes = await getBorrowAPR({ token });
await dryRunAwait(1);
}
const borrowAPY = getBorrowAPRRes;
const { tokenAddress } = tokenInput(token);
if (getInfoRes && getInfoRes.collateralId !== tokenAddress) {
throw new Error("getInfoRes supplied does not match token supplied.");
}
if (!getInfoRes) {
getInfoRes = await getInfo({ token });
}
const {
totalBorrows,
collateralDenomination,
reserveFactor,
totalSupply,
collateralFactor,
cash
} = getInfoRes;
const scaledCollateralDenomination = BigInt(collateralDenomination);
const scaledTotalBorrows = new Quantity(
totalBorrows,
scaledCollateralDenomination
);
const scaledTotalSupply = new Quantity(
totalSupply,
scaledCollateralDenomination
);
const scaledCash = new Quantity(cash, scaledCollateralDenomination);
const scaledCollateralFactor = new Quantity(
BigInt(0),
scaledCollateralDenomination
).fromString(collateralFactor);
const scaledLTV = Quantity.__div(
scaledCollateralFactor,
new Quantity(BigInt(100), BigInt(0))
);
const maximumPotentialBorrows = Quantity.__mul(
scaledTotalSupply,
scaledLTV
);
const notBorrowed = Quantity.__sub(
maximumPotentialBorrows,
scaledTotalBorrows
);
const unutilizedFunds = Quantity.__div(scaledCash, notBorrowed);
const totalPooled = Quantity.__add(scaledTotalBorrows, unutilizedFunds);
const utilizationRate = Quantity.__div(
scaledTotalBorrows,
totalPooled
).toNumber();
const reserveFactorFract = Number(reserveFactor) / 100;
const lnRes = Math.log(1 + borrowAPY);
return Math.exp(lnRes * (1 - reserveFactorFract) * utilizationRate) - 1;
} catch (error) {
throw new Error("Error in getSupplyAPR function: " + error);
}
}
// src/functions/protocolData/getHistoricalAPR.ts
async function getHistoricalAPR({
token,
fillGaps = true
}) {
var _a, _b;
try {
if (!token) {
throw new Error("Please specify a token.");
}
const { oTokenAddress } = tokenInput(token);
const response = await getData({
Target: APRAgentAddress,
Action: "Get-Data",
Token: oTokenAddress,
"Fill-Gaps": fillGaps.toString()
});
if (!((_b = (_a = response.Messages) == null ? void 0 : _a[0]) == null ? void 0 : _b.Data)) {
const errorTag = response.Messages[0].Tags.find(
(tag) => tag.name === "Error"
);
if (errorTag.value === "No data about this market") {
return [
{
apr: 0,
timestamp: Date.now()
}
];
} else {
throw new Error("No historical APR data received");
}
}
return JSON.parse(response.Messages[0].Data);
} catch (error) {
throw new Error("Error in getHistoricalAPR function: " + error);
}
}
// src/functions/utils/getBalance.ts
import { Token, Quantity as Quantity2 } from "ao-tokens";
async function getBalance({
tokenAddress,
walletAddress
}) {
if (!tokenAddress || !walletAddress) {
throw new Error("Please specify a tokenAddress and walletAddress.");
}
try {
const tokenInstance = await Token(tokenAddress);
const balance = await tokenInstance.getBalance(walletAddress);
return new Quantity2(balance.raw, tokenInstance.info.Denomination);
} catch (error) {
throw new Error("Error getting balance: " + error);
}
}
// src/functions/utils/getResult.ts
async function getResult(aoUtils, { transferID, tokenAddress, action }) {
try {
let CONFIG;
if (action === "lend") {
CONFIG = LEND_CONFIG;
} else if (action === "unLend") {
CONFIG = UNLEND_CONFIG;
} else if (action === "borrow") {
CONFIG = BORROW_CONFIG;
} else {
CONFIG = REPAY_CONFIG;
}
const result = validateTransaction(
aoUtils,
transferID,
tokenAddress,
CONFIG
);
return result;
} catch (error) {
throw new Error("Error in getResult function: " + error);
}
}
// src/ao/messaging/sendData.ts
async function sendData(aoUtils, messageTags) {
const convertedMessageTags = Object.entries(messageTags).map(
([name, value]) => ({
name,
value
})
);
convertedMessageTags.push({ name: "Protocol-Name", value: "LiquidOps" });
const targetProcessID = messageTags["Target"];
let messageID;
try {
messageID = await aoUtils.message({
process: targetProcessID,
tags: convertedMessageTags,
signer: aoUtils.signer
});
} catch (error) {
throw new Error(`Error sending ao message: ${error}`);
}
try {
const { Messages, Spawns, Output, Error: Error2 } = await aoUtils.result({
message: messageID,
process: targetProcessID
});
return {
Messages,
Spawns,
Output,
Error: Error2,
initialMessageID: messageID
};
} catch (error) {
throw new Error(`Error reading ao message result: ${error}`);
}
}
// src/functions/utils/transfer.ts
async function transfer(aoUtils, { token, recipient, quantity }) {
var _a;
try {
if (!token || !recipient || !quantity) {
throw new Error("Please specify a token, recipient and quantity.");
}
const { tokenAddress } = tokenInput(token);
const res = await sendData(aoUtils, {
Target: tokenAddress,
Action: "Transfer",
Recipient: recipient,
Quantity: quantity.toString()
});
const hasDebitNotice = (_a = res.Messages[0]) == null ? void 0 : _a.Tags.some(
(tag) => tag.name === "Action" && tag.value === "Debit-Notice"
);
return {
id: res.initialMessageID,
status: hasDebitNotice
};
} catch (error) {
throw new Error("Error in transfer function: " + error);
}
}
// src/ao/utils/connect.ts
import { connect } from "@permaweb/aoconnect";
var DEFAULT_SERVICES = {
MODE: "legacy",
MU_URL: "https://mu.ao-testnet.xyz",
CU_URL: "https://cu.ao-testnet.xyz",
GATEWAY_URL: "https://arweave.net"
};
function connectToAO(services) {
const {
MODE = DEFAULT_SERVICES.MODE,
GRAPHQL_URL,
GRAPHQL_MAX_RETRIES,
GRAPHQL_RETRY_BACKOFF,
GATEWAY_URL = DEFAULT_SERVICES.GATEWAY_URL,
MU_URL = DEFAULT_SERVICES.MU_URL,
CU_URL = DEFAULT_SERVICES.CU_URL
} = services || {};
const { spawn, message, result } = connect({
// @ts-ignore, MODE is needed here but is not in the aoconnect type yet
MODE,
GATEWAY_URL,
GRAPHQL_URL,
GRAPHQL_MAX_RETRIES,
GRAPHQL_RETRY_BACKOFF,
MU_URL,
CU_URL
});
return { spawn, message, result };
}
// src/index.ts
var _LiquidOps = class _LiquidOps {
constructor(signer, configs = {}) {
if (!signer) {
throw new Error("Please specify a ao createDataItemSigner signer");
}
const { spawn, message, result } = connectToAO(configs);
this.aoUtils = {
spawn,
message,
result,
signer,
configs
};
}
//--------------------------------------------------------------------------------------------------------------- borrow
async borrow(params) {
return borrow(this.aoUtils, params);
}
async repay(params) {
return repay(this.aoUtils, params);
}
//--------------------------------------------------------------------------------------------------------------- getTransactions
async getTransactions(params) {
return getTransactions(this.aoUtils, params);
}
//--------------------------------------------------------------------------------------------------------------- lend
async lend(params) {
return lend(this.aoUtils, params);
}
async unLend(params) {
return unLend(this.aoUtils, params);
}
//--------------------------------------------------------------------------------------------------------------- liquidations
getDiscountedQuantity(params) {
return getDiscountedQuantity(params, _LiquidOps.liquidationPrecisionFactor);
}
async getLiquidations() {
return getLiquidations(_LiquidOps.liquidationPrecisionFactor);
}
async liquidate(params) {
return liquidate(this.aoUtils, params);
}
//--------------------------------------------------------------------------------------------------------------- oTokenData
async getBalances(params) {
return getBalances(params);
}
async getBorrowAPR(params) {
return getBorrowAPR(params);
}
async getExchangeRate(params) {
return getExchangeRate(params);
}
async getGlobalPosition(params) {
return getGlobalPosition(params);
}
async getInfo(params) {
return getInfo(params);
}
async getPosition(params) {
return getPosition(params);
}
async getSupplyAPR(params) {
return getSupplyAPR(params);
}
//--------------------------------------------------------------------------------------------------------------- protocolData
async getAllPositions(params) {
return getAllPositions(params);
}
async getHistoricalAPR(params) {
return getHistoricalAPR(params);
}
//--------------------------------------------------------------------------------------------------------------- utils
async getBalance(params) {
return getBalance(params);
}
async getResult(params) {
return getResult(this.aoUtils, params);
}
async transfer(params) {
return transfer(this.aoUtils, params);
}
};
_LiquidOps.liquidationPrecisionFactor = 1e6;
//--------------------------------------------------------------------------------------------------------------- process data
_LiquidOps.oTokens = oTokens;
_LiquidOps.tokens = tokens;
var LiquidOps = _LiquidOps;
var index_default = LiquidOps;
export {
controllerAddress,
index_default as default,
oTokens,
tokenData,
tokenInput,
tokens
};