liquidops
Version:
LiquidOps is an over-collateralised lending and borrowing protocol built on Arweave's L2 AO.
1,599 lines (1,569 loc) • 58.1 kB
JavaScript
// src/ao/utils/connect.ts
import * as aoConnect 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"
};
async function connectToAO(services, maxRetries = 3, initialDelay = 1e3) {
let lastError = null;
let delay = initialDelay;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`AO connection attempt ${attempt}/${maxRetries}`);
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 } = aoConnect.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
});
if (attempt !== 1) {
console.log(`\u2705 AO connected successfully on attempt ${attempt}`);
}
return { spawn, message, result };
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn(`\u274C Connection attempt ${attempt} failed:`, errorMessage);
if (attempt < maxRetries) {
console.log(`\u23F3 Waiting ${delay}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 1.5;
}
}
}
throw new Error(
`Failed to connect to AO after ${maxRetries} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`
);
}
// 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),
deprecated: true,
oIcon: "i_U-jhdMMaib2hK51qPrKXbLo6cx2Nt58_gNz5FA4sw"
},
WAR: {
name: "Wrapped Arweave",
icon: "ICMLzIKdVMedibwgOy014I4yan_F8h2ZhORhRG5dgzs",
ticker: "WAR",
address: "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10",
oTicker: "oWAR",
oAddress: "rAc0aP0g9NXYUXAbvlLjPH_XxyQy6eYmwSuIcf6ukuw",
controllerAddress,
cleanTicker: "wAR",
denomination: BigInt(12),
collateralEnabled: true,
baseDenomination: BigInt(12),
deprecated: false,
oIcon: "lTWBOBtEZ2JvTAHfvoPq5aXRWTVouv7jZ-6B9HTwosU"
},
WUSDC: {
name: "Wrapped USD Circle",
icon: "iNYk0bDqUiH0eLT2rbYjYAI5i126R4ye8iAZb55IaIM",
ticker: "WUSDC",
address: "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ",
oTicker: "oWUSDC",
oAddress: "4MW7uLFtttSLWM-yWEqV9TGD6fSIDrqa4lbTgYL2qHg",
controllerAddress,
cleanTicker: "wUSDC",
denomination: BigInt(6),
collateralEnabled: true,
baseDenomination: BigInt(6),
deprecated: false,
oIcon: "7EEISJIzxC-3RPhgvRc-lAZnP7st1b79_ER4Sc5P_MU"
},
WUSDT: {
name: "Wrapped USD Tether",
icon: "JaxupVYerLRZWLd32llz_3CG8sCQaNhn2hAWm51U_7s",
ticker: "WUSDT",
address: "7j3jUyFpTuepg_uu_sJnwLE6KiTVuA9cLrkfOp2MFlo",
oTicker: "oWUSDT",
oAddress: "9B9J1O5FDoMsFZGJUSOa6TwivsH7LYIfiaizPn7fUHs",
controllerAddress,
cleanTicker: "wUSDT",
denomination: BigInt(18),
collateralEnabled: true,
baseDenomination: BigInt(18),
deprecated: false,
oIcon: "bkAnKOF4NhqPHnccDhPyOzBws42zNE-u9WtxCPdaABU"
},
WETH: {
name: "Wrapped Ethereum",
icon: "Bi7iqzLQXN-wVD3nM8TYGfTI9g7HgGtiD0XuruoQTJk",
ticker: "WETH",
address: "cBgS-V_yGhOe9P1wCIuNSgDA_JS8l4sE5iFcPTr0TD0",
oTicker: "oWETH",
oAddress: "rNa0hdxEZjz_TAUoI85OcPRul_BzoS6Py_3vamJKpr4",
controllerAddress,
cleanTicker: "wETH",
denomination: BigInt(18),
collateralEnabled: true,
baseDenomination: BigInt(18),
deprecated: false,
oIcon: "z1nnBgzGpt-eXHrjD5A9KrQX6dK8E1ONDuBIqB94VTA"
}
};
function convertTicker(ticker) {
if (ticker === "QAR") return "AR";
if (ticker === "WUSDC") return "USDC";
if (ticker === "WAR") return "AR";
if (ticker === "WUSDT") return "USDT";
if (ticker === "WETH") return "ETH";
return ticker;
}
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(aoUtilsInput, { token, quantity, noResult = false }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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
});
if (noResult) {
return transferID;
}
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(aoUtilsInput, { token, quantity, onBehalfOf, noResult = false }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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
});
if (noResult) {
return transferID;
}
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(aoUtilsInput, { token, action, walletAddress, cursor = "" }) {
var _a;
try {
if (!token || !action || !walletAddress) {
throw new Error("Please specify a token, action and walletAddress.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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(aoUtilsInput, { token, quantity, noResult = false }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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
});
if (noResult) {
return transferID;
}
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(aoUtilsInput, { token, quantity, noResult = false }) {
try {
if (!token || !quantity) {
throw new Error("Please specify a token and quantity.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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
});
if (noResult) {
return transferID;
}
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[convertTicker(from.token)].v,
scale: BigInt(10) ** tokenData[from.token].denomination
};
const fromScaledPrice = BigInt(
Math.round(fromData.price * Number(fromData.scale))
);
const toData = {
price: prices[convertTicker(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
})).filter((t) => t.name !== "Owner");
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,
Owner: messageTags.Owner || "1234"
});
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/ao/sharedLogic/globalPositionUtils.ts
function calculateGlobalPositions({
positions,
prices
}) {
const globalPositions = /* @__PURE__ */ new Map();
let highestDenomination = BigInt(0);
for (const [walletAddress, walletPositions] of Object.entries(positions)) {
for (const [token, position] of Object.entries(walletPositions)) {
if (!position) continue;
const tokenDenomination = tokenData[token].denomination;
if (highestDenomination < tokenDenomination) {
highestDenomination = tokenDenomination;
}
}
}
for (const [walletAddress, walletPositions] of Object.entries(positions)) {
const globalPosition = {
borrowBalanceUSD: BigInt(0),
capacityUSD: BigInt(0),
collateralizationUSD: BigInt(0),
liquidationLimitUSD: BigInt(0),
usdDenomination: highestDenomination,
tokenPositions: {}
};
for (const [token, position] of Object.entries(walletPositions)) {
if (!position) continue;
const tokenPosition = {
borrowBalance: BigInt(position.borrowBalance),
capacity: BigInt(position.capacity),
collateralization: BigInt(position.collateralization),
liquidationLimit: BigInt(position.liquidationLimit),
ticker: token
};
globalPosition.tokenPositions[token] = tokenPosition;
const tokenPrice = prices[convertTicker(token)].v;
const tokenDenomination = tokenData[token].denomination;
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;
}
globalPositions.set(walletAddress, globalPosition);
}
return { globalPositions, highestDenomination };
}
// src/functions/liquidations/getLiquidations.ts
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 maxRetries = 3;
const retryDelay = 3e3;
let positions = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
positions = await getAllPositions({ token });
await dryRunAwait(1);
break;
} catch (error) {
console.log(
`Attempt ${attempt} failed for getAllPositions with token ${token}:`,
error
);
if (attempt === maxRetries) {
throw new Error(
`Failed to get all positions for token ${token} after ${maxRetries} attempts: ${error}`
);
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
if (!positions) {
throw new Error(`Unexpected: positions is null for token ${token}`);
}
positionsList.push({
token,
positions
});
}
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"]);
const discountInterval = parseInt(auctionTags["Discount-Interval"]);
const allPositions = {};
for (const { token, positions: localPositions } of positionsList) {
for (const [walletAddress, position] of Object.entries(localPositions)) {
if (!allPositions[walletAddress]) {
allPositions[walletAddress] = {};
}
allPositions[walletAddress][token] = {
borrowBalance: position.borrowBalance,
capacity: position.capacity,
collateralization: position.collateralization,
liquidationLimit: position.liquidationLimit,
ticker: token
};
}
}
const { globalPositions, highestDenomination } = calculateGlobalPositions({
positions: allPositions,
prices
});
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(aoUtilsInput, {
token,
rewardToken,
targetUserAddress,
quantity,
minExpectedQuantity,
noResult = false
}) {
try {
if (!token || !rewardToken || !targetUserAddress || !quantity || !minExpectedQuantity) {
throw new Error(
"Please specify token, reward token, target address, quantity and minExpectedQuantity."
);
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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
});
if (noResult) {
return transferID;
}
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 balances = JSON.parse(res.Messages[0].Data);
const result = {};
for (const key in balances) {
if (Object.prototype.hasOwnProperty.call(balances, key)) {
result[key] = BigInt(balances[key]);
}
}
return result;
} 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: convertTicker(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(convertTicker))
});
await dryRunAwait(1);
const prices = JSON.parse(
redstonePriceFeedRes.Messages[0].Data
);
const positionsPromises = tokensList.map(async (token) => {
const maxRetries = 3;
const retryDelay = 5e3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const position = await getPosition({
token,
recipient: walletAddress
});
await dryRunAwait(1);
return {
token,
position
};
} catch (error) {
console.log(`Attempt ${attempt} failed for token ${token}:`, error);
if (attempt === maxRetries) {
throw new Error(
`Failed to get position for token ${token} after ${maxRetries} attempts: ${error}`
);
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
return {
token,
position: null
};
});
const positionsResults = await Promise.all(positionsPromises);
const positions = {
[walletAddress]: {}
};
for (const { token, position } of positionsResults) {
if (position) {
const tokenPosition = {
borrowBalance: BigInt(position.borrowBalance),
capacity: BigInt(position.capacity),
collateralization: BigInt(position.collateralization),
liquidationLimit: BigInt(position.liquidationLimit),
ticker: token
};
positions[walletAddress][token] = tokenPosition;
}
}
const { globalPositions } = calculateGlobalPositions({
positions,
prices
});
const globalPosition = globalPositions.get(walletAddress);
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[0].toLowerCase() + tag.name.slice(1)).replace(/-/g, ""),
tag.value
])
);
return tagsObject;
} 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 } = getInfoRes;
const scaledCollateralDenomination = BigInt(collateralDenomination);
const scaledTotalBorrows = new Quantity(
totalBorrows,
scaledCollateralDenomination
);
const scaledTotalSupply = new Quantity(
totalSupply,
scaledCollateralDenomination
);
const utilizationRate = Quantity.__div(
scaledTotalBorrows,
scaledTotalSupply
).toNumber();
const reserveFactorFract = Number(reserveFactor) / 100;
return borrowAPY * utilizationRate * (1 - reserveFactorFract);
} catch (error) {
throw new Error("Error in getSupplyAPR function: " + error);
}
}
// src/functions/oTokenData/getCooldown.ts
async function getCooldown({
recipient,
token
}) {
var _a, _b;
if (!recipient) throw new Error("Please specify a recipient");
if (!token) throw new Error("Please specify a token address");
const { oTokenAddress } = tokenInput(token);
const cooldownRes = await getData({
Target: oTokenAddress,
Owner: recipient,
Action: "Is-Cooldown"
});
if (!((_b = (_a = cooldownRes == null ? void 0 : cooldownRes.Messages) == null ? void 0 : _a[0]) == null ? void 0 : _b.Tags)) {
return { onCooldown: false };
}
const cooldownResTags = Object.fromEntries(
cooldownRes.Messages[0].Tags.map((tag) => [
tag.name,
tag.value
])
);
if (!cooldownResTags["Is-Cooldown"]) {
return { onCooldown: false };
}
const expiresOn = parseInt(cooldownResTags["Cooldown-Expires"]);
return {
onCooldown: true,
expiryBlock: expiresOn,
remainingBlocks: expiresOn - parseInt(cooldownResTags["Request-Block-Height"])
};
}
// 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(aoUtilsInput, { transferID, tokenAddress, action }) {
try {
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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 txResult = validateTransaction(
aoUtils,
transferID,
tokenAddress,
CONFIG
);
return txResult;
} 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(aoUtilsInput, { token, recipient, quantity }) {
var _a;
try {
if (!token || !recipient || !quantity) {
throw new Error("Please specify a token, recipient and quantity.");
}
const { spawn, message, result } = await connectToAO(aoUtilsInput.configs);
const aoUtils = {
spawn,
message,
result,
signer: aoUtilsInput.signer,
configs: aoUtilsInput.configs
};
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/functions/utils/trackResult.ts
var SU_ROUTER = "https://su-router.ao-testnet.xyz";
async function trackResult(aoUtilsInput, {
process,
message,
targetProcess,
match,
messageTimestamp,
validateOriginal = true,
validUntil = 1e3 * 60 * 45
}) {
var _a;
if (!process || !message) {
throw new Error("Please specify a process and a message id");
}
if (!match.success && !match.fail) {
throw new Error("Please specify an expected success/fail result match");
}
const aoUtils = await connectToAO(aoUtilsInput.configs);
const matchTag = (tag, expected) => {
if (tag.name !== expected.name) return false;
if (typeof expected.values !== "string") {
return expected.values.includes(tag.value);
}
return tag.value === expected.values;
};
const matchMsg = (msg, expected) => (!expected.Anchor || expected.Anchor === msg.Anchor) && (!expected.Data || expected.Data === msg.Data) && (!expected.Target || expected.Target === msg.Target) && (!expected.Tags || expected.Tags.every((tag1) => msg.Tags.find(
(tag2) => matchTag(tag2, tag1)
)));
let matchedResult;
if (validateOriginal) {
const res = await aoUtils.result({ process, message });
for (const msg of res.Messages) {
if (match.success && matchMsg(msg, match.success)) {
matchedResult = {
match: "success",
message: msg
};
break;
} else if (match.fail && matchMsg(msg, match.fail)) {
matchedResult = {
match: "fail",
message: msg
};
break;
}
}
}
if (matchedResult) {
return matchedResult;
}
if (!messageTimestamp) {
const res = await fetch(`${SU_ROUTER}/${message}?process-id=${process}`);
if (res.status >= 400) {
throw new Error(`Could not find message ${message} on process ${process}`);
}
const messageData = await res.json();
messageTimestamp = parseInt((_a = messageData.assignme