diff --git a/.configs/buffers.json b/.configs/buffers.json new file mode 100644 index 0000000..85c6ddc --- /dev/null +++ b/.configs/buffers.json @@ -0,0 +1,8 @@ +{ + "buffers": [ + "HB7y6727CaMJ7xLAhVRfzsNjHNHiBZCaCCtsr4GUw29C", + "HUenqCZpGbSr352F8ucsHuiq4mDQgweTVEGY5Vja7YnT", + "Ho877bfAR7KUPoqwg1aRLwnzQVta9JQXLpErWe1UEZBL", + "CaRvG8PT2XTr1pCEvqCAcMHXFGgnQYyFJQyrS3QA9jRv" + ] +} \ No newline at end of file diff --git a/.configs/keys.json b/.configs/keys.json new file mode 100644 index 0000000..2181700 --- /dev/null +++ b/.configs/keys.json @@ -0,0 +1,7 @@ +{ + "smartWallet": "BkkBFsRm6VCbZyBG82yuHBnyjJwUJHv1nTJ3GiY44Tkr", + "poolManager": "XD5s9eMuSibXzczBysd8VmG6nVe7DjqMQK1iZMQjANd", + "payerKeyfile": "~/.config/solana/id.json", + "bufferAuthorityKeyfile": "~/.config/solana/id.json", + "executorAuthorityKeyfile": "~/.config/solana/id.json" +} diff --git a/.configs/rpc.json b/.configs/rpc.json new file mode 100644 index 0000000..a40447c --- /dev/null +++ b/.configs/rpc.json @@ -0,0 +1,6 @@ +{ + "localnet": "http://localhost:8899", + "devnet": "https://api.devnet.solana.com", + "testnet": "https://api.testnet.solana.com", + "mainnet-beta": "https://api.mainnet-beta.solana.com" +} diff --git a/.gitignore b/.gitignore index 46455b6..c28d15f 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,5 @@ src/idls/ data/distributor-info.json site/ + +.config/buffers.json diff --git a/package.json b/package.json index 770ac14..4c54100 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "access": "public" }, "devDependencies": { + "@gokiprotocol/client": "^0.10.0", "@project-serum/anchor": "^0.21.0", "@rushstack/eslint-patch": "^1.1.0", "@saberhq/anchor-contrib": "^1.12.44", @@ -39,8 +40,9 @@ "@types/lodash.mapvalues": "^4.6.6", "@types/lodash.zip": "^4.2.6", "@types/mocha": "^9.1.0", - "@types/node": "^16.11.25", + "@types/node": "^17.0.21", "@yarnpkg/doctor": "^3.1.1-rc.2", + "axios": "^0.26.1", "bn.js": "^5.2.0", "chai": "=4.3.4", "eslint": "^8.9.0", @@ -62,6 +64,7 @@ "*.nix": "nixfmt" }, "scripts": { + "init:buffers": "rm -rf .configs/buffers && ts-node ./scripts/init-buffers.ts", "build": "tsc -P tsconfig.build.json && tsc -P tsconfig.esm.json", "typecheck": "tsc", "lint": "eslint . --cache", diff --git a/scripts/bulk-write-set-swap-fee-ixs.ts b/scripts/bulk-write-set-swap-fee-ixs.ts new file mode 100644 index 0000000..4dae779 --- /dev/null +++ b/scripts/bulk-write-set-swap-fee-ixs.ts @@ -0,0 +1,285 @@ +import { GokiSDK } from "@gokiprotocol/client"; +import type { Network, TransactionEnvelope } from "@saberhq/solana-contrib"; +import { + formatNetwork, + SignerWallet, + SolanaProvider, +} from "@saberhq/solana-contrib"; +import type { Fees } from "@saberhq/stableswap-sdk"; +import { Percent } from "@saberhq/token-utils"; +import { Connection, PublicKey } from "@solana/web3.js"; +import * as axios from "axios"; +import { BN } from "bn.js"; +import zip from "lodash.zip"; +import invariant from "tiny-invariant"; + +import type { PoolData } from "../src"; +import { findSaberPool, PoolManagerSDK } from "../src"; +import { PoolWrapper } from "../src/wrappers/pool"; +import { loadBuffers } from "./helpers/loadBuffers"; +import { getRpcUrl, loadKeyConfigs } from "./helpers/loadConfigs"; + +const NEW_ADMIN_FEE = new Percent(new BN(50), new BN(100)); + +interface TokenInfo { + adminFeeAccount: string; + reserve: string; + mint: string; +} + +interface StableSwapState { + /** + * Whether or not the swap is initialized. + */ + isInitialized: boolean; + + /** + * Whether or not the swap is paused. + */ + isPaused: boolean; + + /** + * Nonce used to generate the swap authority. + */ + nonce: number; + + /** + * Mint account for pool token + */ + poolTokenMint: string; + + /** + * Admin account + */ + adminAccount: string; + + tokenA: TokenInfo; + tokenB: TokenInfo; + + /** + * Initial amplification coefficient (A) + */ + initialAmpFactor: string; + + /** + * Target amplification coefficient (A) + */ + targetAmpFactor: string; + + /** + * Ramp A start timestamp + */ + startRampTimestamp: number; + + /** + * Ramp A start timestamp + */ + stopRampTimestamp: number; + + /** + * When the future admin can no longer become the admin, if applicable. + */ + futureAdminDeadline: number; + + /** + * The next admin. + */ + futureAdminAccount: string; + + /** + * Fee schedule + */ + fees: Fees; +} + +interface PoolInfo { + id: string; + name: string; + currency: string; + lpToken: string; + + plotKey: string; + swap: { + config: { + /** + * The public key identifying this instance of the Stable Swap. + */ + swapAccount: string; + /** + * Authority + */ + authority: string; + /** + * Program Identifier for the Swap program + */ + swapProgramID: string; + /** + * Program Identifier for the Token program + */ + tokenProgramID: string; + }; + state: StableSwapState; + }; + newPoolID?: string; + + /** + * Optional info on why the pool is deprecated. + */ + deprecationInfo?: { + /** + * The pool that users should migrate their assets to. + */ + newPoolID?: string; + /** + * Message showing why the pool is deprecated. + */ + message?: string; + /** + * Link to more information. + */ + link?: string; + }; +} + +interface RegistryData { + pools: PoolInfo[]; +} + +const main = async () => { + const network = formatNetwork((process.env.NETWORK as Network) ?? "devnet"); + const connection = new Connection(getRpcUrl()); + const keysCfg = loadKeyConfigs(); + const provider = SolanaProvider.init({ + connection, + wallet: new SignerWallet(keysCfg.payerKP), + }); + const gokiSDK = GokiSDK.load({ provider }); + const pmSDK = PoolManagerSDK.load({ provider }); + const pmW = await pmSDK.loadManager(keysCfg.poolManager); + + const resp = await axios.default.get( + `https://registry.saber.so/data/pools-info.${ + network === "localnet" ? "devnet" : network + }.json` + ); + const registryData = resp.data; + + const poolKeys = await Promise.all( + registryData.pools.map(async (poolInfo) => { + const [poolKey] = await findSaberPool( + keysCfg.poolManager, + new PublicKey(poolInfo.swap.state.tokenA.mint), + new PublicKey(poolInfo.swap.state.tokenB.mint) + ); + return poolKey; + }) + ); + + const poolsData = await pmSDK.provider.connection.getMultipleAccountsInfo( + poolKeys + ); + const poolsDataParsed = poolsData.map((buf) => { + const data = buf?.data; + invariant(data, "data not found"); + console.log({ data }); + return pmSDK.programs.Pools.coder.accounts.decode( + "Pool", + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + data as Buffer + ); + }); + + const poolWrappers = zip(poolKeys, poolsDataParsed).map( + ([key, parsedData]) => { + invariant(key, "pool key not found"); + invariant(pmW.data, "poolManager data"); + invariant(parsedData, "pool data not found"); + + return new PoolWrapper(pmSDK, key, parsedData, pmW.data.admin); + } + ); + + const ixs = await Promise.all( + poolWrappers.map((pw) => { + const swap = pw.data.swap; + const poolInfo = registryData.pools.find( + (poolInfo) => poolInfo.swap.config.swapAccount === swap.toString() + ); + invariant(poolInfo, "poolInfo not found"); + const { + adminTrade: __unused1, + adminWithdraw: __unused2, + ...prevFees + } = poolInfo.swap.state.fees; + + const newFees: Fees = { + adminTrade: NEW_ADMIN_FEE, + adminWithdraw: NEW_ADMIN_FEE, + ...prevFees, + }; + return pw.setNewFees(newFees); + }) + ); + + const buffers = loadBuffers(); + const bundleIndices = new Array(buffers.length).fill(0); + const appendBufferTxs: TransactionEnvelope[] = []; + for (let i = 0; i < 100; i++) { + const ix = ixs[i % ixs.length]?.getInstruction(0); + invariant(ix, "instruction"); + + const bufferIdx = i % buffers.length; + const buffer = buffers[bufferIdx]; + invariant(buffer, "buffer does not exist"); + + const bundleIdx = bundleIndices[bufferIdx]; + invariant(bundleIdx !== undefined, "bundleIdx"); + const tx = gokiSDK.instructionBuffer.appendInstruction( + buffer, + bundleIdx, + ix, + keysCfg.bufferAuthorityKP.publicKey + ); + bundleIndices[bufferIdx] = bundleIdx + 1; + + tx.addSigners(keysCfg.bufferAuthorityKP); + appendBufferTxs.push(tx); + } + + const txs: TransactionEnvelope[] = []; + while (appendBufferTxs) { + const tx1 = appendBufferTxs.shift(); + if (!tx1) { + break; + } + + const tx2 = appendBufferTxs.pop(); + txs.push(tx2 ? tx1.combine(tx2) : tx1); + } + + if (network === "mainnet" && process.env.DRY_RUN === "false") { + await Promise.all( + txs.map(async (tx) => { + const pendingTx = await tx.send(); + const confirmedTx = await pendingTx.wait({ commitment: "finalized" }); + confirmedTx.printLogs(); + console.log("\n"); + }) + ); + + console.log( + `wrote to buffers ... ${buffers.map((b) => b.toString()).join(", ")}` + ); + } +}; + +main() + .then() + .catch((err) => { + if (err) { + console.error(err); + process.exit(1); + } else { + process.exit(0); + } + }); diff --git a/scripts/helpers/loadBuffers.ts b/scripts/helpers/loadBuffers.ts new file mode 100644 index 0000000..60e4f13 --- /dev/null +++ b/scripts/helpers/loadBuffers.ts @@ -0,0 +1,15 @@ +import { PublicKey } from "@solana/web3.js"; + +import * as buffersRaw from "../../.configs/buffers.json"; + +interface BuffersRaw { + buffers: string[]; +} + +export const loadBuffers = (): PublicKey[] => { + const { buffers } = buffersRaw as BuffersRaw; + if (buffers.length === 0) { + throw new Error("No buffer found"); + } + return buffers.map((b) => new PublicKey(b)); +}; diff --git a/scripts/helpers/loadConfigs.ts b/scripts/helpers/loadConfigs.ts new file mode 100644 index 0000000..383f0a5 --- /dev/null +++ b/scripts/helpers/loadConfigs.ts @@ -0,0 +1,50 @@ +import type { Network } from "@saberhq/solana-contrib"; +import type { Keypair } from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; + +import * as keysRaw from "../../.configs/keys.json"; +import * as rpcRaw from "../../.configs/rpc.json"; +import { readKeyfile } from "./readKeyfile"; +interface KeysRaw { + smartWallet: string; + poolManager: string; + payerKeyfile: string; + bufferAuthorityKeyfile: string; + executorAuthorityKeyfile: string; +} + +export interface KeysConfig { + smartWallet: PublicKey; + poolManager: PublicKey; + payerKP: Keypair; + bufferAuthorityKP: Keypair; + executorAuthorityKP: Keypair; +} + +export interface RpcUrls { + localnet: string; + devnet: string; + testnet: string; + ["mainnet-beta"]: string; +} + +export const loadKeyConfigs = (): KeysConfig => { + const keys = keysRaw as KeysRaw; + return { + smartWallet: new PublicKey(keys.smartWallet), + poolManager: new PublicKey(keys.poolManager), + payerKP: readKeyfile(keys.payerKeyfile), + bufferAuthorityKP: readKeyfile(keys.bufferAuthorityKeyfile), + executorAuthorityKP: readKeyfile(keys.executorAuthorityKeyfile), + }; +}; + +export const loadRpcURL = (network: Network): string => { + const rpcUrls = rpcRaw as RpcUrls; + return rpcUrls[network]; +}; + +export const getRpcUrl = (): string => { + const network = (process.env.NETWORK as Network) ?? "devnet"; + return loadRpcURL(network); +}; diff --git a/scripts/helpers/readKeyfile.ts b/scripts/helpers/readKeyfile.ts new file mode 100644 index 0000000..a27510f --- /dev/null +++ b/scripts/helpers/readKeyfile.ts @@ -0,0 +1,15 @@ +import { Keypair } from "@solana/web3.js"; +import fs from "fs"; +import path from "path"; + +export const readKeyfile = (filePath: string): Keypair => { + if (filePath[0] === "~") { + filePath = path.join(process.env.HOME as string, filePath.slice(1)); + } + + return Keypair.fromSecretKey( + Uint8Array.from( + JSON.parse(fs.readFileSync(filePath, { encoding: "utf-8" })) as number[] + ) + ); +}; diff --git a/scripts/init-buffers.ts b/scripts/init-buffers.ts new file mode 100644 index 0000000..2af74b4 --- /dev/null +++ b/scripts/init-buffers.ts @@ -0,0 +1,61 @@ +import { GokiSDK } from "@gokiprotocol/client"; +import { SignerWallet, SolanaProvider } from "@saberhq/solana-contrib"; +import { Connection } from "@solana/web3.js"; +import { BN } from "bn.js"; +import * as fs from "fs/promises"; + +import { getRpcUrl, loadKeyConfigs } from "./helpers/loadConfigs"; + +const NUM_BUFFERS = process.env.NUM_BUFFERS ?? 4; +const BUFFER_SIZE = parseInt(process.env.BUFFER_SIZE ?? (100 * 412).toString()); +export const NUM_BUNDLES = 25; + +const main = async () => { + const connection = new Connection(getRpcUrl()); + + const keysCfg = loadKeyConfigs(); + const provider = SolanaProvider.init({ + connection, + wallet: new SignerWallet(keysCfg.payerKP), + }); + + const gokiSDK = GokiSDK.load({ + provider, + }); + + const buffers = []; + for (let i = 0; i < NUM_BUFFERS; i++) { + const { tx, bufferAccount: bufferAccount } = + await gokiSDK.instructionBuffer.initBuffer({ + bufferSize: BUFFER_SIZE, + smartWallet: keysCfg.smartWallet, + eta: new BN(-1), + numBundles: NUM_BUNDLES, + authority: keysCfg.bufferAuthorityKP.publicKey, + executor: keysCfg.executorAuthorityKP.publicKey, + }); + tx.addSigners(keysCfg.bufferAuthorityKP, keysCfg.executorAuthorityKP); + const pendingTx = await tx.send(); + const confirmedTx = await pendingTx.wait(); + confirmedTx.printLogs(); + + buffers.push(bufferAccount.toString()); + } + + const buffersJSON = JSON.stringify({ buffers }, null, 2); + console.log(buffersJSON); + + const f = `${__dirname}/../.configs/buffers.json`; + await fs.writeFile(f, buffersJSON); +}; + +main() + .then() + .catch((err) => { + if (err) { + console.error(err); + process.exit(1); + } else { + process.exit(0); + } + }); diff --git a/tsconfig.json b/tsconfig.json index 78fe409..4530386 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "@saberhq/tsconfig/tsconfig.lib.json", "compilerOptions": { - "module": "CommonJS" + "module": "CommonJS", + "resolveJsonModule": true, + "types": ["node"] }, - "include": ["./src", "./tests", "scripts/"] + "include": ["./src", "./tests", "./scripts", "./configs"] } diff --git a/yarn.lock b/yarn.lock index 23f0ab6..e490b4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,6 +113,24 @@ __metadata: languageName: node linkType: hard +"@gokiprotocol/client@npm:^0.10.0": + version: 0.10.0 + resolution: "@gokiprotocol/client@npm:0.10.0" + dependencies: + lodash.mapvalues: ^4.6.0 + tiny-invariant: ^1.2.0 + tslib: ^2.3.1 + peerDependencies: + "@project-serum/anchor": ">=0.17" + "@saberhq/anchor-contrib": ^1.12 + "@saberhq/solana-contrib": ^1.12 + "@saberhq/token-utils": ^1.12 + "@solana/web3.js": ^1.29.2 + bn.js: ^5 + checksum: 47e386918457cc674cfcf93b41f0f2f20bf30368abb34c5391aa780a33fcac3bcf70599ae916bc75a46caa3005f00a516492d3843c39e630ce30abbffb6e919c + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.9.2": version: 0.9.2 resolution: "@humanwhocodes/config-array@npm:0.9.2" @@ -297,6 +315,7 @@ __metadata: version: 0.0.0-use.local resolution: "@saberhq/pool-manager@workspace:." dependencies: + "@gokiprotocol/client": ^0.10.0 "@project-serum/anchor": ^0.21.0 "@rushstack/eslint-patch": ^1.1.0 "@saberhq/anchor-contrib": ^1.12.44 @@ -313,8 +332,9 @@ __metadata: "@types/lodash.mapvalues": ^4.6.6 "@types/lodash.zip": ^4.2.6 "@types/mocha": ^9.1.0 - "@types/node": ^16.11.25 + "@types/node": ^17.0.21 "@yarnpkg/doctor": ^3.1.1-rc.2 + axios: ^0.26.1 bn.js: ^5.2.0 chai: =4.3.4 eslint: ^8.9.0 @@ -742,10 +762,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.11.25": - version: 16.11.25 - resolution: "@types/node@npm:16.11.25" - checksum: 0b6e25a81364be89256ad1a36341e27b387e646d3186e270108a8bb7b6ecdfdf5ae037aa1c75a5117b8a7509c80093b75431cd5cfcfbc4d553b52e7db2ca272e +"@types/node@npm:^17.0.21": + version: 17.0.21 + resolution: "@types/node@npm:17.0.21" + checksum: 89dcd2fe82f21d3634266f8384e9c865cf8af49685639fbdbd799bdd1040480fb1e8eeda2d3b9fce41edbe704d2a4be9f427118c4ae872e8d9bb7cbeb3c41a94 languageName: node linkType: hard @@ -1710,6 +1730,15 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.26.1": + version: 0.26.1 + resolution: "axios@npm:0.26.1" + dependencies: + follow-redirects: ^1.14.8 + checksum: d9eb58ff4bc0b36a04783fc9ff760e9245c829a5a1052ee7ca6013410d427036b1d10d04e7380c02f3508c5eaf3485b1ae67bd2adbfec3683704745c8d7a6e1a + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -3040,6 +3069,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.14.8": + version: 1.14.9 + resolution: "follow-redirects@npm:1.14.9" + peerDependenciesMeta: + debug: + optional: true + checksum: f5982e0eb481818642492d3ca35a86989c98af1128b8e1a62911a3410621bc15d2b079e8170b35b19d3bdee770b73ed431a257ed86195af773771145baa57845 + languageName: node + linkType: hard + "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0"