Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/neptune service #515

Merged
merged 4 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/sdk-ts/src/client/wasm/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './swap/index.js'
export * from './types.js'
export * from './swap/index.js'
export * from './neptune/index.js'
export * from './incentives/index.js'
export * from './nameservice/index.js'
export * from './trading_strategies/index.js'
9 changes: 9 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AssetInfo } from './types.js'

export function getDenom(assetInfo: AssetInfo): string | undefined {
if ('native_token' in assetInfo) {
return assetInfo.native_token.denom
}

return assetInfo.token.contract_addr
}
6 changes: 6 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './types.js'
export * from './service.js'
export * from './transformer.js'
export * from './queries/index.js'

export const NEPTUNE_PRICE_CONTRACT = 'inj1u6cclz0qh5tep9m2qayry9k97dm46pnlqf8nre'
19 changes: 19 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/queries/QueryGetPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BaseWasmQuery } from '../../BaseWasmQuery.js'
import { toBase64 } from '../../../../utils/index.js'
import { AssetInfo } from '../types.js'

export declare namespace QueryGetPrices {
export interface Params {
assets: AssetInfo[]
}
}

export class QueryGetPrices extends BaseWasmQuery<QueryGetPrices.Params> {
toPayload() {
return toBase64({
get_prices: {
assets: this.params.assets,
},
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BaseWasmQuery } from '../../BaseWasmQuery.js';
import { toBase64 } from '../../../../utils/index.js';
import { AssetInfo } from '../types.js';

export declare namespace QueryGetAllLendingRates {
export interface Params {
limit?: number;
startAfter?: AssetInfo;
}
}

export class QueryGetAllLendingRates extends BaseWasmQuery<QueryGetAllLendingRates.Params> {
toPayload() {
const payload = {
get_all_lending_rates: {
...(this.params.limit !== undefined ? { limit: this.params.limit } : {}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets change this to:

...(this.params.limit ? { limit: this.params.limit } : {}),

...(this.params.startAfter ? { start_after: this.params.startAfter } : {}),
},
};

return toBase64(payload);
}
}
2 changes: 2 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { QueryGetPrices } from './QueryGetPrices.js'
export { QueryGetAllLendingRates } from './QueryLendingRates.js'
268 changes: 268 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import {
Network,
isMainnet,
NetworkEndpoints,
getNetworkEndpoints,
} from '@injectivelabs/networks'
import { getDenom } from './helper.js'
import { ChainGrpcWasmApi } from '../../chain/index.js'
import { QueryGetPrices, QueryGetAllLendingRates } from './queries/index.js'
import { NeptuneQueryTransformer } from './transformer.js'
import ExecArgNeptuneDeposit from '../../../core/modules/wasm/exec-args/ExecArgNeptuneDeposit.js'
import ExecArgNeptuneWithdraw from '../../../core/modules/wasm/exec-args/ExecArgNeptuneWithdraw.js'
import MsgExecuteContractCompat from '../../../core/modules/wasm/msgs/MsgExecuteContractCompat.js'
import { GeneralException } from '@injectivelabs/exceptions'
import { NEPTUNE_PRICE_CONTRACT } from './index.js'
Comment on lines +7 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets sort these by length and move import { GeneralException } from '@injectivelabs/exceptions' to the top

import {
AssetInfo,
AssetInfoWithPrice,
NEPTUNE_USDT_CW20_CONTRACT,
} from './types.js'

const NEPTUNE_USDT_MARKET_CONTRACT =
'inj1nc7gjkf2mhp34a6gquhurg8qahnw5kxs5u3s4u'
const NEPTUNE_USDT_INTEREST_CONTRACT =
'inj1ftech0pdjrjawltgejlmpx57cyhsz6frdx2dhq'

export class NeptuneService {
private client: ChainGrpcWasmApi
private priceOracleContract: string

/**
* Constructs a new NeptuneService instan ce.
* @param network The network to use (default: Mainnet).
* @param endpoints Optional custom network endpoints.
*/
constructor(
network: Network = Network.MainnetSentry,
endpoints?: NetworkEndpoints,
) {
if (!isMainnet(network)) {
throw new GeneralException(new Error('Please switch to mainnet network'))
}

const networkEndpoints = endpoints || getNetworkEndpoints(network)
this.client = new ChainGrpcWasmApi(networkEndpoints.grpc)
this.priceOracleContract = NEPTUNE_PRICE_CONTRACT
}

/**
* Fetch prices for given assets from the Neptune Price Oracle contract.
* @param assets Array of AssetInfo objects.
* @returns Array of Price objects.
*/
async fetchPrices(assets: AssetInfo[]): Promise<AssetInfoWithPrice[]> {
const queryGetPricesPayload = new QueryGetPrices({ assets }).toPayload()

try {
const response = await this.client.fetchSmartContractState(
this.priceOracleContract,
queryGetPricesPayload,
)

const prices =
NeptuneQueryTransformer.contractPricesResponseToPrices(response)

return prices
} catch (error) {
console.error('Error fetching prices:', error)
throw new GeneralException(new Error('Failed to fetch prices'))
}
}

/**
* Fetch the redemption ratio based on CW20 and native asset prices.
* @param cw20Asset AssetInfo for the CW20 token.
* @param nativeAsset AssetInfo for the native token.
* @returns Redemption ratio as a number.
*/
async fetchRedemptionRatio({
cw20Asset,
nativeAsset,
}: {
cw20Asset: AssetInfo
nativeAsset: AssetInfo
}): Promise<number> {
const prices = await this.fetchPrices([cw20Asset, nativeAsset])

const [cw20Price] = prices
const [nativePrice] = prices.reverse()
Comment on lines +88 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect extraction of prices due to array mutation

Using prices.reverse() mutates the prices array in place, which can lead to logical errors. This affects the assignment of cw20Price and nativePrice, potentially causing both variables to reference the same price.

Apply this diff to correctly extract the prices without mutating the array:

-    const [cw20Price] = prices
-    const [nativePrice] = prices.reverse()
+    const [cw20Price, nativePrice] = prices
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [cw20Price] = prices
const [nativePrice] = prices.reverse()
const [cw20Price, nativePrice] = prices


if (!cw20Price || !nativePrice) {
throw new GeneralException(
new Error('Failed to compute redemption ratio'),
)
}

return Number(cw20Price.price) / Number(nativePrice.price)
}

/**
* Convert CW20 nUSDT to bank nUSDT using the redemption ratio.
* @param amountCW20 Amount in CW20 nUSDT.
* @param redemptionRatio Redemption ratio.
* @returns Amount in bank nUSDT.
*/
calculateBankAmount(amountCW20: number, redemptionRatio: number): number {
return amountCW20 * redemptionRatio
}
Comment on lines +107 to +108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate redemptionRatio in calculateBankAmount

While multiplying by zero doesn't cause a runtime error, it's prudent to validate that redemptionRatio is a positive number to ensure logical correctness.

Consider adding a check:

    calculateBankAmount(amountCW20: number, redemptionRatio: number): number {
+     if (redemptionRatio <= 0) {
+       throw new Error('Redemption ratio must be a positive number')
+     }
      return amountCW20 * redemptionRatio
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return amountCW20 * redemptionRatio
}
calculateBankAmount(amountCW20: number, redemptionRatio: number): number {
if (redemptionRatio <= 0) {
throw new Error('Redemption ratio must be a positive number')
}
return amountCW20 * redemptionRatio
}


/**
* Convert bank nUSDT to CW20 nUSDT using the redemption ratio.
* @param amountBank Amount in bank nUSDT.
* @param redemptionRatio Redemption ratio.
* @returns Amount in CW20 nUSDT.
*/
calculateCw20Amount(amountBank: number, redemptionRatio: number): number {
return amountBank / redemptionRatio
}
Comment on lines +117 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent division by zero in calculateCw20Amount

Dividing by zero will cause a runtime error. It's essential to check that redemptionRatio is not zero before performing the division.

Add a validation check for redemptionRatio:

    calculateCw20Amount(amountBank: number, redemptionRatio: number): number {
+     if (redemptionRatio === 0) {
+       throw new Error('Redemption ratio cannot be zero')
+     }
      return amountBank / redemptionRatio
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return amountBank / redemptionRatio
}
calculateCw20Amount(amountBank: number, redemptionRatio: number): number {
if (redemptionRatio === 0) {
throw new Error('Redemption ratio cannot be zero')
}
return amountBank / redemptionRatio
}


/**
* Create a deposit message.
* @param sender Sender's Injective address.
* @param contractAddress USDT market contract address.
* @param denom Denomination of the asset.
* @param amount Amount to deposit as a string.
* @returns MsgExecuteContractCompat message.
*/
createDepositMsg({
denom,
amount,
sender,
contractAddress = NEPTUNE_USDT_MARKET_CONTRACT,
}: {
denom: string
amount: string
sender: string
contractAddress?: string
}): MsgExecuteContractCompat {
return MsgExecuteContractCompat.fromJSON({
sender,
contractAddress,
execArgs: ExecArgNeptuneDeposit.fromJSON({}),
funds: {
denom,
amount,
},
})
}
Comment on lines +128 to +148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for message creation methods

Both createDepositMsg and createWithdrawMsg methods accept amounts as strings without validation. Consider:

  1. Validating amount format (numeric string)
  2. Checking for negative values
  3. Validating address format for contract addresses

Example validation:

private validateAmount(amount: string): void {
  if (!/^\d+$/.test(amount)) {
    throw new Error('Amount must be a numeric string')
  }
  if (BigInt(amount) <= 0n) {
    throw new Error('Amount must be positive')
  }
}

Also applies to: 157-176


/**
* Create a withdraw message.
* @param sender Sender's Injective address.
* @param contractAddress nUSDT contract address.
* @param amount Amount to withdraw as a string.
* @returns MsgExecuteContractCompat message.
*/
createWithdrawMsg({
amount,
sender,
cw20ContractAddress = NEPTUNE_USDT_CW20_CONTRACT,
marketContractAddress = NEPTUNE_USDT_MARKET_CONTRACT,
}: {
amount: string
sender: string
cw20ContractAddress?: string
marketContractAddress?: string
}): MsgExecuteContractCompat {
return MsgExecuteContractCompat.fromJSON({
sender,
contractAddress: cw20ContractAddress,
execArgs: ExecArgNeptuneWithdraw.fromJSON({
amount,
contract: marketContractAddress,
}),
})
}

/**
* Fetch lending rates with optional pagination parameters.
* @param limit Maximum number of lending rates to fetch.
* @param startAfter AssetInfo to start after for pagination.
* @returns Array of [AssetInfo, Decimal256] tuples.
*/
async getLendingRates({
limit,
startAfter,
contractAddress = NEPTUNE_USDT_INTEREST_CONTRACT,
}: {
limit?: number
startAfter?: AssetInfo
contractAddress?: string
}): Promise<Array<{ assetInfo: AssetInfo; lendingRate: string }>> {
const query = new QueryGetAllLendingRates({ limit, startAfter })
const payload = query.toPayload()

try {
const response = await this.client.fetchSmartContractState(
contractAddress,
payload,
)

const lendingRates =
NeptuneQueryTransformer.contractLendingRatesResponseToLendingRates(
response,
)

return lendingRates
} catch (error) {
console.error('Error fetching lending rates:', error)
throw new GeneralException(new Error('Failed to fetch lending rates'))
}
}

/**
* Fetch the lending rate for a specific denom by querying the smart contract with pagination.
* @param denom The denomination string of the asset to find the lending rate for.
* @returns Lending rate as a string.
*/
async getLendingRateByDenom({
denom,
contractAddress = NEPTUNE_USDT_INTEREST_CONTRACT,
}: {
denom: string
contractAddress?: string
}): Promise<string | undefined> {
const limit = 10
let startAfter = undefined

while (true) {
const lendingRates = await this.getLendingRates({
limit,
startAfter,
contractAddress,
})

if (lendingRates.length === 0) {
return
}

for (const { assetInfo, lendingRate } of lendingRates) {
const currentDenom = getDenom(assetInfo)

if (currentDenom === denom) {
return lendingRate
}
}

if (lendingRates.length < limit) {
return
}

const lastLendingRate = lendingRates[lendingRates.length - 1]

startAfter = lastLendingRate.assetInfo
}
Comment on lines +229 to +255
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add safeguards against infinite loops in getLendingRateByDenom

The while loop could potentially run indefinitely if the contract consistently returns the same number of results. Consider:

  1. Adding a maximum iteration limit
  2. Implementing a timeout mechanism
+ const MAX_ITERATIONS = 100
+ let iterations = 0
  while (true) {
+   if (iterations++ >= MAX_ITERATIONS) {
+     throw new Error('Maximum iteration limit reached')
+   }
    const lendingRates = await this.getLendingRates({

Committable suggestion skipped: line range outside the PR's diff.

}

/**
* Calculates APY from APR and compounding frequency.
*
* @param apr - The annual percentage rate as a decimal (e.g., 0.10 for 10%)
* @param compoundingFrequency - Number of times interest is compounded per year
* @returns The annual percentage yield as a decimal
*/
calculateAPY(apr: number, compoundingFrequency = 365): number {
return Math.pow(1 + apr / compoundingFrequency, compoundingFrequency) - 1
}
}
27 changes: 27 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { WasmContractQueryResponse } from '../types.js'
import { toUtf8 } from '../../../utils/index.js'
import { AssetInfo, PriceResponse, LendingRateResponse } from './types.js'

export class NeptuneQueryTransformer {
static contractPricesResponseToPrices(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; price: string }> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use [] instead of Array, same for the rest:

{ assetInfo: AssetInfo; price: string }[]

const data = JSON.parse(toUtf8(response.data)) as PriceResponse

return data.map(([assetInfo, priceInfo]) => ({
assetInfo,
price: priceInfo.price,
}))
}
Comment on lines +6 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for JSON parsing and data validation.

The current implementation could throw errors in several scenarios:

  1. Invalid JSON data
  2. Unexpected data structure
  3. Missing or null values

Consider implementing error handling:

 static contractPricesResponseToPrices(
   response: WasmContractQueryResponse,
 ): Array<{ assetInfo: AssetInfo; price: string }> {
-  const data = JSON.parse(toUtf8(response.data)) as PriceResponse
+  try {
+    const data = JSON.parse(toUtf8(response.data)) as PriceResponse
+    if (!Array.isArray(data)) {
+      throw new Error('Invalid price response format')
+    }
 
-  return data.map(([assetInfo, priceInfo]) => ({
-    assetInfo,
-    price: priceInfo.price,
-  }))
+    return data.map(([assetInfo, priceInfo]) => {
+      if (!assetInfo || !priceInfo?.price) {
+        throw new Error('Invalid price data structure')
+      }
+      return {
+        assetInfo,
+        price: priceInfo.price,
+      }
+    })
+  } catch (error) {
+    throw new Error(`Failed to transform price response: ${error.message}`)
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static contractPricesResponseToPrices(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; price: string }> {
const data = JSON.parse(toUtf8(response.data)) as PriceResponse
return data.map(([assetInfo, priceInfo]) => ({
assetInfo,
price: priceInfo.price,
}))
}
static contractPricesResponseToPrices(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; price: string }> {
try {
const data = JSON.parse(toUtf8(response.data)) as PriceResponse
if (!Array.isArray(data)) {
throw new Error('Invalid price response format')
}
return data.map(([assetInfo, priceInfo]) => {
if (!assetInfo || !priceInfo?.price) {
throw new Error('Invalid price data structure')
}
return {
assetInfo,
price: priceInfo.price,
}
})
} catch (error) {
throw new Error(`Failed to transform price response: ${error.message}`)
}
}


static contractLendingRatesResponseToLendingRates(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; lendingRate: string }> {
const data = JSON.parse(toUtf8(response.data)) as LendingRateResponse

return data.map(([assetInfo, lendingRate]) => ({
assetInfo,
lendingRate,
}))
}
Comment on lines +17 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for lending rates transformation.

Similar to the prices method, this implementation needs proper error handling.

Apply similar error handling improvements:

 static contractLendingRatesResponseToLendingRates(
   response: WasmContractQueryResponse,
 ): Array<{ assetInfo: AssetInfo; lendingRate: string }> {
-  const data = JSON.parse(toUtf8(response.data)) as LendingRateResponse
+  try {
+    const data = JSON.parse(toUtf8(response.data)) as LendingRateResponse
+    if (!Array.isArray(data)) {
+      throw new Error('Invalid lending rate response format')
+    }
 
-  return data.map(([assetInfo, lendingRate]) => ({
-    assetInfo,
-    lendingRate,
-  }))
+    return data.map(([assetInfo, lendingRate]) => {
+      if (!assetInfo || !lendingRate) {
+        throw new Error('Invalid lending rate data structure')
+      }
+      return {
+        assetInfo,
+        lendingRate,
+      }
+    })
+  } catch (error) {
+    throw new Error(`Failed to transform lending rate response: ${error.message}`)
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static contractLendingRatesResponseToLendingRates(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; lendingRate: string }> {
const data = JSON.parse(toUtf8(response.data)) as LendingRateResponse
return data.map(([assetInfo, lendingRate]) => ({
assetInfo,
lendingRate,
}))
}
static contractLendingRatesResponseToLendingRates(
response: WasmContractQueryResponse,
): Array<{ assetInfo: AssetInfo; lendingRate: string }> {
try {
const data = JSON.parse(toUtf8(response.data)) as LendingRateResponse
if (!Array.isArray(data)) {
throw new Error('Invalid lending rate response format')
}
return data.map(([assetInfo, lendingRate]) => {
if (!assetInfo || !lendingRate) {
throw new Error('Invalid lending rate data structure')
}
return {
assetInfo,
lendingRate,
}
})
} catch (error) {
throw new Error(`Failed to transform lending rate response: ${error.message}`)
}
}

}
19 changes: 19 additions & 0 deletions packages/sdk-ts/src/client/wasm/neptune/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type AssetInfo =
| {
token: {
contract_addr: string
}
}
| {
native_token: {
denom: string
}
}

export type AssetInfoWithPrice = {assetInfo: AssetInfo, price: string }

export type PriceResponse = Array<[AssetInfo, { price: string }]>
export type LendingRateResponse = Array<[AssetInfo, string]>

export const NEPTUNE_USDT_CW20_CONTRACT =
'inj1cy9hes20vww2yr6crvs75gxy5hpycya2hmjg9s'
Loading