From 683f233b568d6a335ce82dc230352de50e825ae1 Mon Sep 17 00:00:00 2001 From: vasemkin Date: Tue, 30 Jan 2024 20:09:49 +0300 Subject: [PATCH] add utils --- apps/ddca/src/app/app.config.ts | 6 + apps/ddca/src/app/app.service.spec.ts | 17 +- libs/constants/src/index.ts | 1 + libs/constants/src/lib/strategies.ts | 4 + libs/jupiter-client/src/lib/client.ts | 419 +++++++++++++------------- libs/utils/.eslintrc.json | 30 ++ libs/utils/README.md | 11 + libs/utils/jest.config.ts | 11 + libs/utils/package.json | 8 + libs/utils/project.json | 27 ++ libs/utils/src/index.ts | 1 + libs/utils/src/lib/slp.spec.ts | 30 ++ libs/utils/src/lib/spl.ts | 85 ++++++ libs/utils/tsconfig.json | 22 ++ libs/utils/tsconfig.lib.json | 10 + libs/utils/tsconfig.spec.json | 9 + libs/utils/vite.config.ts | 47 +++ package.json | 1 + tsconfig.base.json | 5 +- 19 files changed, 522 insertions(+), 222 deletions(-) create mode 100644 apps/ddca/src/app/app.config.ts create mode 100644 libs/constants/src/lib/strategies.ts create mode 100644 libs/utils/.eslintrc.json create mode 100644 libs/utils/README.md create mode 100644 libs/utils/jest.config.ts create mode 100644 libs/utils/package.json create mode 100644 libs/utils/project.json create mode 100644 libs/utils/src/index.ts create mode 100644 libs/utils/src/lib/slp.spec.ts create mode 100644 libs/utils/src/lib/spl.ts create mode 100644 libs/utils/tsconfig.json create mode 100644 libs/utils/tsconfig.lib.json create mode 100644 libs/utils/tsconfig.spec.json create mode 100644 libs/utils/vite.config.ts diff --git a/apps/ddca/src/app/app.config.ts b/apps/ddca/src/app/app.config.ts new file mode 100644 index 0000000..70e39f7 --- /dev/null +++ b/apps/ddca/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { Strategy } from '@jupjup/constants' + +export type AppConfig = { + tradingStrategy: Strategy + startingFundsUSDC: string +} diff --git a/apps/ddca/src/app/app.service.spec.ts b/apps/ddca/src/app/app.service.spec.ts index fb78a11..a8453c0 100644 --- a/apps/ddca/src/app/app.service.spec.ts +++ b/apps/ddca/src/app/app.service.spec.ts @@ -18,16 +18,13 @@ describe('AppService', () => { describe.only('getData', () => { it('should get data from jupiter client', async () => { - const res = await getQuote({ - inputMint: SOLANA_NATIVE_SOL_ADDRESS, - outputMint: SOLANA_WEN_ADDRESS, - amount: '100000', - }) - - console.log({ res }) - - // expect( - // ).not.toThrow + expect( + await getQuote({ + inputMint: SOLANA_NATIVE_SOL_ADDRESS, + outputMint: SOLANA_WEN_ADDRESS, + amount: '100000', + }), + ).not.toThrow }) }) }) diff --git a/libs/constants/src/index.ts b/libs/constants/src/index.ts index c0b175b..2e84ebf 100644 --- a/libs/constants/src/index.ts +++ b/libs/constants/src/index.ts @@ -1 +1,2 @@ export * from './lib/solana' +export * from './lib/strategies' diff --git a/libs/constants/src/lib/strategies.ts b/libs/constants/src/lib/strategies.ts new file mode 100644 index 0000000..080d271 --- /dev/null +++ b/libs/constants/src/lib/strategies.ts @@ -0,0 +1,4 @@ +export enum Strategy { + PING_PONG, + DDCA, +} diff --git a/libs/jupiter-client/src/lib/client.ts b/libs/jupiter-client/src/lib/client.ts index 912d799..bbeef74 100644 --- a/libs/jupiter-client/src/lib/client.ts +++ b/libs/jupiter-client/src/lib/client.ts @@ -16,341 +16,340 @@ The rate limit is 150 requests / 60 seconds. If you need a higher rate limit, fe * OpenAPI spec version: 6.0.0 */ -import { customInstance } from './axios'; -import type { BodyType } from './axios'; +import { customInstance } from './axios' +import type { BodyType } from './axios' export type GetIndexedRouteMapParams = { -/** - * Default is false. Direct Routes limits Jupiter routing to single hop routes only. - */ -onlyDirectRoutes?: OnlyDirectRoutesParameterParameter; -}; + /** + * Default is false. Direct Routes limits Jupiter routing to single hop routes only. + */ + onlyDirectRoutes?: OnlyDirectRoutesParameterParameter +} -export type GetProgramIdToLabel200 = {[key: string]: string}; +export type GetProgramIdToLabel200 = { [key: string]: string } /** * If you want to charge the user a fee, you can specify the fee in BPS. Fee % is taken out of the output token. */ -export type PlatformFeeBpsParameterParameter = number; +export type PlatformFeeBpsParameterParameter = number /** * Rough estimate of the max accounts to be used for the quote, so that you can compose with your own accounts */ -export type MaxAccountsParameterParameter = number; +export type MaxAccountsParameterParameter = number /** * Default is false. Instead of using versioned transaction, this will use the legacy transaction. */ -export type AsLegacyTransactionParameterParameter = boolean; +export type AsLegacyTransactionParameterParameter = boolean /** * Default is false. Direct Routes limits Jupiter routing to single hop routes only. */ -export type OnlyDirectRoutesParameterParameter = boolean; +export type OnlyDirectRoutesParameterParameter = boolean /** * Default is that all DEXes are included. You can pass in the DEXes that you want to exclude and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). */ -export type ExcludeDexesParameterParameter = string[]; +export type ExcludeDexesParameterParameter = string[] /** * Default is that all DEXes are included. You can pass in the DEXes that you want to include only and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). */ -export type DexesParameterParameter = string[]; - -export type SwapModeParameterParameter = typeof SwapModeParameterParameter[keyof typeof SwapModeParameterParameter]; +export type DexesParameterParameter = string[] +export type SwapModeParameterParameter = + (typeof SwapModeParameterParameter)[keyof typeof SwapModeParameterParameter] // eslint-disable-next-line @typescript-eslint/no-redeclare export const SwapModeParameterParameter = { - ExactIn: 'ExactIn', - ExactOut: 'ExactOut', -} as const; + ExactIn: 'ExactIn', + ExactOut: 'ExactOut', +} as const /** * The slippage % in BPS. If the output token amount exceeds the slippage then the swap transaction will fail. */ -export type SlippageParameterParameter = number; +export type SlippageParameterParameter = number /** * The amount to swap, have to factor in the token decimals. */ -export type AmountParameterParameter = string; +export type AmountParameterParameter = string /** * Output token mint address */ -export type OutputMintParameterParameter = string; +export type OutputMintParameterParameter = string /** * Input token mint address */ -export type InputMintParameterParameter = string; +export type InputMintParameterParameter = string export type GetQuoteParams = { -/** - * Input token mint address - */ -inputMint: InputMintParameterParameter; -/** - * Output token mint address - */ -outputMint: OutputMintParameterParameter; -/** - * The amount to swap, have to factor in the token decimals. - */ -amount: AmountParameterParameter; -/** - * The slippage % in BPS. If the output token amount exceeds the slippage then the swap transaction will fail. - */ -slippageBps?: SlippageParameterParameter; -/** - * (ExactIn or ExactOut) Defaults to ExactIn. ExactOut is for supporting use cases where you need an exact token amount, like payments. In this case the slippage is on the input token. - */ -swapMode?: SwapModeParameterParameter; -/** - * Default is that all DEXes are included. You can pass in the DEXes that you want to include only and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). - */ -dexes?: DexesParameterParameter; -/** - * Default is that all DEXes are included. You can pass in the DEXes that you want to exclude and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). - */ -excludeDexes?: ExcludeDexesParameterParameter; -/** - * Default is false. Direct Routes limits Jupiter routing to single hop routes only. - */ -onlyDirectRoutes?: OnlyDirectRoutesParameterParameter; -/** - * Default is false. Instead of using versioned transaction, this will use the legacy transaction. - */ -asLegacyTransaction?: AsLegacyTransactionParameterParameter; -/** - * If you want to charge the user a fee, you can specify the fee in BPS. Fee % is taken out of the output token. - */ -platformFeeBps?: PlatformFeeBpsParameterParameter; -/** - * Rough estimate of the max accounts to be used for the quote, so that you can compose with your own accounts - */ -maxAccounts?: MaxAccountsParameterParameter; -}; + /** + * Input token mint address + */ + inputMint: InputMintParameterParameter + /** + * Output token mint address + */ + outputMint: OutputMintParameterParameter + /** + * The amount to swap, have to factor in the token decimals. + */ + amount: AmountParameterParameter + /** + * The slippage % in BPS. If the output token amount exceeds the slippage then the swap transaction will fail. + */ + slippageBps?: SlippageParameterParameter + /** + * (ExactIn or ExactOut) Defaults to ExactIn. ExactOut is for supporting use cases where you need an exact token amount, like payments. In this case the slippage is on the input token. + */ + swapMode?: SwapModeParameterParameter + /** + * Default is that all DEXes are included. You can pass in the DEXes that you want to include only and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). + */ + dexes?: DexesParameterParameter + /** + * Default is that all DEXes are included. You can pass in the DEXes that you want to exclude and separate them by `,`. You can check out the full list [here](https://quote-api.jup.ag/v6/program-id-to-label). + */ + excludeDexes?: ExcludeDexesParameterParameter + /** + * Default is false. Direct Routes limits Jupiter routing to single hop routes only. + */ + onlyDirectRoutes?: OnlyDirectRoutesParameterParameter + /** + * Default is false. Instead of using versioned transaction, this will use the legacy transaction. + */ + asLegacyTransaction?: AsLegacyTransactionParameterParameter + /** + * If you want to charge the user a fee, you can specify the fee in BPS. Fee % is taken out of the output token. + */ + platformFeeBps?: PlatformFeeBpsParameterParameter + /** + * Rough estimate of the max accounts to be used for the quote, so that you can compose with your own accounts + */ + maxAccounts?: MaxAccountsParameterParameter +} /** * All the possible route and their corresponding output mints */ -export type IndexedRouteMapResponseIndexedRouteMap = {[key: string]: number[]}; +export type IndexedRouteMapResponseIndexedRouteMap = { [key: string]: number[] } export interface IndexedRouteMapResponse { - /** All the possible route and their corresponding output mints */ - indexedRouteMap: IndexedRouteMapResponseIndexedRouteMap; - /** All the mints that are indexed to match in indexedRouteMap */ - mintKeys: string[]; + /** All the possible route and their corresponding output mints */ + indexedRouteMap: IndexedRouteMapResponseIndexedRouteMap + /** All the mints that are indexed to match in indexedRouteMap */ + mintKeys: string[] } export interface SwapInstructionsResponse { - /** The lookup table addresses that you can use if you are using versioned transaction. */ - addressLookupTableAddresses: string[]; - /** Unwrap the SOL if `wrapAndUnwrapSol = true`. */ - cleanupInstruction?: Instruction; - /** The necessary instructions to setup the compute budget. */ - computeBudgetInstructions: Instruction[]; - /** Setup missing ATA for the users. */ - setupInstructions: Instruction[]; - /** The actual swap instruction. */ - swapInstruction: Instruction; - /** If you are using `useTokenLedger = true`. */ - tokenLedgerInstruction?: Instruction; + /** The lookup table addresses that you can use if you are using versioned transaction. */ + addressLookupTableAddresses: string[] + /** Unwrap the SOL if `wrapAndUnwrapSol = true`. */ + cleanupInstruction?: Instruction + /** The necessary instructions to setup the compute budget. */ + computeBudgetInstructions: Instruction[] + /** Setup missing ATA for the users. */ + setupInstructions: Instruction[] + /** The actual swap instruction. */ + swapInstruction: Instruction + /** If you are using `useTokenLedger = true`. */ + tokenLedgerInstruction?: Instruction } export interface SwapResponse { - lastValidBlockHeight: number; - prioritizationFeeLamports?: number; - swapTransaction: string; + lastValidBlockHeight: number + prioritizationFeeLamports?: number + swapTransaction: string } /** * Prioritization fee lamports paid for the transaction in addition to the signatures fee. Mutually exclusive with compute_unit_price_micro_lamports. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ -export type SwapRequestPrioritizationFeeLamports = number | string; +export type SwapRequestPrioritizationFeeLamports = number | string /** * The compute unit price to prioritize the transaction, the additional fee will be `computeUnitLimit (1400000) * computeUnitPriceMicroLamports`. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ -export type SwapRequestComputeUnitPriceMicroLamports = number | string; +export type SwapRequestComputeUnitPriceMicroLamports = number | string export interface SwapRequest { - /** Default is false. Request a legacy transaction rather than the default versioned transaction, needs to be paired with a quote using asLegacyTransaction otherwise the transaction might be too large. */ - asLegacyTransaction?: boolean; - /** The compute unit price to prioritize the transaction, the additional fee will be `computeUnitLimit (1400000) * computeUnitPriceMicroLamports`. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ - computeUnitPriceMicroLamports?: SwapRequestComputeUnitPriceMicroLamports; - /** Public key of the token account that will be used to receive the token out of the swap. If not provided, the user's ATA will be used. If provided, we assume that the token account is already initialized. */ - destinationTokenAccount?: string; - /** When enabled, it will do a swap simulation to get the compute unit used and set it in ComputeBudget's compute unit limit. This will increase latency slightly since there will be one extra RPC call to simulate this. Default is `false`. */ - dynamicComputeUnitLimit?: boolean; - /** Fee token account for the output token, it is derived using the seeds = ["referral_ata", referral_account, mint] and the `REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3` referral contract (only pass in if you set a `platformFeeBps` in `/quote` and make sure that the feeAccount has been created). */ - feeAccount?: string; - /** Prioritization fee lamports paid for the transaction in addition to the signatures fee. Mutually exclusive with compute_unit_price_micro_lamports. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ - prioritizationFeeLamports?: SwapRequestPrioritizationFeeLamports; - quoteResponse: QuoteResponse; - /** Restrict intermediate tokens to a top token set that has stable liquidity. This will help to ease potential high slippage error rate when swapping with minimal impact on pricing. */ - restrictIntermediateTokens?: boolean; - /** When enabled, it will not do any rpc calls check on user's accounts. Enable it only when you already setup all the accounts needed for the trasaction, like wrapping or unwrapping sol, destination account is already created. */ - skipUserAccountsRpcCalls?: boolean; - /** The user public key. */ - userPublicKey: string; - /** Default is true. This enables the usage of shared program accountns. That means no intermediate token accounts or open orders accounts need to be created for the users. But it also means that the likelihood of hot accounts is higher. */ - useSharedAccounts?: boolean; - /** Default is false. This is useful when the instruction before the swap has a transfer that increases the input token amount. Then, the swap will just use the difference between the token ledger token amount and post token amount. */ - useTokenLedger?: boolean; - /** Default is true. If true, will automatically wrap/unwrap SOL. If false, it will use wSOL token account. Will be ignored if `destinationTokenAccount` is set because the `destinationTokenAccount` may belong to a different user that we have no authority to close. */ - wrapAndUnwrapSol?: boolean; + /** Default is false. Request a legacy transaction rather than the default versioned transaction, needs to be paired with a quote using asLegacyTransaction otherwise the transaction might be too large. */ + asLegacyTransaction?: boolean + /** The compute unit price to prioritize the transaction, the additional fee will be `computeUnitLimit (1400000) * computeUnitPriceMicroLamports`. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ + computeUnitPriceMicroLamports?: SwapRequestComputeUnitPriceMicroLamports + /** Public key of the token account that will be used to receive the token out of the swap. If not provided, the user's ATA will be used. If provided, we assume that the token account is already initialized. */ + destinationTokenAccount?: string + /** When enabled, it will do a swap simulation to get the compute unit used and set it in ComputeBudget's compute unit limit. This will increase latency slightly since there will be one extra RPC call to simulate this. Default is `false`. */ + dynamicComputeUnitLimit?: boolean + /** Fee token account for the output token, it is derived using the seeds = ["referral_ata", referral_account, mint] and the `REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3` referral contract (only pass in if you set a `platformFeeBps` in `/quote` and make sure that the feeAccount has been created). */ + feeAccount?: string + /** Prioritization fee lamports paid for the transaction in addition to the signatures fee. Mutually exclusive with compute_unit_price_micro_lamports. If `auto` is used, Jupiter will automatically set a priority fee and it will be capped at 5,000,000 lamports / 0.005 SOL. */ + prioritizationFeeLamports?: SwapRequestPrioritizationFeeLamports + quoteResponse: QuoteResponse + /** Restrict intermediate tokens to a top token set that has stable liquidity. This will help to ease potential high slippage error rate when swapping with minimal impact on pricing. */ + restrictIntermediateTokens?: boolean + /** When enabled, it will not do any rpc calls check on user's accounts. Enable it only when you already setup all the accounts needed for the trasaction, like wrapping or unwrapping sol, destination account is already created. */ + skipUserAccountsRpcCalls?: boolean + /** The user public key. */ + userPublicKey: string + /** Default is true. This enables the usage of shared program accountns. That means no intermediate token accounts or open orders accounts need to be created for the users. But it also means that the likelihood of hot accounts is higher. */ + useSharedAccounts?: boolean + /** Default is false. This is useful when the instruction before the swap has a transfer that increases the input token amount. Then, the swap will just use the difference between the token ledger token amount and post token amount. */ + useTokenLedger?: boolean + /** Default is true. If true, will automatically wrap/unwrap SOL. If false, it will use wSOL token account. Will be ignored if `destinationTokenAccount` is set because the `destinationTokenAccount` may belong to a different user that we have no authority to close. */ + wrapAndUnwrapSol?: boolean } export interface SwapInfo { - ammKey: string; - feeAmount: string; - feeMint: string; - inAmount: string; - inputMint: string; - label?: string; - outAmount: string; - outputMint: string; + ammKey: string + feeAmount: string + feeMint: string + inAmount: string + inputMint: string + label?: string + outAmount: string + outputMint: string } export interface RoutePlanStep { - percent: number; - swapInfo: SwapInfo; + percent: number + swapInfo: SwapInfo } export interface PlatformFee { - amount?: string; - feeBps?: number; + amount?: string + feeBps?: number } -export type SwapMode = typeof SwapMode[keyof typeof SwapMode]; - +export type SwapMode = (typeof SwapMode)[keyof typeof SwapMode] // eslint-disable-next-line @typescript-eslint/no-redeclare export const SwapMode = { - ExactIn: 'ExactIn', - ExactOut: 'ExactOut', -} as const; + ExactIn: 'ExactIn', + ExactOut: 'ExactOut', +} as const export interface QuoteResponse { - contextSlot?: number; - inAmount: string; - inputMint: string; - otherAmountThreshold: string; - outAmount: string; - outputMint: string; - platformFee?: PlatformFee; - priceImpactPct: string; - routePlan: RoutePlanStep[]; - slippageBps: number; - swapMode: SwapMode; - timeTaken?: number; + contextSlot?: number + inAmount: string + inputMint: string + otherAmountThreshold: string + outAmount: string + outputMint: string + platformFee?: PlatformFee + priceImpactPct: string + routePlan: RoutePlanStep[] + slippageBps: number + swapMode: SwapMode + timeTaken?: number } export interface AccountMeta { - isSigner: boolean; - isWritable: boolean; - pubkey: string; + isSigner: boolean + isWritable: boolean + pubkey: string } export interface Instruction { - accounts: AccountMeta[]; - data: string; - programId: string; + accounts: AccountMeta[] + data: string + programId: string } - - - // eslint-disable-next-line - type SecondParameter any> = T extends ( - config: any, - args: infer P, +type SecondParameter any> = T extends ( + config: any, + args: infer P, ) => any - ? P - : never; + ? P + : never - - /** +/** * Sends a GET request to the Jupiter API to get the best priced quote. * @summary GET /quote */ export const getQuote = ( - params: GetQuoteParams, - options?: SecondParameter,) => { - return customInstance( - {url: `/quote`, method: 'GET', - params - }, - options); - } - + params: GetQuoteParams, + options?: SecondParameter, +) => { + return customInstance({ url: `/quote`, method: 'GET', params }, options) +} + /** * Returns a transaction that you can use from the quote you get from `/quote`. * @summary POST /swap */ export const postSwap = ( - swapRequest: BodyType, - options?: SecondParameter,) => { - return customInstance( - {url: `/swap`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: swapRequest - }, - options); - } - + swapRequest: BodyType, + options?: SecondParameter, +) => { + return customInstance( + { + url: `/swap`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: swapRequest, + }, + options, + ) +} + /** * Returns instructions that you can use from the quote you get from `/quote`. * @summary POST /swap-instructions */ export const postSwapInstructions = ( - swapRequest: BodyType, - options?: SecondParameter,) => { - return customInstance( - {url: `/swap-instructions`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: swapRequest - }, - options); - } - + swapRequest: BodyType, + options?: SecondParameter, +) => { + return customInstance( + { + url: `/swap-instructions`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: swapRequest, + }, + options, + ) +} + /** * Returns a hash, which key is the program id and value is the label. This is used to help map error from transaction by identifying the fault program id. With that, we can use the `excludeDexes` or `dexes` parameter. * @summary GET /program-id-to-label */ -export const getProgramIdToLabel = ( - - options?: SecondParameter,) => { - return customInstance( - {url: `/program-id-to-label`, method: 'GET' - }, - options); - } - +export const getProgramIdToLabel = (options?: SecondParameter) => { + return customInstance( + { url: `/program-id-to-label`, method: 'GET' }, + options, + ) +} + /** * Returns a hash map, input mint as key and an array of valid output mint as values, token mints are indexed to reduce the file size * @summary GET /indexed-route-map */ export const getIndexedRouteMap = ( - params?: GetIndexedRouteMapParams, - options?: SecondParameter,) => { - return customInstance( - {url: `/indexed-route-map`, method: 'GET', - params - }, - options); - } - + params?: GetIndexedRouteMapParams, + options?: SecondParameter, +) => { + return customInstance( + { url: `/indexed-route-map`, method: 'GET', params }, + options, + ) +} -type AwaitedInput = PromiseLike | T; +type AwaitedInput = PromiseLike | T - type Awaited = O extends AwaitedInput ? T : never; +type Awaited = O extends AwaitedInput ? T : never export type GetQuoteResult = NonNullable>> export type PostSwapResult = NonNullable>> -export type PostSwapInstructionsResult = NonNullable>> +export type PostSwapInstructionsResult = NonNullable< + Awaited> +> export type GetProgramIdToLabelResult = NonNullable>> export type GetIndexedRouteMapResult = NonNullable>> diff --git a/libs/utils/.eslintrc.json b/libs/utils/.eslintrc.json new file mode 100644 index 0000000..b795fab --- /dev/null +++ b/libs/utils/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"] + } + ] + } + } + ] +} diff --git a/libs/utils/README.md b/libs/utils/README.md new file mode 100644 index 0000000..ad4ba3d --- /dev/null +++ b/libs/utils/README.md @@ -0,0 +1,11 @@ +# utils + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build utils` to build the library. + +## Running unit tests + +Run `nx test utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/utils/jest.config.ts b/libs/utils/jest.config.ts new file mode 100644 index 0000000..166adf6 --- /dev/null +++ b/libs/utils/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'utils', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/utils', +} diff --git a/libs/utils/package.json b/libs/utils/package.json new file mode 100644 index 0000000..f66e003 --- /dev/null +++ b/libs/utils/package.json @@ -0,0 +1,8 @@ +{ + "name": "@jupjup/utils", + "version": "0.0.1", + "dependencies": {}, + "main": "./index.js", + "module": "./index.mjs", + "typings": "./index.d.ts" +} diff --git a/libs/utils/project.json b/libs/utils/project.json new file mode 100644 index 0000000..3cd8b7c --- /dev/null +++ b/libs/utils/project.json @@ -0,0 +1,27 @@ +{ + "name": "utils", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/utils/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/utils" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/utils/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts new file mode 100644 index 0000000..842d335 --- /dev/null +++ b/libs/utils/src/index.ts @@ -0,0 +1 @@ +export * from './lib/utils' diff --git a/libs/utils/src/lib/slp.spec.ts b/libs/utils/src/lib/slp.spec.ts new file mode 100644 index 0000000..18442fd --- /dev/null +++ b/libs/utils/src/lib/slp.spec.ts @@ -0,0 +1,30 @@ +import { parseUSDC } from './spl' + +describe('parseUSDC', () => { + it('should correctly parse a whole number amount', () => { + expect(parseUSDC(10)).toBe('10000000') + }) + + it('should correctly parse a decimal amount', () => { + expect(parseUSDC(10.5)).toBe('10500000') + }) + + it('should correctly handle zero', () => { + expect(parseUSDC(0)).toBe('0') + }) + + it('should handle very large numbers', () => { + expect(parseUSDC(123456789.123456)).toBe('123456789123456') + }) + + it('should throw an error for negative numbers', () => { + expect(() => parseUSDC(-1)).toThrow('Invalid USDC amount') + }) + + it('should throw an error for non-numeric inputs', () => { + // @ts-ignore + expect(() => parseUSDC('abc')).toThrow('Invalid USDC amount') + // @ts-ignore + expect(() => parseUSDC(undefined)).toThrow('Invalid USDC amount') + }) +}) diff --git a/libs/utils/src/lib/spl.ts b/libs/utils/src/lib/spl.ts new file mode 100644 index 0000000..59d8896 --- /dev/null +++ b/libs/utils/src/lib/spl.ts @@ -0,0 +1,85 @@ +import { SOLANA_USDC_ADDRESS } from '@jupjup/constants' +import { getQuote, QuoteResponse } from '@jupjup/jupiter-client' + +/** + * Calculates the amount of SPL Token that can be received for a given amount of USDC. + * + * This function calls the `getQuote` function from a DEX module to fetch the current + * exchange rate between USDC and the specified SPL Token. It then calculates and returns + * the amount of SPL Token that would be received for the specified amount of USDC. + * + * @param {number} usdcAmount - The amount of USDC to be traded. This should be a positive number. + * @param {string} usdcMint - The mint address for USDC. This is used to specify the type of USDC being traded. + * @param {string} splTokenMint - The mint address for the SPL Token to be received. This identifies which SPL Token is being exchanged for. + * @returns {Promise} A promise that resolves to the amount of SPL Token that can be received. + * + * @example + * // Example usage + * calculateSPLTokenAmount(100, 'USDC_Mint_Address', 'SPL_Token_Mint_Address') + * .then(amount => console.log(`You will receive ${amount} SPL Tokens`)) + * .catch(error => console.error(error)); + * + * @throws {Error} Throws an error if the `getQuote` function call fails or if the quote response is invalid. + * + * Note: This function assumes that the `getQuote` function and `QuoteResponse` interface are correctly implemented + * and available in the scope where this function is used. The function also assumes that the DEX API returns the + * 'outAmount' as a string which needs to be parsed into a number. + */ +export const calculateSPLTokenAmount = async ( + usdcAmount: string, + splTokenMint: string, +): Promise => { + // Call getQuote with USDC amount and mint addresses + let quoteResponse: QuoteResponse + try { + quoteResponse = await getQuote({ + inputMint: SOLANA_USDC_ADDRESS, + outputMint: splTokenMint, + amount: usdcAmount, + }) + } catch (error) { + console.error('Error getting quote: ', error) + throw new Error('Unable to get quote') + } + + // Check if the response is valid + if (!quoteResponse || !quoteResponse.outAmount) { + throw new Error('Invalid quote response') + } + + // Convert outAmount to number (assuming it's a string in the response) + const splTokenAmount = parseFloat(quoteResponse.outAmount) + + // You can add additional checks or logic here if needed + + return splTokenAmount +} + +/** + * Parses a decimal USDC amount to its string representation in the smallest unit. + * + * USDC, like many tokens on Solana, uses 6 decimal places. This function converts + * a decimal USDC value into the equivalent amount in the smallest unit, represented + * as a string. For example, an input of 10.5 will be converted to '10500000'. + * + * @param {number} usdcAmount - The decimal USDC amount. + * @returns {string} The string representation of the USDC amount in the smallest unit. + * + * @example + * // Example usage + * const amount = parseUSDC(10.5); // '10500000' + * + * @throws {Error} Throws an error if the input is not a valid number. + */ +export const parseUSDC = (usdcAmount: number): string => { + if (typeof usdcAmount === 'undefined' || isNaN(usdcAmount) || usdcAmount < 0) { + throw new Error('Invalid USDC amount') + } + + const decimals = 6 // USDC has 6 decimal places + const smallestUnitMultiplier = 10 ** decimals + const smallestUnitAmount = usdcAmount * smallestUnitMultiplier + + // Use BigInt to avoid precision issues with very large numbers + return BigInt(smallestUnitAmount).toString() +} diff --git a/libs/utils/tsconfig.json b/libs/utils/tsconfig.json new file mode 100644 index 0000000..3cee374 --- /dev/null +++ b/libs/utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/utils/tsconfig.lib.json b/libs/utils/tsconfig.lib.json new file mode 100644 index 0000000..fd48dec --- /dev/null +++ b/libs/utils/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/utils/tsconfig.spec.json b/libs/utils/tsconfig.spec.json new file mode 100644 index 0000000..ceb45ea --- /dev/null +++ b/libs/utils/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/utils/vite.config.ts b/libs/utils/vite.config.ts new file mode 100644 index 0000000..e28c59a --- /dev/null +++ b/libs/utils/vite.config.ts @@ -0,0 +1,47 @@ +/// +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import * as path from 'path' +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin' + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/libs/utils', + + plugins: [ + nxViteTsPaths(), + dts({ + entryRoot: 'src', + tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), + skipDiagnostics: true, + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + outDir: '../../dist/libs/utils', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'utils', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: [], + }, + }, +}) diff --git a/package.json b/package.json index 289928e..35603c7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "prepare": "pnpm nx run jupiter-client:generate", "test": "pnpm nx run-many -t test", "test:app": "pnpm nx test ddca --verbose", + "test:utils": "pnpm nx test utils --verbose", "test:jup-client": "pnpm nx test jupiter-client --verbose" }, "private": true, diff --git a/tsconfig.base.json b/tsconfig.base.json index c9c1be6..c74eaa5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,7 +8,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, - "target": "es2015", + "target": "ESNext", "module": "esnext", "lib": ["es2020", "dom"], "skipLibCheck": true, @@ -16,7 +16,8 @@ "baseUrl": ".", "paths": { "@jupjup/constants": ["libs/constants/src/index.ts"], - "@jupjup/jupiter-client": ["libs/jupiter-client/src/index.ts"] + "@jupjup/jupiter-client": ["libs/jupiter-client/src/index.ts"], + "@jupjup/utils": ["libs/utils/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]