diff --git a/ts/client/src/builder.ts b/ts/client/src/builder.ts new file mode 100644 index 000000000..be3f2ac47 --- /dev/null +++ b/ts/client/src/builder.ts @@ -0,0 +1,95 @@ +// https://github.com/Vincent-Pang/builder-pattern + +export type IBuilder = { + [k in keyof T]-?: ((arg: T[k]) => IBuilder) & (() => T[k]); +} & { + build(): T; +}; + +type Clazz = new (...args: unknown[]) => T; + +/** + * Create a Builder for a class. Returned objects will be of the class type. + * + * e.g. let obj: MyClass = Builder(MyClass).setA(5).setB("str").build(); + * + * @param type the name of the class to instantiate. + * @param template optional class partial which the builder will derive initial params from. + * @param override optional class partial which the builder will override params from when calling build(). + */ +export function Builder( + type: Clazz, + template?: Partial | null, + override?: Partial | null, +): IBuilder; + +/** + * Create a Builder for an interface. Returned objects will be untyped. + * + * e.g. let obj: Interface = Builder().setA(5).setB("str").build(); + * + * @param template optional partial object which the builder will derive initial params from. + * @param override optional partial object which the builder will override params from when calling build(). + */ +export function Builder( + template?: Partial | null, + override?: Partial | null, +): IBuilder; + +export function Builder( + typeOrTemplate?: Clazz | Partial | null, + templateOrOverride?: Partial | null, + override?: Partial | null, +): IBuilder { + let type: Clazz | undefined; + let template: Partial | null | undefined; + let overrideValues: Partial | null | undefined; + + if (typeOrTemplate instanceof Function) { + type = typeOrTemplate; + template = templateOrOverride; + overrideValues = override; + } else { + template = typeOrTemplate; + overrideValues = templateOrOverride; + } + + const built: Record = template + ? Object.assign({}, template) + : {}; + + const builder = new Proxy( + {}, + { + get(target, prop) { + if ('build' === prop) { + if (overrideValues) { + Object.assign(built, overrideValues); + } + + if (type) { + // A class name (identified by the constructor) was passed. Instantiate it with props. + const obj: T = new type(); + return () => + Object.assign(obj as T & Record, { ...built }); + } else { + // No type information - just return the object. + return () => built; + } + } + + return (...args: unknown[]): unknown => { + // If no arguments passed return current value. + if (0 === args.length) { + return built[prop.toString()]; + } + + built[prop.toString()] = args[0]; + return builder; + }; + }, + }, + ); + + return builder as IBuilder; +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 432216186..1f15fcffe 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -45,6 +45,7 @@ import { Serum3Side, generateSerum3MarketExternalVaultSignerAddress, } from './accounts/serum3'; +import { PerpEditParams, TokenEditParams } from './clientIxParamBuilder'; import { OPENBOOK_PROGRAM_ID } from './constants'; import { Id } from './ids'; import { IDL, MangoV4 } from './mango_v4'; @@ -310,64 +311,43 @@ export class MangoClient { public async tokenEdit( group: Group, mintPk: PublicKey, - oracle: PublicKey | null, - oracleConfig: OracleConfigParams | null, - groupInsuranceFund: boolean | null, - interestRateParams: InterestRateParams | null, - loanFeeRate: number | null, - loanOriginationFeeRate: number | null, - maintAssetWeight: number | null, - initAssetWeight: number | null, - maintLiabWeight: number | null, - initLiabWeight: number | null, - liquidationFee: number | null, - stablePriceDelayIntervalSeconds: number | null, - stablePriceDelayGrowthLimit: number | null, - stablePriceGrowthLimit: number | null, - minVaultToDepositsRatio: number | null, - netBorrowLimitPerWindowQuote: number | null, - netBorrowLimitWindowSizeTs: number | null, - borrowWeightScaleStartQuote: number | null, - depositWeightScaleStartQuote: number | null, - resetStablePrice: boolean | null, - resetNetBorrowLimit: boolean | null, - reduceOnly: boolean | null, + params: TokenEditParams, ): Promise { const bank = group.getFirstBankByMint(mintPk); const mintInfo = group.mintInfosMapByTokenIndex.get(bank.tokenIndex)!; return await this.program.methods .tokenEdit( - oracle, - oracleConfig, - groupInsuranceFund, - interestRateParams, - loanFeeRate, - loanOriginationFeeRate, - maintAssetWeight, - initAssetWeight, - maintLiabWeight, - initLiabWeight, - liquidationFee, - stablePriceDelayIntervalSeconds, - stablePriceDelayGrowthLimit, - stablePriceGrowthLimit, - minVaultToDepositsRatio, - netBorrowLimitPerWindowQuote !== null - ? new BN(netBorrowLimitPerWindowQuote) + params.oracle, + params.oracleConfig, + params.groupInsuranceFund, + params.interestRateParams, + params.loanFeeRate, + params.loanOriginationFeeRate, + params.maintAssetWeight, + params.initAssetWeight, + params.maintLiabWeight, + params.initLiabWeight, + params.liquidationFee, + params.stablePriceDelayIntervalSeconds, + params.stablePriceDelayGrowthLimit, + params.stablePriceGrowthLimit, + params.minVaultToDepositsRatio, + params.netBorrowLimitPerWindowQuote !== null + ? new BN(params.netBorrowLimitPerWindowQuote) : null, - netBorrowLimitWindowSizeTs !== null - ? new BN(netBorrowLimitWindowSizeTs) + params.netBorrowLimitWindowSizeTs !== null + ? new BN(params.netBorrowLimitWindowSizeTs) : null, - borrowWeightScaleStartQuote, - depositWeightScaleStartQuote, - resetStablePrice ?? false, - resetNetBorrowLimit ?? false, - reduceOnly, + params.borrowWeightScaleStartQuote, + params.depositWeightScaleStartQuote, + params.resetStablePrice ?? false, + params.resetNetBorrowLimit ?? false, + params.reduceOnly, ) .accounts({ group: group.publicKey, - oracle: oracle ?? bank.oracle, + oracle: params.oracle ?? bank.oracle, admin: (this.program.provider as AnchorProvider).wallet.publicKey, mintInfo: mintInfo.publicKey, }) @@ -1653,67 +1633,43 @@ export class MangoClient { public async perpEditMarket( group: Group, perpMarketIndex: PerpMarketIndex, - oracle: PublicKey | null, // TODO: stable price resetting should be a separate flag - oracleConfig: OracleConfigParams | null, - baseDecimals: number | null, - maintAssetWeight: number | null, - initAssetWeight: number | null, - maintLiabWeight: number | null, - initLiabWeight: number | null, - liquidationFee: number | null, - makerFee: number | null, - takerFee: number | null, - feePenalty: number | null, - minFunding: number | null, - maxFunding: number | null, - impactQuantity: number | null, - groupInsuranceFund: boolean | null, - trustedMarket: boolean | null, - settleFeeFlat: number | null, - settleFeeAmountThreshold: number | null, - settleFeeFractionLowHealth: number | null, - stablePriceDelayIntervalSeconds: number | null, - stablePriceDelayGrowthLimit: number | null, - stablePriceGrowthLimit: number | null, - settlePnlLimitFactor: number | null, - settlePnlLimitWindowSize: number | null, - reduceOnly: boolean | null, + params: PerpEditParams, ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); return await this.program.methods .perpEditMarket( - oracle, - oracleConfig, - baseDecimals, - maintAssetWeight, - initAssetWeight, - maintLiabWeight, - initLiabWeight, - liquidationFee, - makerFee, - takerFee, - minFunding, - maxFunding, - impactQuantity !== null ? new BN(impactQuantity) : null, - groupInsuranceFund, - trustedMarket, - feePenalty, - settleFeeFlat, - settleFeeAmountThreshold, - settleFeeFractionLowHealth, - stablePriceDelayIntervalSeconds, - stablePriceDelayGrowthLimit, - stablePriceGrowthLimit, - settlePnlLimitFactor, - settlePnlLimitWindowSize !== null - ? new BN(settlePnlLimitWindowSize) + params.oracle, + params.oracleConfig, + params.baseDecimals, + params.maintAssetWeight, + params.initAssetWeight, + params.maintLiabWeight, + params.initLiabWeight, + params.liquidationFee, + params.makerFee, + params.takerFee, + params.minFunding, + params.maxFunding, + params.impactQuantity !== null ? new BN(params.impactQuantity) : null, + params.groupInsuranceFund, + params.trustedMarket, + params.feePenalty, + params.settleFeeFlat, + params.settleFeeAmountThreshold, + params.settleFeeFractionLowHealth, + params.stablePriceDelayIntervalSeconds, + params.stablePriceDelayGrowthLimit, + params.stablePriceGrowthLimit, + params.settlePnlLimitFactor, + params.settlePnlLimitWindowSize !== null + ? new BN(params.settlePnlLimitWindowSize) : null, - reduceOnly, + params.reduceOnly, ) .accounts({ group: group.publicKey, - oracle: oracle ?? perpMarket.oracle, + oracle: params.oracle ?? perpMarket.oracle, admin: (this.program.provider as AnchorProvider).wallet.publicKey, perpMarket: perpMarket.publicKey, }) diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts new file mode 100644 index 000000000..883640a6a --- /dev/null +++ b/ts/client/src/clientIxParamBuilder.ts @@ -0,0 +1,108 @@ +import { PublicKey } from '@solana/web3.js'; +import { InterestRateParams, OracleConfigParams } from './types'; + +export interface TokenEditParams { + oracle: PublicKey | null; + oracleConfig: OracleConfigParams | null; + groupInsuranceFund: boolean | null; + interestRateParams: InterestRateParams | null; + loanFeeRate: number | null; + loanOriginationFeeRate: number | null; + maintAssetWeight: number | null; + initAssetWeight: number | null; + maintLiabWeight: number | null; + initLiabWeight: number | null; + liquidationFee: number | null; + stablePriceDelayIntervalSeconds: number | null; + stablePriceDelayGrowthLimit: number | null; + stablePriceGrowthLimit: number | null; + minVaultToDepositsRatio: number | null; + netBorrowLimitPerWindowQuote: number | null; + netBorrowLimitWindowSizeTs: number | null; + borrowWeightScaleStartQuote: number | null; + depositWeightScaleStartQuote: number | null; + resetStablePrice: boolean | null; + resetNetBorrowLimit: boolean | null; + reduceOnly: boolean | null; +} + +export const NullTokenEditParams: TokenEditParams = { + oracle: null, + oracleConfig: null, + groupInsuranceFund: null, + interestRateParams: null, + loanFeeRate: null, + loanOriginationFeeRate: null, + maintAssetWeight: null, + initAssetWeight: null, + maintLiabWeight: null, + initLiabWeight: null, + liquidationFee: null, + stablePriceDelayIntervalSeconds: null, + stablePriceDelayGrowthLimit: null, + stablePriceGrowthLimit: null, + minVaultToDepositsRatio: null, + netBorrowLimitPerWindowQuote: null, + netBorrowLimitWindowSizeTs: null, + borrowWeightScaleStartQuote: null, + depositWeightScaleStartQuote: null, + resetStablePrice: null, + resetNetBorrowLimit: null, + reduceOnly: null, +}; + +export interface PerpEditParams { + oracle: PublicKey | null; + oracleConfig: OracleConfigParams | null; + baseDecimals: number | null; + maintAssetWeight: number | null; + initAssetWeight: number | null; + maintLiabWeight: number | null; + initLiabWeight: number | null; + liquidationFee: number | null; + makerFee: number | null; + takerFee: number | null; + feePenalty: number | null; + minFunding: number | null; + maxFunding: number | null; + impactQuantity: number | null; + groupInsuranceFund: boolean | null; + trustedMarket: boolean | null; + settleFeeFlat: number | null; + settleFeeAmountThreshold: number | null; + settleFeeFractionLowHealth: number | null; + stablePriceDelayIntervalSeconds: number | null; + stablePriceDelayGrowthLimit: number | null; + stablePriceGrowthLimit: number | null; + settlePnlLimitFactor: number | null; + settlePnlLimitWindowSize: number | null; + reduceOnly: boolean | null; +} + +export const NullPerpEditParams: PerpEditParams = { + oracle: null, + oracleConfig: null, + baseDecimals: null, + maintAssetWeight: null, + initAssetWeight: null, + maintLiabWeight: null, + initLiabWeight: null, + liquidationFee: null, + makerFee: null, + takerFee: null, + feePenalty: null, + minFunding: null, + maxFunding: null, + impactQuantity: null, + groupInsuranceFund: null, + trustedMarket: null, + settleFeeFlat: null, + settleFeeAmountThreshold: null, + settleFeeFractionLowHealth: null, + stablePriceDelayIntervalSeconds: null, + stablePriceDelayGrowthLimit: null, + stablePriceGrowthLimit: null, + settlePnlLimitFactor: null, + settlePnlLimitWindowSize: null, + reduceOnly: null, +}; diff --git a/ts/client/src/scripts/mb-admin.ts b/ts/client/src/scripts/mb-admin.ts index f8b5467ca..5c94669e3 100644 --- a/ts/client/src/scripts/mb-admin.ts +++ b/ts/client/src/scripts/mb-admin.ts @@ -22,7 +22,9 @@ import { Serum3SelfTradeBehavior, Serum3Side, } from '../accounts/serum3'; +import { Builder } from '../builder'; import { MangoClient } from '../client'; +import { NullPerpEditParams } from '../clientIxParamBuilder'; import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../constants'; import { buildVersionedTx, toNative } from '../utils'; @@ -481,31 +483,7 @@ async function makePerpMarketReduceOnly() { await client.perpEditMarket( group, perpMarket.perpMarketIndex, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - true, + Builder(NullPerpEditParams).reduceOnly(true).build(), ); } @@ -520,31 +498,7 @@ async function makePerpMarketUntrusted() { await client.perpEditMarket( group, perpMarket.perpMarketIndex, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, + Builder(NullPerpEditParams).trustedMarket(false).build(), ); } diff --git a/ts/client/src/scripts/mb-edit-perp-market.ts b/ts/client/src/scripts/mb-edit-perp-market.ts deleted file mode 100644 index c750acfa0..000000000 --- a/ts/client/src/scripts/mb-edit-perp-market.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; -import { Connection, Keypair } from '@solana/web3.js'; -import * as dotenv from 'dotenv'; -import fs from 'fs'; -import { PerpMarket } from '../accounts/perp'; -import { MangoClient } from '../client'; -import { MANGO_V4_ID } from '../constants'; -dotenv.config(); - -// -// (untested?) script which closes a mango account cleanly, first closes all positions, withdraws all tokens and then closes it -// -async function editPerpMarket(perpMarketName: string) { - const options = AnchorProvider.defaultOptions(); - const connection = new Connection(process.env.MB_CLUSTER_URL!, options); - - // admin - const admin = Keypair.fromSecretKey( - Buffer.from( - JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')), - ), - ); - const adminWallet = new Wallet(admin); - const adminProvider = new AnchorProvider(connection, adminWallet, options); - const client = await MangoClient.connect( - adminProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'], - ); - console.log(`Admin ${admin.publicKey.toBase58()}`); - - // fetch group - const group = await client.getGroupForCreator(admin.publicKey, 2); - console.log(`Found group ${group.publicKey.toBase58()}`); - - const pm: PerpMarket = group.getPerpMarketByName(perpMarketName); - - const signature = await client.perpEditMarket( - group, - pm.perpMarketIndex, - pm.oracle, - { - confFilter: pm.oracleConfig.confFilter.toNumber(), - maxStalenessSlots: null, - }, - pm.baseDecimals, - pm.maintAssetWeight.toNumber(), - pm.initAssetWeight.toNumber(), - pm.maintLiabWeight.toNumber(), - pm.initLiabWeight.toNumber(), - pm.liquidationFee.toNumber(), - pm.makerFee.toNumber(), - pm.takerFee.toNumber(), - pm.feePenalty, - pm.minFunding.toNumber(), - pm.maxFunding.toNumber(), - // pm.impactQuantity.toNumber(), - 1, - pm.groupInsuranceFund, - pm.trustedMarket, - pm.settleFeeFlat, - pm.settleFeeAmountThreshold, - pm.settleFeeFractionLowHealth, - null, - null, - null, - null, - null, - null, - ); - - console.log('Tx Successful:', signature); - - process.exit(); -} - -async function main() { - await editPerpMarket('BTC-PERP'); -} - -main(); diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index ef8d931e3..cf561fbf5 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -9,7 +9,12 @@ import { Serum3SelfTradeBehavior, Serum3Side, } from '../accounts/serum3'; +import { Builder } from '../builder'; import { MangoClient } from '../client'; +import { + NullPerpEditParams, + NullTokenEditParams, +} from '../clientIxParamBuilder'; import { MANGO_V4_ID } from '../constants'; // @@ -102,28 +107,7 @@ async function main() { await client.tokenEdit( group, bank.mint, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - true, - null, - null, + Builder(NullTokenEditParams).resetStablePrice(true).build(), ); } async function setPerpPrice( @@ -135,31 +119,7 @@ async function main() { await client.perpEditMarket( group, perpMarket.perpMarketIndex, - perpMarket.oracle, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, + Builder(NullPerpEditParams).oracle(perpMarket.oracle).build(), ); } @@ -243,28 +203,11 @@ async function main() { await client.tokenEdit( group, buyMint, - group.getFirstBankByMint(buyMint).oracle, - null, - null, - null, - null, - null, - 1.0, - 1.0, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, + Builder(NullTokenEditParams) + .oracle(group.getFirstBankByMint(buyMint).oracle) + .maintAssetWeight(1.0) + .initAssetWeight(1.0) + .build(), ); try { // At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have. @@ -286,28 +229,11 @@ async function main() { await client.tokenEdit( group, buyMint, - group.getFirstBankByMint(buyMint).oracle, - null, - null, - null, - null, - null, - 0.9, - 0.8, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, + Builder(NullTokenEditParams) + .oracle(group.getFirstBankByMint(buyMint).oracle) + .maintAssetWeight(0.9) + .initAssetWeight(0.8) + .build(), ); } }