diff --git a/.gitignore b/.gitignore index 9e91684..838e9e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,36 @@ -build -suilend-cli/node_modules +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +**node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..a56567e Binary files /dev/null and b/bun.lockb differ diff --git a/cli/.eslintrc.json b/cli/.eslintrc.json new file mode 100644 index 0000000..d765b28 --- /dev/null +++ b/cli/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "ignorePatterns": ["_generated"], + "extends": [ + "prettier", + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended" + ], + "plugins": ["prettier"], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "prettier/prettier": ["error"], + "import/order": [ + "error", + { + "pathGroups": [], + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type" + ], + "newlines-between": "always", + "alphabetize": { "order": "asc", "caseInsensitive": true } + } + ], + "sort-imports": [ + "error", + { + "ignoreDeclarationSort": true + } + ] + } +} diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..3b45112 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +dist/ diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..c8f9acf --- /dev/null +++ b/cli/package.json @@ -0,0 +1,38 @@ +{ + "name": "@suilend/springsui-cli", + "version": "1.0.0", + "private": true, + "description": "A CLI for interacting with the SpringSui program", + "author": "Suilend", + "license": "MIT", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.js" + }, + "types": "./src/index.ts", + "scripts": { + "build": "rm -rf ./dist && bun tsc", + "eslint": "eslint --fix \"./src/**/*.ts\"", + "prettier": "prettier --write \"./src/**/*\"", + "lint": "bun eslint && bun prettier && bun tsc", + "release": "bun run build && bun ts-node ./prepublish.ts && cd ./dist && npm publish --access public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/solendprotocol/liquid-staking.git" + }, + "bugs": { + "url": "https://github.com/solendprotocol/liquid-staking/issues" + }, + "dependencies": { + "@mysten/bcs": "1.1.0", + "@mysten/sui": "1.12.0", + "commander": "^12.1.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.12.7", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/cli/prepublish.ts b/cli/prepublish.ts new file mode 100644 index 0000000..e8b8aac --- /dev/null +++ b/cli/prepublish.ts @@ -0,0 +1,29 @@ +import fs from "fs"; + +// 1. Update package.json +import packageJson from "./package.json"; +const newPackageJson = Object.assign({}, packageJson); + +newPackageJson["private"] = false; +newPackageJson["main"] = "./index.js"; + +const exportsMap: Record = { + ".": "./index.js", +}; +const files = ( + fs.readdirSync("./dist/", { recursive: true }) as string[] +).filter((file) => file !== "index.js" && file.endsWith(".js")); +for (const file of files) { + const fileName = file.substring( + 0, + file.endsWith("index.js") + ? file.lastIndexOf("/index.js") + : file.lastIndexOf(".js"), + ); + exportsMap[`./${fileName}`] = `./${file}`; +} +newPackageJson["exports"] = exportsMap as any; + +newPackageJson["types"] = "./index.js"; + +fs.writeFileSync("./dist/package.json", JSON.stringify(newPackageJson), "utf8"); diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 0000000..e8ae9ed --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,338 @@ +import { SuiClient } from "@mysten/sui/client"; +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; +import { Transaction } from "@mysten/sui/transactions"; +import { fromBase64 } from "@mysten/sui/utils"; +import { program } from "commander"; + +import * as sdk from "../../sdk/src"; +import { LstClient } from "../../sdk/src"; +import { PACKAGE_ID } from "../../sdk/src/_generated/liquid_staking"; + +const LIQUID_STAKING_INFO = { + id: "0xdae271405d47f04ab6c824d3b362b7375844ec987a2627845af715fdcd835795", + type: "0xba2a31b3b21776d859c9fdfe797f52b069fe8fe0961605ab093ca4eb437d2632::ripleys::RIPLEYS", + weightHookId: + "0xf244912738939d351aa762dd98c075f873fd95f2928db5fd9e74fbb01c9a686c", +}; + +const RPC_URL = "https://fullnode.mainnet.sui.io"; + +const keypair = Ed25519Keypair.fromSecretKey( + fromBase64(process.env.SUI_SECRET_KEY!), +); + +async function mint(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const tx = new Transaction(); + const [sui] = tx.splitCoins(tx.gas, [BigInt(options.amount)]); + const rSui = lstClient.mint(tx, sui); + tx.transferObjects([rSui], keypair.toSuiAddress()); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function redeem(options: any) { + const client = new SuiClient({ url: RPC_URL }); + + const lstCoins = await client.getCoins({ + owner: keypair.toSuiAddress(), + coinType: LIQUID_STAKING_INFO.type, + limit: 1000, + }); + + const tx = new Transaction(); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + if (lstCoins.data.length > 1) { + tx.mergeCoins( + lstCoins.data[0].coinObjectId, + lstCoins.data.slice(1).map((c) => c.coinObjectId), + ); + } + + const [lst] = tx.splitCoins(lstCoins.data[0].coinObjectId, [ + BigInt(options.amount), + ]); + const sui = lstClient.redeemLst(tx, lst); + + tx.transferObjects([sui], keypair.toSuiAddress()); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function increaseValidatorStake(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); + if (!adminCapId) return; + + const tx = new Transaction(); + lstClient.increaseValidatorStake( + tx, + adminCapId, + options.validatorAddress, + options.amount, + ); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function decreaseValidatorStake(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); + if (!adminCapId) return; + + const tx = new Transaction(); + lstClient.decreaseValidatorStake( + tx, + adminCapId, + options.validatorIndex, + options.amount, + ); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function updateFees(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const adminCap = ( + await client.getOwnedObjects({ + owner: keypair.toSuiAddress(), + filter: { + StructType: `${PACKAGE_ID}::liquid_staking::AdminCap<${LIQUID_STAKING_INFO.type}>`, + }, + }) + ).data[0]; + const adminCapId = adminCap.data?.objectId; + if (!adminCapId) return; + + const tx = new Transaction(); + lstClient.updateFees(tx, adminCapId, options); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function initializeWeightHook(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); + if (!adminCapId) return; + + const tx = new Transaction(); + const weightHookAdminCap = lstClient.initializeWeightHook(tx, adminCapId); + tx.transferObjects([weightHookAdminCap], keypair.toSuiAddress()); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function setValidatorAddressesAndWeights(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + if (options.validators.length != options.weights.length) { + throw new Error("Validators and weights arrays must be of the same length"); + } + + const validatorAddressesAndWeights = new Map(); + for (let i = 0; i < options.validators.length; i++) { + validatorAddressesAndWeights.set( + options.validators[i], + options.weights[i] as number, + ); + } + + console.log(validatorAddressesAndWeights); + + const weightHookAdminCapId = await lstClient.getWeightHookAdminCapId( + keypair.toSuiAddress(), + ); + if (!weightHookAdminCapId) return; + + const tx = new Transaction(); + lstClient.setValidatorAddressesAndWeights( + tx, + LIQUID_STAKING_INFO.weightHookId, + weightHookAdminCapId, + validatorAddressesAndWeights, + ); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +async function rebalance(options: any) { + const client = new SuiClient({ url: RPC_URL }); + const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + + const tx = new Transaction(); + lstClient.rebalance(tx, LIQUID_STAKING_INFO.weightHookId); + + const txResponse = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + options: { + showEvents: true, + showEffects: true, + showObjectChanges: true, + }, + }); + + console.log(txResponse); +} + +program.version("1.0.0").description("Spring Sui CLI"); + +program + .command("mint") + .description("mint some rSui") + .option("--amount ", "Amount of SUI in MIST") + .action(mint); + +program + .command("redeem") + .description("redeem some SUI") + .option("--amount ", "Amount of LST to redeem") + .action(redeem); + +program + .command("increase-validator-stake") + .description("increase validator stake") + .option("--validator-address ", "Validator address") + .option("--amount ", "Amount of SUI to delegate to validator") + .action(increaseValidatorStake); + +program + .command("decrease-validator-stake") + .description("decrease validator stake") + .option("--validator-index ", "Validator index") + .option("--amount ", "Amount of SUI to undelegate from validator") + .action(decreaseValidatorStake); + +program + .command("update-fees") + .description("update fees") + .option("--mint-fee-bps ", "Mint fee bps") + .option("--redeem-fee-bps ", "Redeem fee bps") + .option("--spread-fee ", "Spread fee") + .action(updateFees); + +program + .command("fetch-state") + .description("fetch the current state of the liquid staking pool") + .action(async () => { + const client = new SuiClient({ url: RPC_URL }); + try { + const state = await sdk.fetchLiquidStakingInfo( + LIQUID_STAKING_INFO, + client, + ); + console.log("Current Liquid Staking State:"); + console.log(JSON.stringify(state, null, 2)); + } catch (error) { + console.error("Error fetching state:", error); + } + }); + +program + .command("initialize-weight-hook") + .description("initialize weight hook") + .action(initializeWeightHook); + +function collect(pair: any, previous: any) { + const [key, value] = pair.split("="); + if (!value) { + throw new Error(`Invalid format for ${pair}. Use key=value format.`); + } + return { ...previous, [key]: value }; +} + +program + .command("set-validator-addresses-and-weights") + .description("set validator addresses and weights") + .option("-v, --validators ", "Validator addresses") + .option("-w, --weights ", "Weights") + .action(setValidatorAddressesAndWeights); + +program + .command("rebalance") + .description("rebalance the validator set") + .action(rebalance); + +program.parse(process.argv); diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..9c37b42 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "commonjs", + "resolveJsonModule": true, + "declaration": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67c6804 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "8.57.0", + "eslint-config-next": "14.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5" + }, + "private": true, + "workspaces": [ + "cli", + "sdk" + ] +} diff --git a/sdk/bun.lockb b/sdk/bun.lockb deleted file mode 100755 index 00a2372..0000000 Binary files a/sdk/bun.lockb and /dev/null differ diff --git a/sdk/package.json b/sdk/package.json index bb643de..3e45947 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -32,14 +32,6 @@ }, "devDependencies": { "@types/node": "^20.12.7", - "@typescript-eslint/eslint-plugin": "^8.8.1", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "8.57.0", - "eslint-config-next": "14.1.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-prettier": "^5.1.3", - "prettier": "^3.2.5", "ts-node": "^10.9.2", "typescript": "^5.3.3" } diff --git a/sdk/prepublish.ts b/sdk/prepublish.ts new file mode 100644 index 0000000..e8b8aac --- /dev/null +++ b/sdk/prepublish.ts @@ -0,0 +1,29 @@ +import fs from "fs"; + +// 1. Update package.json +import packageJson from "./package.json"; +const newPackageJson = Object.assign({}, packageJson); + +newPackageJson["private"] = false; +newPackageJson["main"] = "./index.js"; + +const exportsMap: Record = { + ".": "./index.js", +}; +const files = ( + fs.readdirSync("./dist/", { recursive: true }) as string[] +).filter((file) => file !== "index.js" && file.endsWith(".js")); +for (const file of files) { + const fileName = file.substring( + 0, + file.endsWith("index.js") + ? file.lastIndexOf("/index.js") + : file.lastIndexOf(".js"), + ); + exportsMap[`./${fileName}`] = `./${file}`; +} +newPackageJson["exports"] = exportsMap as any; + +newPackageJson["types"] = "./index.js"; + +fs.writeFileSync("./dist/package.json", JSON.stringify(newPackageJson), "utf8"); diff --git a/sdk/src/createNewLst.ts b/sdk/src/createNewLst.ts index d9b85ed..3d088ff 100644 --- a/sdk/src/createNewLst.ts +++ b/sdk/src/createNewLst.ts @@ -9,7 +9,8 @@ import { } from "./_generated/liquid_staking/fees/functions"; import * as generated from "./_generated/liquid_staking/liquid-staking/functions"; import { LiquidStakingInfo } from "./_generated/liquid_staking/liquid-staking/structs"; -import { LstClient } from "./functions"; + +import { LstClient } from "./index"; const keypair = Ed25519Keypair.fromSecretKey( fromBase64(process.env.SUI_SECRET_KEY!), diff --git a/sdk/src/functions.ts b/sdk/src/functions.ts deleted file mode 100644 index cae90af..0000000 --- a/sdk/src/functions.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { SuiClient } from "@mysten/sui/client"; -import { Transaction, TransactionObjectInput } from "@mysten/sui/transactions"; - -import { phantom } from "./_generated/_framework/reified"; -import { PACKAGE_ID, setPublishedAt } from "./_generated/liquid_staking"; -import { - newBuilder, - setRedeemFeeBps, - setSpreadFeeBps, - setSuiMintFeeBps, - toFeeConfig, -} from "./_generated/liquid_staking/fees/functions"; -import * as generated from "./_generated/liquid_staking/liquid-staking/functions"; -import { LiquidStakingInfo } from "./_generated/liquid_staking/liquid-staking/structs"; -import * as weightHookGenerated from "./_generated/liquid_staking/weight/functions"; -import { WeightHook } from "./_generated/liquid_staking/weight/structs"; - -export interface LiquidStakingObjectInfo { - id: string; - type: string; -} - -const SUI_SYSTEM_STATE_ID = - "0x0000000000000000000000000000000000000000000000000000000000000005"; -const SUILEND_VALIDATOR_ADDRESS = - "0xce8e537664ba5d1d5a6a857b17bd142097138706281882be6805e17065ecde89"; -const SPRING_SUI_UPGRADE_CAP_ID = - "0x393ea4538463add6f405f2b1e3e6d896e17850975c772135843de26d14cd17c6"; - -async function getLatestPackageId( - client: SuiClient, - upgradeCapId: string, -): Promise { - const object = await client.getObject({ - id: upgradeCapId, - options: { - showContent: true, - }, - }); - - return (object.data?.content as unknown as any).fields.package; -} - -export class LstClient { - liquidStakingObject: LiquidStakingObjectInfo; - client: SuiClient; - - static async initialize( - client: SuiClient, - liquidStakingObjectInfo: LiquidStakingObjectInfo, - ): Promise { - const publishedAt = await getLatestPackageId( - client, - SPRING_SUI_UPGRADE_CAP_ID, - ); - setPublishedAt(publishedAt); - console.log(`Initialized LstClient with package ID: ${publishedAt}`); - - return new LstClient(liquidStakingObjectInfo, client); - } - - constructor(liquidStakingObject: LiquidStakingObjectInfo, client: SuiClient) { - this.liquidStakingObject = liquidStakingObject; - this.client = client; - } - - async getAdminCapId(address: string): Promise { - const res = ( - await this.client.getOwnedObjects({ - owner: address, - filter: { - StructType: `${PACKAGE_ID}::liquid_staking::AdminCap<${this.liquidStakingObject.type}>`, - }, - }) - ).data; - - if (res.length == 0) { - return null; - } - - return res[0].data?.objectId; - } - - async getWeightHookAdminCapId( - address: string, - ): Promise { - const res = ( - await this.client.getOwnedObjects({ - owner: address, - filter: { - StructType: `${PACKAGE_ID}::weight::WeightHookAdminCap<${this.liquidStakingObject.type}>`, - }, - }) - ).data; - - if (res.length == 0) { - return null; - } - - return res[0].data?.objectId; - } - - // returns the lst object - mint(tx: Transaction, suiCoinId: TransactionObjectInput) { - const [rSui] = generated.mint(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - sui: suiCoinId, - systemState: SUI_SYSTEM_STATE_ID, - }); - - return rSui; - } - - // returns the sui coin - redeemLst(tx: Transaction, lstId: TransactionObjectInput) { - const [sui] = generated.redeem(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - systemState: SUI_SYSTEM_STATE_ID, - lst: lstId, - }); - - return sui; - } - - // admin functions - - increaseValidatorStake( - tx: Transaction, - adminCapId: TransactionObjectInput, - validatorAddress: string, - suiAmount: number, - ) { - generated.increaseValidatorStake(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - adminCap: adminCapId, - systemState: SUI_SYSTEM_STATE_ID, - validatorAddress, - suiAmount: BigInt(suiAmount), - }); - } - - decreaseValidatorStake( - tx: Transaction, - adminCapId: TransactionObjectInput, - validatorAddress: string, - maxSuiAmount: number, - ) { - generated.decreaseValidatorStake(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - adminCap: adminCapId, - systemState: SUI_SYSTEM_STATE_ID, - validatorAddress, - maxSuiAmount: BigInt(maxSuiAmount), - }); - } - - collectFees(tx: Transaction, adminCapId: TransactionObjectInput) { - const [sui] = generated.collectFees(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - systemState: SUI_SYSTEM_STATE_ID, - adminCap: adminCapId, - }); - - return sui; - } - - updateFees( - tx: Transaction, - adminCapId: TransactionObjectInput, - feeConfigArgs: FeeConfigArgs, - ) { - let [builder] = newBuilder(tx); - - if (feeConfigArgs.mintFeeBps != null) { - console.log(`Setting mint fee bps to ${feeConfigArgs.mintFeeBps}`); - - builder = setSuiMintFeeBps(tx, { - self: builder, - fee: BigInt(feeConfigArgs.mintFeeBps), - })[0]; - } - - if (feeConfigArgs.redeemFeeBps != null) { - console.log(`Setting redeem fee bps to ${feeConfigArgs.redeemFeeBps}`); - builder = setRedeemFeeBps(tx, { - self: builder, - fee: BigInt(feeConfigArgs.redeemFeeBps), - })[0]; - } - - if (feeConfigArgs.spreadFee != null) { - builder = setSpreadFeeBps(tx, { - self: builder, - fee: BigInt(feeConfigArgs.spreadFee), - })[0]; - } - - const [feeConfig] = toFeeConfig(tx, builder); - - generated.updateFees(tx, this.liquidStakingObject.type, { - self: this.liquidStakingObject.id, - adminCap: adminCapId, - feeConfig, - }); - } - - // weight hook functions - initializeWeightHook(tx: Transaction, adminCapId: TransactionObjectInput) { - const [weightHook, weightHookAdminCap] = weightHookGenerated.new_( - tx, - this.liquidStakingObject.type, - adminCapId, - ); - - tx.moveCall({ - target: `0x2::transfer::public_share_object`, - typeArguments: [ - `${WeightHook.$typeName}<${this.liquidStakingObject.type}>`, - ], - arguments: [weightHook], - }); - - return weightHookAdminCap; - } - - setValidatorAddressesAndWeights( - tx: Transaction, - weightHookId: TransactionObjectInput, - weightHookAdminCap: TransactionObjectInput, - validatorAddressesAndWeights: Map, - ) { - const [vecMap] = tx.moveCall({ - target: `0x2::vec_map::empty`, - typeArguments: ["address", "u64"], - arguments: [], - }); - - for (const [ - validatorAddress, - weight, - ] of validatorAddressesAndWeights.entries()) { - tx.moveCall({ - target: `0x2::vec_map::insert`, - typeArguments: ["address", "u64"], - arguments: [ - vecMap, - tx.pure.address(validatorAddress), - tx.pure.u64(weight), - ], - }); - } - - weightHookGenerated.setValidatorAddressesAndWeights( - tx, - this.liquidStakingObject.type, - { - self: weightHookId, - weightHookAdminCap, - validatorAddressesAndWeights: vecMap, - }, - ); - } - - rebalance(tx: Transaction, weightHookId: TransactionObjectInput) { - weightHookGenerated.rebalance(tx, this.liquidStakingObject.type, { - self: weightHookId, - systemState: SUI_SYSTEM_STATE_ID, - liquidStakingInfo: this.liquidStakingObject.id, - }); - } -} - -// user functions -export async function fetchLiquidStakingInfo( - info: LiquidStakingObjectInfo, - client: SuiClient, -): Promise> { - return LiquidStakingInfo.fetch(client, phantom(info.type), info.id); -} - -interface FeeConfigArgs { - mintFeeBps?: number; - redeemFeeBps?: number; - spreadFee?: number; -} - -// only works for sSui -export async function getSpringSuiApy(client: SuiClient) { - const res = await client.getValidatorsApy(); - const validatorApy = res.apys.find( - (apy) => apy.address == SUILEND_VALIDATOR_ADDRESS, - ); - return validatorApy?.apy; -} diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 316fcf7..cae90af 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,338 +1,294 @@ import { SuiClient } from "@mysten/sui/client"; -import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; -import { Transaction } from "@mysten/sui/transactions"; -import { fromBase64 } from "@mysten/sui/utils"; -import { program } from "commander"; - -import { PACKAGE_ID } from "./_generated/liquid_staking"; -import * as sdk from "./functions"; -import { LstClient } from "./functions"; - -const LIQUID_STAKING_INFO = { - id: "0xdae271405d47f04ab6c824d3b362b7375844ec987a2627845af715fdcd835795", - type: "0xba2a31b3b21776d859c9fdfe797f52b069fe8fe0961605ab093ca4eb437d2632::ripleys::RIPLEYS", - weightHookId: - "0xf244912738939d351aa762dd98c075f873fd95f2928db5fd9e74fbb01c9a686c", -}; - -const RPC_URL = "https://fullnode.mainnet.sui.io"; - -const keypair = Ed25519Keypair.fromSecretKey( - fromBase64(process.env.SUI_SECRET_KEY!), -); - -async function mint(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); - - const tx = new Transaction(); - const [sui] = tx.splitCoins(tx.gas, [BigInt(options.amount)]); - const rSui = lstClient.mint(tx, sui); - tx.transferObjects([rSui], keypair.toSuiAddress()); - - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); - - console.log(txResponse); +import { Transaction, TransactionObjectInput } from "@mysten/sui/transactions"; + +import { phantom } from "./_generated/_framework/reified"; +import { PACKAGE_ID, setPublishedAt } from "./_generated/liquid_staking"; +import { + newBuilder, + setRedeemFeeBps, + setSpreadFeeBps, + setSuiMintFeeBps, + toFeeConfig, +} from "./_generated/liquid_staking/fees/functions"; +import * as generated from "./_generated/liquid_staking/liquid-staking/functions"; +import { LiquidStakingInfo } from "./_generated/liquid_staking/liquid-staking/structs"; +import * as weightHookGenerated from "./_generated/liquid_staking/weight/functions"; +import { WeightHook } from "./_generated/liquid_staking/weight/structs"; + +export interface LiquidStakingObjectInfo { + id: string; + type: string; } -async function redeem(options: any) { - const client = new SuiClient({ url: RPC_URL }); - - const lstCoins = await client.getCoins({ - owner: keypair.toSuiAddress(), - coinType: LIQUID_STAKING_INFO.type, - limit: 1000, - }); - - const tx = new Transaction(); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); - - if (lstCoins.data.length > 1) { - tx.mergeCoins( - lstCoins.data[0].coinObjectId, - lstCoins.data.slice(1).map((c) => c.coinObjectId), - ); - } - - const [lst] = tx.splitCoins(lstCoins.data[0].coinObjectId, [ - BigInt(options.amount), - ]); - const sui = lstClient.redeemLst(tx, lst); - - tx.transferObjects([sui], keypair.toSuiAddress()); - - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, +const SUI_SYSTEM_STATE_ID = + "0x0000000000000000000000000000000000000000000000000000000000000005"; +const SUILEND_VALIDATOR_ADDRESS = + "0xce8e537664ba5d1d5a6a857b17bd142097138706281882be6805e17065ecde89"; +const SPRING_SUI_UPGRADE_CAP_ID = + "0x393ea4538463add6f405f2b1e3e6d896e17850975c772135843de26d14cd17c6"; + +async function getLatestPackageId( + client: SuiClient, + upgradeCapId: string, +): Promise { + const object = await client.getObject({ + id: upgradeCapId, options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, + showContent: true, }, }); - console.log(txResponse); + return (object.data?.content as unknown as any).fields.package; } -async function increaseValidatorStake(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); +export class LstClient { + liquidStakingObject: LiquidStakingObjectInfo; + client: SuiClient; + + static async initialize( + client: SuiClient, + liquidStakingObjectInfo: LiquidStakingObjectInfo, + ): Promise { + const publishedAt = await getLatestPackageId( + client, + SPRING_SUI_UPGRADE_CAP_ID, + ); + setPublishedAt(publishedAt); + console.log(`Initialized LstClient with package ID: ${publishedAt}`); - const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); - if (!adminCapId) return; + return new LstClient(liquidStakingObjectInfo, client); + } - const tx = new Transaction(); - lstClient.increaseValidatorStake( - tx, - adminCapId, - options.validatorAddress, - options.amount, - ); + constructor(liquidStakingObject: LiquidStakingObjectInfo, client: SuiClient) { + this.liquidStakingObject = liquidStakingObject; + this.client = client; + } - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); + async getAdminCapId(address: string): Promise { + const res = ( + await this.client.getOwnedObjects({ + owner: address, + filter: { + StructType: `${PACKAGE_ID}::liquid_staking::AdminCap<${this.liquidStakingObject.type}>`, + }, + }) + ).data; + + if (res.length == 0) { + return null; + } - console.log(txResponse); -} + return res[0].data?.objectId; + } -async function decreaseValidatorStake(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + async getWeightHookAdminCapId( + address: string, + ): Promise { + const res = ( + await this.client.getOwnedObjects({ + owner: address, + filter: { + StructType: `${PACKAGE_ID}::weight::WeightHookAdminCap<${this.liquidStakingObject.type}>`, + }, + }) + ).data; + + if (res.length == 0) { + return null; + } - const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); - if (!adminCapId) return; + return res[0].data?.objectId; + } - const tx = new Transaction(); - lstClient.decreaseValidatorStake( - tx, - adminCapId, - options.validatorIndex, - options.amount, - ); + // returns the lst object + mint(tx: Transaction, suiCoinId: TransactionObjectInput) { + const [rSui] = generated.mint(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + sui: suiCoinId, + systemState: SUI_SYSTEM_STATE_ID, + }); - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); + return rSui; + } - console.log(txResponse); -} + // returns the sui coin + redeemLst(tx: Transaction, lstId: TransactionObjectInput) { + const [sui] = generated.redeem(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + systemState: SUI_SYSTEM_STATE_ID, + lst: lstId, + }); -async function updateFees(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + return sui; + } - const adminCap = ( - await client.getOwnedObjects({ - owner: keypair.toSuiAddress(), - filter: { - StructType: `${PACKAGE_ID}::liquid_staking::AdminCap<${LIQUID_STAKING_INFO.type}>`, - }, - }) - ).data[0]; - const adminCapId = adminCap.data?.objectId; - if (!adminCapId) return; + // admin functions + + increaseValidatorStake( + tx: Transaction, + adminCapId: TransactionObjectInput, + validatorAddress: string, + suiAmount: number, + ) { + generated.increaseValidatorStake(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + adminCap: adminCapId, + systemState: SUI_SYSTEM_STATE_ID, + validatorAddress, + suiAmount: BigInt(suiAmount), + }); + } - const tx = new Transaction(); - lstClient.updateFees(tx, adminCapId, options); + decreaseValidatorStake( + tx: Transaction, + adminCapId: TransactionObjectInput, + validatorAddress: string, + maxSuiAmount: number, + ) { + generated.decreaseValidatorStake(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + adminCap: adminCapId, + systemState: SUI_SYSTEM_STATE_ID, + validatorAddress, + maxSuiAmount: BigInt(maxSuiAmount), + }); + } - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); + collectFees(tx: Transaction, adminCapId: TransactionObjectInput) { + const [sui] = generated.collectFees(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + systemState: SUI_SYSTEM_STATE_ID, + adminCap: adminCapId, + }); - console.log(txResponse); -} + return sui; + } -async function initializeWeightHook(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + updateFees( + tx: Transaction, + adminCapId: TransactionObjectInput, + feeConfigArgs: FeeConfigArgs, + ) { + let [builder] = newBuilder(tx); - const adminCapId = await lstClient.getAdminCapId(keypair.toSuiAddress()); - if (!adminCapId) return; + if (feeConfigArgs.mintFeeBps != null) { + console.log(`Setting mint fee bps to ${feeConfigArgs.mintFeeBps}`); - const tx = new Transaction(); - const weightHookAdminCap = lstClient.initializeWeightHook(tx, adminCapId); - tx.transferObjects([weightHookAdminCap], keypair.toSuiAddress()); + builder = setSuiMintFeeBps(tx, { + self: builder, + fee: BigInt(feeConfigArgs.mintFeeBps), + })[0]; + } - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); + if (feeConfigArgs.redeemFeeBps != null) { + console.log(`Setting redeem fee bps to ${feeConfigArgs.redeemFeeBps}`); + builder = setRedeemFeeBps(tx, { + self: builder, + fee: BigInt(feeConfigArgs.redeemFeeBps), + })[0]; + } - console.log(txResponse); -} + if (feeConfigArgs.spreadFee != null) { + builder = setSpreadFeeBps(tx, { + self: builder, + fee: BigInt(feeConfigArgs.spreadFee), + })[0]; + } -async function setValidatorAddressesAndWeights(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + const [feeConfig] = toFeeConfig(tx, builder); - if (options.validators.length != options.weights.length) { - throw new Error("Validators and weights arrays must be of the same length"); + generated.updateFees(tx, this.liquidStakingObject.type, { + self: this.liquidStakingObject.id, + adminCap: adminCapId, + feeConfig, + }); } - const validatorAddressesAndWeights = new Map(); - for (let i = 0; i < options.validators.length; i++) { - validatorAddressesAndWeights.set( - options.validators[i], - options.weights[i] as number, + // weight hook functions + initializeWeightHook(tx: Transaction, adminCapId: TransactionObjectInput) { + const [weightHook, weightHookAdminCap] = weightHookGenerated.new_( + tx, + this.liquidStakingObject.type, + adminCapId, ); - } - - console.log(validatorAddressesAndWeights); - - const weightHookAdminCapId = await lstClient.getWeightHookAdminCapId( - keypair.toSuiAddress(), - ); - if (!weightHookAdminCapId) return; - - const tx = new Transaction(); - lstClient.setValidatorAddressesAndWeights( - tx, - LIQUID_STAKING_INFO.weightHookId, - weightHookAdminCapId, - validatorAddressesAndWeights, - ); - - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); - - console.log(txResponse); -} -async function rebalance(options: any) { - const client = new SuiClient({ url: RPC_URL }); - const lstClient = await LstClient.initialize(client, LIQUID_STAKING_INFO); + tx.moveCall({ + target: `0x2::transfer::public_share_object`, + typeArguments: [ + `${WeightHook.$typeName}<${this.liquidStakingObject.type}>`, + ], + arguments: [weightHook], + }); - const tx = new Transaction(); - lstClient.rebalance(tx, LIQUID_STAKING_INFO.weightHookId); - - const txResponse = await client.signAndExecuteTransaction({ - transaction: tx, - signer: keypair, - options: { - showEvents: true, - showEffects: true, - showObjectChanges: true, - }, - }); - - console.log(txResponse); -} + return weightHookAdminCap; + } -program.version("1.0.0").description("Spring Sui CLI"); - -program - .command("mint") - .description("mint some rSui") - .option("--amount ", "Amount of SUI in MIST") - .action(mint); - -program - .command("redeem") - .description("redeem some SUI") - .option("--amount ", "Amount of LST to redeem") - .action(redeem); - -program - .command("increase-validator-stake") - .description("increase validator stake") - .option("--validator-address ", "Validator address") - .option("--amount ", "Amount of SUI to delegate to validator") - .action(increaseValidatorStake); - -program - .command("decrease-validator-stake") - .description("decrease validator stake") - .option("--validator-index ", "Validator index") - .option("--amount ", "Amount of SUI to undelegate from validator") - .action(decreaseValidatorStake); - -program - .command("update-fees") - .description("update fees") - .option("--mint-fee-bps ", "Mint fee bps") - .option("--redeem-fee-bps ", "Redeem fee bps") - .option("--spread-fee ", "Spread fee") - .action(updateFees); - -program - .command("fetch-state") - .description("fetch the current state of the liquid staking pool") - .action(async () => { - const client = new SuiClient({ url: RPC_URL }); - try { - const state = await sdk.fetchLiquidStakingInfo( - LIQUID_STAKING_INFO, - client, - ); - console.log("Current Liquid Staking State:"); - console.log(JSON.stringify(state, null, 2)); - } catch (error) { - console.error("Error fetching state:", error); + setValidatorAddressesAndWeights( + tx: Transaction, + weightHookId: TransactionObjectInput, + weightHookAdminCap: TransactionObjectInput, + validatorAddressesAndWeights: Map, + ) { + const [vecMap] = tx.moveCall({ + target: `0x2::vec_map::empty`, + typeArguments: ["address", "u64"], + arguments: [], + }); + + for (const [ + validatorAddress, + weight, + ] of validatorAddressesAndWeights.entries()) { + tx.moveCall({ + target: `0x2::vec_map::insert`, + typeArguments: ["address", "u64"], + arguments: [ + vecMap, + tx.pure.address(validatorAddress), + tx.pure.u64(weight), + ], + }); } - }); -program - .command("initialize-weight-hook") - .description("initialize weight hook") - .action(initializeWeightHook); + weightHookGenerated.setValidatorAddressesAndWeights( + tx, + this.liquidStakingObject.type, + { + self: weightHookId, + weightHookAdminCap, + validatorAddressesAndWeights: vecMap, + }, + ); + } -function collect(pair: any, previous: any) { - const [key, value] = pair.split("="); - if (!value) { - throw new Error(`Invalid format for ${pair}. Use key=value format.`); + rebalance(tx: Transaction, weightHookId: TransactionObjectInput) { + weightHookGenerated.rebalance(tx, this.liquidStakingObject.type, { + self: weightHookId, + systemState: SUI_SYSTEM_STATE_ID, + liquidStakingInfo: this.liquidStakingObject.id, + }); } - return { ...previous, [key]: value }; } -program - .command("set-validator-addresses-and-weights") - .description("set validator addresses and weights") - .option("-v, --validators ", "Validator addresses") - .option("-w, --weights ", "Weights") - .action(setValidatorAddressesAndWeights); +// user functions +export async function fetchLiquidStakingInfo( + info: LiquidStakingObjectInfo, + client: SuiClient, +): Promise> { + return LiquidStakingInfo.fetch(client, phantom(info.type), info.id); +} -program - .command("rebalance") - .description("rebalance the validator set") - .action(rebalance); +interface FeeConfigArgs { + mintFeeBps?: number; + redeemFeeBps?: number; + spreadFee?: number; +} -program.parse(process.argv); +// only works for sSui +export async function getSpringSuiApy(client: SuiClient) { + const res = await client.getValidatorsApy(); + const validatorApy = res.apys.find( + (apy) => apy.address == SUILEND_VALIDATOR_ADDRESS, + ); + return validatorApy?.apy; +}