diff --git a/src/ao/sharedLogic/globalPositionUtils.ts b/src/ao/sharedLogic/globalPositionUtils.ts index b0bf61d4..469f6047 100644 --- a/src/ao/sharedLogic/globalPositionUtils.ts +++ b/src/ao/sharedLogic/globalPositionUtils.ts @@ -25,12 +25,12 @@ export interface GlobalPosition { }; } -type RedstonePrices = Record; - // Shared utility for calculating global positions interface CalculateGlobalPositionParams { positions: Record>; // walletAddress -> token -> position - prices: RedstonePrices; + prices: { + [ticker: string]: number; + }; } interface CalculateGlobalPositionResult { @@ -83,7 +83,7 @@ export function calculateGlobalPositions({ globalPosition.tokenPositions[token] = tokenPosition; // Get token price and denomination for USD conversion - const tokenPrice = prices[convertTicker(token)].v; + const tokenPrice = prices[convertTicker(token)]; const tokenDenomination = tokenData[token as SupportedTokensTickers].denomination; diff --git a/src/ao/utils/connect.ts b/src/ao/utils/connect.ts index 8555438d..51cbbed1 100644 --- a/src/ao/utils/connect.ts +++ b/src/ao/utils/connect.ts @@ -16,6 +16,7 @@ export type Services = { GRAPHQL_RETRY_BACKOFF?: number; MU_URL?: string; CU_URL?: string; + HB_NODE_URL?: string; }; export interface AoUtils { @@ -30,6 +31,7 @@ const DEFAULT_SERVICES: Services = { MU_URL: "https://mu.ao-testnet.xyz", CU_URL: "https://cu.ao-testnet.xyz", GATEWAY_URL: "https://arweave.net", + HB_NODE_URL: "https://state-2.forward.computer", }; export function connectToAO(services?: Partial) { @@ -46,6 +48,7 @@ export function connectToAO(services?: Partial) { GATEWAY_URL = DEFAULT_SERVICES.GATEWAY_URL, MU_URL = DEFAULT_SERVICES.MU_URL, CU_URL = DEFAULT_SERVICES.CU_URL, + HB_NODE_URL = DEFAULT_SERVICES.HB_NODE_URL, } = services || {}; const configs = { @@ -55,6 +58,7 @@ export function connectToAO(services?: Partial) { GRAPHQL_RETRY_BACKOFF, MU_URL, CU_URL, + HB_NODE_URL, }; const { spawn, message, result, dryrun } = connect({ diff --git a/src/functions/liquidations/getDiscountedQuantity.ts b/src/functions/liquidations/getDiscountedQuantity.ts index 64a9180a..8726b511 100644 --- a/src/functions/liquidations/getDiscountedQuantity.ts +++ b/src/functions/liquidations/getDiscountedQuantity.ts @@ -15,7 +15,7 @@ function getTokenValue( ) { // token datas const fromData = { - price: prices[convertTicker(from.token)].v, + price: prices[convertTicker(from.token)], scale: BigInt(10) ** tokenData[from.token as SupportedTokensTickers].denomination, @@ -24,7 +24,7 @@ function getTokenValue( Math.round(fromData.price * Number(fromData.scale)), ); const toData = { - price: prices[convertTicker(to)].v, + price: prices[convertTicker(to)], scale: BigInt(10) ** tokenData[to as SupportedTokensTickers].denomination, }; const toScaledPrice = BigInt(Math.round(toData.price * Number(toData.scale))); diff --git a/src/functions/liquidations/getLiquidations.ts b/src/functions/liquidations/getLiquidations.ts index 52497c17..49ed027b 100644 --- a/src/functions/liquidations/getLiquidations.ts +++ b/src/functions/liquidations/getLiquidations.ts @@ -1,6 +1,4 @@ -import { getData } from "../../ao/messaging/getData"; import { - collateralEnabledTickers, tokens, controllerAddress, } from "../../ao/utils/tokenAddressData"; @@ -9,13 +7,16 @@ import { getAllPositions, GetAllPositionsRes, } from "../protocolData/getAllPositions"; -import { dryRunAwait } from "../../ao/utils/dryRunAwait"; -import { convertTicker } from "../../ao/utils/tokenAddressData"; import { calculateGlobalPositions, TokenPosition, } from "../../ao/sharedLogic/globalPositionUtils"; import { Services } from "../../ao/utils/connect"; +import Patching from "../utils/patching"; + +export interface RedstonePrices { + [ticker: string]: number; +}; export interface GetLiquidationsRes { liquidations: Map; @@ -38,11 +39,6 @@ export interface QualifyingPosition { discount: BigInt; } -export type RedstonePrices = Record< - string, - { t: number; a: string; v: number } ->; - interface Tag { name: string; value: string; @@ -60,20 +56,16 @@ export async function getLiquidations( // Get list of tokens to process const tokensList = Object.keys(tokens); + // patching config + const patching = new Patching(config?.HB_NODE_URL); + // Make a request to RedStone oracle process for prices (same used onchain) - const redstonePriceFeedRes = await getData( - { - Owner: controllerAddress, - Target: redstoneOracleAddress, - Action: "v2.Request-Latest-Data", - Tickers: JSON.stringify(collateralEnabledTickers.map(convertTicker)), - }, - config, + const prices = await patching.now( + redstoneOracleAddress, + "/price", + { json: true } ); - // add dry run await to not get rate limited - await dryRunAwait(1); - // Get positions for each token const positionsList = []; for (const token of tokensList) { @@ -84,8 +76,6 @@ export async function getLiquidations( for (let attempt = 1; attempt <= maxRetries; attempt++) { try { positions = await getAllPositions({ token }, config); - // add dry run await to not get rate limited - await dryRunAwait(1); break; // Success, exit retry loop } catch (error) { console.log( @@ -117,30 +107,22 @@ export async function getLiquidations( } // get discovered liquidations - const auctionsRes = await getData( - { - Target: controllerAddress, - Action: "Get-Auctions", - }, - config, - ); - // add dry run await to not get rate limited - await dryRunAwait(1); - - // parse prices and auctions - const prices: RedstonePrices = JSON.parse( - redstonePriceFeedRes.Messages[0].Data, - ); - const auctions: Record = JSON.parse( - auctionsRes.Messages[0].Data, - ); + const [auctions, discountConfig] = await Promise.all([ + patching.now( + controllerAddress, + "/auctions", + { json: true } + ), + patching.compute( + controllerAddress, + "/discount-config", + { json: true } + ) + ]); // maximum discount percentage and discount period - const auctionTags = Object.fromEntries( - auctionsRes.Messages[0].Tags.map((tag: Tag) => [tag.name, tag.value]), - ); - const maxDiscount = parseFloat(auctionTags["Initial-Discount"]); - const discountInterval = parseInt(auctionTags["Discount-Interval"]); + const maxDiscount = discountConfig.max; + const discountInterval = discountConfig.interval; // Convert positions to the format expected by calculateGlobalPositions const allPositions: Record> = {}; diff --git a/src/functions/liquidations/getLiquidationsMap.ts b/src/functions/liquidations/getLiquidationsMap.ts index 5ee73922..b6585251 100644 --- a/src/functions/liquidations/getLiquidationsMap.ts +++ b/src/functions/liquidations/getLiquidationsMap.ts @@ -1,4 +1,3 @@ -import { getData } from "../../ao/messaging/getData"; import { collateralEnabledTickers, controllerAddress, @@ -17,6 +16,7 @@ import { } from "../../ao/sharedLogic/globalPositionUtils"; import { RedstonePrices } from "./getLiquidations"; import { Services } from "../../ao/utils/connect"; +import Patching from "../utils/patching"; export interface GetLiquidationsMapRes { /** The wallet/user address that owns this position */ @@ -42,20 +42,16 @@ export async function getLiquidationsMap( // Get list of tokens to process const tokensList = Object.keys(tokens); + // setup patching + const patching = new Patching(config?.HB_NODE_URL); + // Make a request to RedStone oracle process for prices (same used onchain) - const redstonePriceFeedRes = await getData( - { - Owner: controllerAddress, - Target: redstoneOracleAddress, - Action: "v2.Request-Latest-Data", - Tickers: JSON.stringify(collateralEnabledTickers.map(convertTicker)), - }, - config, + const prices = await patching.now( + redstoneOracleAddress, + "/price", + { json: true } ); - // add dry run await to not get rate limited - await dryRunAwait(1); - // Get positions for each token const positionsList = []; for (const token of tokensList) { @@ -98,11 +94,6 @@ export async function getLiquidationsMap( }); } - // parse prices - const prices: RedstonePrices = JSON.parse( - redstonePriceFeedRes.Messages[0].Data, - ); - // Convert positions to the format expected by calculateGlobalPositions const allPositions: Record> = {}; diff --git a/src/functions/oTokenData/getBalances.ts b/src/functions/oTokenData/getBalances.ts index d5a3602d..3b837998 100644 --- a/src/functions/oTokenData/getBalances.ts +++ b/src/functions/oTokenData/getBalances.ts @@ -1,6 +1,6 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching from "../utils/patching"; export interface GetBalances { token: TokenInput; @@ -19,20 +19,13 @@ export async function getBalances( const { oTokenAddress } = tokenInput(token); - const res = await getData( - { - Target: oTokenAddress, - Action: "Balances", - }, - config, + const patching = new Patching(config?.HB_NODE_URL); + const balances = await patching.compute( + oTokenAddress, + "/balances", + { json: true } ); - 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: GetBalancesRes = {}; for (const key in balances) { diff --git a/src/functions/oTokenData/getBorrowAPR.ts b/src/functions/oTokenData/getBorrowAPR.ts index acd73020..1506b759 100644 --- a/src/functions/oTokenData/getBorrowAPR.ts +++ b/src/functions/oTokenData/getBorrowAPR.ts @@ -1,6 +1,6 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching from "../utils/patching"; export interface GetBorrowAPR { token: TokenInput; @@ -17,38 +17,15 @@ export async function getBorrowAPR( throw new Error("Please specify a token."); } + const patching = new Patching(config?.HB_NODE_URL); const { oTokenAddress } = tokenInput(token); - - const checkDataRes = await getData( - { - Target: oTokenAddress, - Action: "Get-APR", - }, - config, + const res = await patching.now( + oTokenAddress, + "/pool-state/rates", + { json: true } ); - const tags = checkDataRes.Messages[0].Tags; - const aprResponse: { - "Annual-Percentage-Rate": string; - "Rate-Multiplier": string; - } = { - "Annual-Percentage-Rate": "", - "Rate-Multiplier": "", - }; - - tags.forEach((tag: { name: string; value: string }) => { - 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; + return parseFloat(res.borrow); } catch (error) { throw new Error("Error in getBorrowAPR function: " + error); } diff --git a/src/functions/oTokenData/getCooldown.ts b/src/functions/oTokenData/getCooldown.ts index b08f3efd..f59434b2 100644 --- a/src/functions/oTokenData/getCooldown.ts +++ b/src/functions/oTokenData/getCooldown.ts @@ -1,6 +1,6 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { tokenInput } from "../../ao/utils/tokenInput"; +import Patching from "../utils/patching"; export interface GetCooldown { recipient: string; @@ -23,37 +23,30 @@ export async function getCooldown( if (!token) throw new Error("Please specify a token address"); const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); - const cooldownRes = await getData( - { - Target: oTokenAddress, - Owner: recipient, - Action: "Is-Cooldown", - }, - config, + const cooldownRes = await patching.now( + oTokenAddress, + `/cooldowns/${recipient}` ); - if (!cooldownRes?.Messages?.[0]?.Tags) { + if (!cooldownRes || cooldownRes == "") { return { onCooldown: false }; } - const cooldownResTags = Object.fromEntries( - cooldownRes.Messages[0].Tags.map((tag: { name: string; value: string }) => [ - tag.name, - tag.value, - ]), - ); + const networkInfo = await ( + await fetch(`${config?.GATEWAY_URL || "https://arweave.net"}/info`) + ).json(); + const currentBlock = networkInfo?.height || 0; + const expiryBlock = parseInt(cooldownRes); - if (!cooldownResTags["Is-Cooldown"]) { + if (expiryBlock <= currentBlock) { return { onCooldown: false }; } - const expiresOn = parseInt(cooldownResTags["Cooldown-Expires"]); - return { onCooldown: true, - expiryBlock: expiresOn, - remainingBlocks: - expiresOn - parseInt(cooldownResTags["Request-Block-Height"]), + expiryBlock, + remainingBlocks: expiryBlock - currentBlock, }; } diff --git a/src/functions/oTokenData/getCurrentState.ts b/src/functions/oTokenData/getCurrentState.ts new file mode 100644 index 00000000..772af2f8 --- /dev/null +++ b/src/functions/oTokenData/getCurrentState.ts @@ -0,0 +1,27 @@ +import { Services } from "../../ao/utils/connect"; +import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching, { PatchStateoToken } from "../utils/patching"; + +export interface GetCurrentState { + token: TokenInput; +} + +export type GetCurrentStateRes = PatchStateoToken["pool-state"]; + +export async function getCurrentState( + { token }: GetCurrentState, + config?: Services, +): Promise { + if (!token) { + throw new Error("Please specify a token."); + } + + const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); + + return await patching.now( + oTokenAddress, + "/pool-state", + { json: true } + ); +} diff --git a/src/functions/oTokenData/getEnvironment.ts b/src/functions/oTokenData/getEnvironment.ts new file mode 100644 index 00000000..3f2a3e8b --- /dev/null +++ b/src/functions/oTokenData/getEnvironment.ts @@ -0,0 +1,27 @@ +import { Services } from "../../ao/utils/connect"; +import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching, { PatchStateoToken } from "../utils/patching"; + +export interface GetEnvironment { + token: TokenInput; +} + +export type GetEnvironmentRes = PatchStateoToken["environment"]; + +export async function getEnvironment( + { token }: GetEnvironment, + config?: Services, +): Promise { + if (!token) { + throw new Error("Please specify a token."); + } + + const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); + + return await patching.compute( + oTokenAddress, + "/environment", + { json: true } + ); +} diff --git a/src/functions/oTokenData/getExchangeRate.ts b/src/functions/oTokenData/getExchangeRate.ts index b1b098c8..add466fc 100644 --- a/src/functions/oTokenData/getExchangeRate.ts +++ b/src/functions/oTokenData/getExchangeRate.ts @@ -1,6 +1,6 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching from "../utils/patching"; export interface GetExchangeRate { token: TokenInput; @@ -19,21 +19,25 @@ export async function getExchangeRate( } const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); - const message = await getData( - { - Target: oTokenAddress, - Action: "Exchange-Rate-Current", - ...(quantity && { Quantity: quantity.toString() }), - }, - config, + const tokenInfo = await patching.now( + oTokenAddress, + "/token-info", + { json: true } ); - const valueTag = message.Messages[0].Tags.find( - (tag: { name: string; value: string }) => tag.name === "Value", + const totalSupply = BigInt(tokenInfo.supply); + if (totalSupply === BigInt(0)) return quantity; + + const poolState = await patching.now( + oTokenAddress, + "/pool-state", + { json: true } ); + const totalPooled = BigInt(poolState.cash) + BigInt(poolState["total-borrows"]) - BigInt(poolState["total-reserves"]); - return BigInt(valueTag.value); + return totalPooled * (quantity as bigint) / totalSupply; } catch (error) { throw new Error("Error in getExchangeRate function: " + error); } diff --git a/src/functions/oTokenData/getGlobalPosition.ts b/src/functions/oTokenData/getGlobalPosition.ts index 99f96903..f3792ed4 100644 --- a/src/functions/oTokenData/getGlobalPosition.ts +++ b/src/functions/oTokenData/getGlobalPosition.ts @@ -1,21 +1,17 @@ -import { getData } from "../../ao/messaging/getData"; import { tokens, redstoneOracleAddress, - controllerAddress, } from "../../ao/utils/tokenAddressData"; -import { collateralEnabledTickers } from "../../ao/utils/tokenAddressData"; import { getPosition } from "./getPosition"; import { dryRunAwait } from "../../ao/utils/dryRunAwait"; -import { convertTicker } from "../../ao/utils/tokenAddressData"; import { calculateGlobalPositions, TokenPosition, GlobalPosition, } from "../../ao/sharedLogic/globalPositionUtils"; import { Services } from "../../ao/utils/connect"; - -type RedstonePrices = Record; +import Patching from "../utils/patching"; +import { RedstonePrices } from "../liquidations/getLiquidations"; export interface GetGlobalPositionRes { globalPosition: GlobalPosition; @@ -35,25 +31,17 @@ export async function getGlobalPosition( throw new Error("Please specify a wallet address."); } + // setup patching + const patching = new Patching(config?.HB_NODE_URL); + // Get list of tokens to process const tokensList = Object.keys(tokens); // Make a request to RedStone oracle for prices - const redstonePriceFeedRes = await getData( - { - Owner: controllerAddress, - Target: redstoneOracleAddress, - Action: "v2.Request-Latest-Data", - Tickers: JSON.stringify(collateralEnabledTickers.map(convertTicker)), - }, - config, - ); - // add dry run await to not get rate limited - await dryRunAwait(1); - - // Parse prices - const prices: RedstonePrices = JSON.parse( - redstonePriceFeedRes.Messages[0].Data, + const prices = await patching.now( + redstoneOracleAddress, + "/price", + { json: true } ); // Fetch positions for each token diff --git a/src/functions/oTokenData/getInfo.ts b/src/functions/oTokenData/getInfo.ts index 3d41dc6e..572486bb 100644 --- a/src/functions/oTokenData/getInfo.ts +++ b/src/functions/oTokenData/getInfo.ts @@ -1,39 +1,12 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching, { PatchStateoToken } from "../utils/patching"; export interface GetInfo { token: TokenInput; } -export interface GetInfoRes { - collateralDenomination: string; - liquidationThreshold: string; - totalSupply: string; - totalBorrows: string; - valueLimit: string; - name: string; - collateralFactor: string; - totalReserves: string; - cash: string; - oracle: string; - logo: string; - reserveFactor: string; - denomination: string; - collateralId: string; - ticker: string; - kinkParam: string; - jumpRate: string; - baseRate: string; - utilization: string; - initRate: string; - oracleDelayTolerance: string; -} - -interface Tag { - name: string; - value: string; -} +export type GetInfoRes = PatchStateoToken["token-info"]; export async function getInfo( { token }: GetInfo, @@ -45,23 +18,13 @@ export async function getInfo( } const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); - const res = await getData( - { - Target: oTokenAddress, - Action: "Info", - }, - config, + return await patching.compute( + oTokenAddress, + "/token-info", + { json: true } ); - - const tagsObject = Object.fromEntries( - res.Messages[0].Tags.map((tag: Tag) => [ - (tag.name[0].toLowerCase() + tag.name.slice(1)).replace(/-/g, ""), - tag.value, - ]), - ); - - return tagsObject as GetInfoRes; } catch (error) { throw new Error("Error in getInfo function: " + error); } diff --git a/src/functions/oTokenData/getPosition.ts b/src/functions/oTokenData/getPosition.ts index 9a853b6d..e5045787 100644 --- a/src/functions/oTokenData/getPosition.ts +++ b/src/functions/oTokenData/getPosition.ts @@ -1,21 +1,13 @@ -import { getData } from "../../ao/messaging/getData"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; -import { convertTicker } from "../../ao/utils/tokenAddressData"; import { Services } from "../../ao/utils/connect"; +import Patching, { PatchStateoToken } from "../utils/patching"; export interface GetPosition { token: TokenInput; recipient: string; } -export interface GetPositionRes { - capacity: string; - borrowBalance: string; - collateralTicker: string; - collateralDenomination: string; - collateralization: string; - liquidationLimit: string; -} +export type GetPositionRes = PatchStateoToken["positions"][""]; interface Tag { name: string; @@ -32,28 +24,13 @@ export async function getPosition( } const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); - const res = await getData( - { - Target: oTokenAddress, - Action: "Position", - ...(recipient && { Recipient: recipient }), - }, - config, - ); - - const tagsObject = Object.fromEntries( - res.Messages[0].Tags.map((tag: 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"], - }; + return await patching.now( + oTokenAddress, + `/positions/${recipient}`, + { json: true } + ) } catch (error) { throw new Error("Error in getPosition function: " + error); } diff --git a/src/functions/oTokenData/getSupplyAPR.ts b/src/functions/oTokenData/getSupplyAPR.ts index 8cafbe9a..10c2621b 100644 --- a/src/functions/oTokenData/getSupplyAPR.ts +++ b/src/functions/oTokenData/getSupplyAPR.ts @@ -4,6 +4,9 @@ import { getBorrowAPR, GetBorrowAPRRes } from "./getBorrowAPR"; import { getInfo, GetInfoRes } from "./getInfo"; import { dryRunAwait } from "../../ao/utils/dryRunAwait"; import { Services } from "../../ao/utils/connect"; +import { getEnvironment } from "./getEnvironment"; +import { getCurrentState } from "./getCurrentState"; +import { tokenData } from "../../ao/utils/tokenAddressData"; export interface GetSupplyAPR { token: TokenInput; @@ -24,31 +27,23 @@ export async function getSupplyAPR( if (!getBorrowAPRRes) { getBorrowAPRRes = await getBorrowAPR({ token }, config); - // add await for 1 second due to double dry run request - await dryRunAwait(1); } const borrowAPY = getBorrowAPRRes; - const { tokenAddress } = tokenInput(token); - // validate getInfoRes is for the correct token - if (getInfoRes && getInfoRes.collateralId !== tokenAddress) { - throw new Error("getInfoRes supplied does not match token supplied."); - } - if (!getInfoRes) { - getInfoRes = await getInfo({ token }, config); - } - - const { totalBorrows, collateralDenomination, reserveFactor, totalSupply } = - getInfoRes; + const [environment, currentState, info] = await Promise.all([ + getEnvironment({ token }, config), + getCurrentState({ token }, config), + getInfo({ token }, config) + ]); - const scaledCollateralDenomination = BigInt(collateralDenomination); + const scaledCollateralDenomination = tokenData[token].baseDenomination; const scaledTotalBorrows = new Quantity( - totalBorrows, + currentState["total-borrows"], scaledCollateralDenomination, ); const scaledTotalSupply = new Quantity( - totalSupply, + info.supply, scaledCollateralDenomination, ); @@ -59,7 +54,7 @@ export async function getSupplyAPR( ).toNumber(); // Reserve factor in fractions - const reserveFactorFract = Number(reserveFactor) / 100; + const reserveFactorFract = Number(environment["risk-parameters"]["reserve-factor"]) / 100; // Apply standard Compound V2 formula: // Supply APY = Borrow APY × Utilization Rate × (1 - Reserve Factor) diff --git a/src/functions/protocolData/getAllPositions.ts b/src/functions/protocolData/getAllPositions.ts index e6bec247..8e388df2 100644 --- a/src/functions/protocolData/getAllPositions.ts +++ b/src/functions/protocolData/getAllPositions.ts @@ -1,6 +1,6 @@ -import { getData } from "../../ao/messaging/getData"; import { Services } from "../../ao/utils/connect"; import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching from "../utils/patching"; export interface GetAllPositions { token: TokenInput; @@ -25,27 +25,24 @@ export async function getAllPositions( } const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); - const res = await getData( - { - Target: oTokenAddress, - Action: "Positions", - }, - config, + const allPositions = await patching.now( + oTokenAddress, + "/positions", + { json: true } ); - const allPositions = JSON.parse(res.Messages[0].Data); - const transformedPositions: GetAllPositionsRes = {}; 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"]), + borrowBalance: BigInt(originalPosition.borrowBalance), + capacity: BigInt(originalPosition.capacity), + collateralization: BigInt(originalPosition.collateralization), + liquidationLimit: BigInt(originalPosition.liquidationLimit), }; } diff --git a/src/functions/utils/getBalance.ts b/src/functions/utils/getBalance.ts index d8f3016f..34d4c503 100644 --- a/src/functions/utils/getBalance.ts +++ b/src/functions/utils/getBalance.ts @@ -1,5 +1,6 @@ import { Token, Quantity } from "ao-tokens"; -import { getInfo } from "../oTokenData/getInfo"; +import Patching from "./patching"; +import { Services } from "../../ao/utils/connect"; export interface GetBalance { tokenAddress: string; @@ -11,16 +12,23 @@ export type GetBalanceRes = Quantity; export async function getBalance({ tokenAddress, walletAddress, -}: GetBalance): Promise { +}: GetBalance, config?: Services): Promise { 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 Quantity(balance.raw, tokenInstance.info.Denomination); - } catch (error) { - throw new Error("Error getting balance: " + error); - } + const patching = new Patching(config?.HB_NODE_URL); + const [rawBalance, tokenInfo] = await Promise.all([ + patching.now( + tokenAddress, + `/balances/${walletAddress}` + ), + patching.compute( + tokenAddress, + "/token-info", + { json: true } + ) + ]); + + return new Quantity(rawBalance, BigInt(tokenInfo.denomination)); } diff --git a/src/functions/utils/getOrderResult.ts b/src/functions/utils/getOrderResult.ts new file mode 100644 index 00000000..17698bfa --- /dev/null +++ b/src/functions/utils/getOrderResult.ts @@ -0,0 +1,29 @@ +import { Services } from "../../ao/utils/connect"; +import { TokenInput, tokenInput } from "../../ao/utils/tokenInput"; +import Patching, { PatchStateoToken } from "../utils/patching"; + +export interface GetOrderResult { + token: TokenInput; + /** The order ID is the initiating message ID (pushed-for tag value) */ + orderId: string; +} + +export type GetOrderResultRes = PatchStateoToken["orders"][""]; + +export async function getOrderResult( + { token, orderId }: GetOrderResult, + config?: Services, +): Promise { + if (!token || !orderId) { + throw new Error("Please specify a token and an order ID."); + } + + const { oTokenAddress } = tokenInput(token); + const patching = new Patching(config?.HB_NODE_URL); + + return await patching.now( + oTokenAddress, + `/orders/${orderId}`, + { json: true } + ); +} diff --git a/src/functions/utils/getPrice.ts b/src/functions/utils/getPrice.ts index 612d5ca7..e823dac7 100644 --- a/src/functions/utils/getPrice.ts +++ b/src/functions/utils/getPrice.ts @@ -1,12 +1,8 @@ import { TokenInput } from "../../ao/utils/tokenInput"; -import { getData } from "../../ao/messaging/getData"; -import { - controllerAddress, - convertTicker, -} from "../../ao/utils/tokenAddressData"; +import { convertTicker } from "../../ao/utils/tokenAddressData"; import { redstoneOracleAddress } from "../../ao/utils/tokenAddressData"; -import { RedstonePrices } from "../liquidations/getLiquidations"; import { Services } from "../../ao/utils/connect"; +import Patching from "./patching"; export interface GetPrice { token: TokenInput | string; @@ -22,23 +18,12 @@ export async function getPrice( throw new Error("Please specify a token."); } - try { - const redstonePriceFeedRes = await getData( - { - Owner: controllerAddress, - Target: redstoneOracleAddress, - Action: "v2.Request-Latest-Data", - Tickers: JSON.stringify([convertTicker(token)]), - }, - config, - ); + const patching = new Patching(config?.HB_NODE_URL); + const prices = await patching.now( + redstoneOracleAddress, + "/price", + { json: true } + ); - const prices: RedstonePrices = JSON.parse( - redstonePriceFeedRes.Messages[0].Data, - ); - - return prices[convertTicker(token)].v; - } catch (error) { - throw new Error("Error getting price: " + error); - } + return prices[convertTicker(token)] || 0; } diff --git a/src/functions/utils/patching.ts b/src/functions/utils/patching.ts new file mode 100644 index 00000000..807416ec --- /dev/null +++ b/src/functions/utils/patching.ts @@ -0,0 +1,180 @@ +export default class Patching { + #hbNode: string; + + constructor(node = "https://state-2.forward.computer") { + this.#hbNode = node; + } + + async compute(process: Process, path: Path, config?: Config) { + return await this.#get( + process, + path, + "compute", + config + ); + } + + async now(process: Process, path: Path, config?: Config) { + return await this.#get( + process, + path, + "now", + config + ); + } + + async #get(process: Process, path: Path, method: "now" | "compute", config?: Config): Promise, Path> : string> { + const searchParams = new URLSearchParams(); + + if (config?.json) { + searchParams.set("require-codec", "application/json"); + searchParams.set("accept-bundle", "true"); + } + + const url = new URL( + `${process}~process@1.0/${method}/${path}?${searchParams.toString()}`, + this.#hbNode + ); + + const res = await fetch(url); + + if (config?.json) { + const { body } = await res.json(); + + return body; + } + + // @ts-expect-error + return await res.text(); + } +} + +type PatchState = + Process extends "SmmMv0rJwfIDVM3RvY2-P729JFYwhdGSeGo2deynbfY" // controller + ? PatchStateController + : Process extends "R5rRjBFS90qIGaohtzd1IoyPwZD0qJZ25QXkP7_p5a0" // oracle + ? PatchStateOracle + : PatchStateoToken; // oTokens + +type PathValue = + Path extends `/${infer Rest}` + ? PathValue + : Path extends `${infer Key}/${infer SubPath}` + ? Key extends keyof T + ? PathValue + : never + : Path extends keyof T + ? T[Path] + : never; + +interface GetPatchConfig { + json: boolean; +} + +interface Friend { + id: string; + ticker: string; + oToken: string; + denomination: number; +} + +export interface PatchStateoToken { + "token-info": { + name: string; + ticker: string; + logo: string; + denomination: string; + supply: string; + }; + environment: { + collateral: { + id: string; + denomination: number; + }; + "risk-parameters": { + "collateral-factor": number; + "liquidation-threshold": number; + "reserve-factor": number; + }; + "limits": { + "value-limit": string; + "cooldown-period": number; + "enabled-interactions": string[]; + "disabled-interactions": string[]; + }; + "oracle-parameters": { + oracle: string; + "oracle-delay-tolerance": number; + }; + "interest-model": { + rates: { + init: number; + base: number; + jump: number; + }; + "kink-param": number; + }; + friends: Friend[]; + }; + "pool-state": { + "total-borrows": string; + cash: string; + "total-reserves": string; + utilization: string; + rates: { + borrow: string; + supply: string; + }; + "interest-accrual": { + "borrow-index": string; + "last-updated": number; + "reserves-remainder": string; + }; + }; + balances: Record; + cooldowns: Record; + positions: Record; + loans: Record; + "interest-indices": Record; + orders: Record; +} + +interface PatchStateController { + "token-info": { + name: string; + }; + oracle: string; + "discount-config": { + min: number; + max: number; + interval: number; + }; + tokens: Friend[]; + queue: { + address: string; + origin: string; + }; + auctions: Record; +} + +interface PatchStateOracle { + "token-info": { + name: string; + version: string; + }; + whitelist: string[]; + relayers: string[]; + prices: Record; + price: Record; +} diff --git a/src/index.ts b/src/index.ts index e8b60b59..441f2ffb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,6 +125,9 @@ import { GetEarningsRes, } from "./functions/lend/getEarnings"; import { DryRunFIFO } from "./ao/messaging/DryRunFIFO"; +import { GetEnvironment, getEnvironment, GetEnvironmentRes } from "./functions/oTokenData/getEnvironment"; +import { getCurrentState, GetCurrentState, GetCurrentStateRes } from "./functions/oTokenData/getCurrentState"; +import { getOrderResult, GetOrderResult, GetOrderResultRes } from "./functions/utils/getOrderResult"; class LiquidOps { private signer: any; @@ -230,6 +233,14 @@ class LiquidOps { return getInfo(params, this.configs); } + async getEnvironment(params: GetEnvironment): Promise { + return getEnvironment(params, this.configs); + } + + async getCurrentState(params: GetCurrentState): Promise { + return getCurrentState(params, this.configs); + } + async getPosition(params: GetPosition): Promise { return getPosition(params, this.configs); } @@ -253,7 +264,7 @@ class LiquidOps { //--------------------------------------------------------------------------------------------------------------- utils async getBalance(params: GetBalance): Promise { - return getBalance(params); + return getBalance(params, this.configs); } async getPrice(params: GetPrice): Promise { @@ -272,6 +283,10 @@ class LiquidOps { return trackResult({ signer: this.signer, configs: this.configs }, params); } + async getOrderResult(params: GetOrderResult): Promise { + return getOrderResult(params, this.configs); + } + //--------------------------------------------------------------------------------------------------------------- process data static oTokens = oTokens; @@ -323,6 +338,10 @@ export type { GetPositionRes, GetSupplyAPR, GetSupplyAPRRes, + GetCurrentState, + GetCurrentStateRes, + GetEnvironment, + GetEnvironmentRes, // protocol data GetAllPositions, @@ -339,6 +358,8 @@ export type { TransferRes, TrackResult, TrackResultRes, + GetOrderResult, + GetOrderResultRes, // Utility types for constructor/setup AoUtils, diff --git a/tests.ts b/tests.ts index d0e9809c..7062fe82 100644 --- a/tests.ts +++ b/tests.ts @@ -1,4 +1,4 @@ -import LiquidOps, { DryRunFIFO } from "./src"; +import LiquidOps, { DryRunFIFO, lqdTokenAddress } from "./src"; import { ownerToAddress } from "./tests/testsHelpers/arweaveUtils"; import { createDataItemSigner } from "@permaweb/aoconnect"; import { formatGlobalPosition } from "./src/ao/utils/formatGlobalPosition";