diff --git a/package.json b/package.json index f3d141a..5c3a750 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "dependencies": { "@dappio-wonderland/gateway-idls": "^0.2.6", - "@dappio-wonderland/navigator": "^0.2.11", + "@dappio-wonderland/navigator": "^0.2.20", "@jup-ag/core": "1.0.0-beta.26", "@project-serum/anchor": "^0.24.2", "@project-serum/borsh": "^0.2.5", diff --git a/src/builder.ts b/src/builder.ts index 366f968..424fba3 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -14,6 +14,7 @@ import { nftFinance, katana, friktion, + lido, } from "@dappio-wonderland/navigator"; import { ActionType, @@ -63,6 +64,7 @@ import { ProtocolFrancium } from "./protocols/francium"; import { ProtocolKatana } from "./protocols/katana"; import { ProtocolTulip } from "./protocols/tulip"; import { ProtocolFriktion } from "./protocols/friktion"; +import { ProtocolLido } from "./protocols/lido"; export class GatewayBuilder { public params: GatewayParams; @@ -109,6 +111,7 @@ export class GatewayBuilder { // Extra Metadata poolDirection: PoolDirection.Obverse, swapMinOutAmount: new anchor.BN(0), + validatorIndex: 0, }; this._metadata = { @@ -1188,6 +1191,19 @@ export class GatewayBuilder { ); break; + case SupportedProtocols.Lido: + this._metadata.vault = await lido.infos.getVault( + this._provider.connection, + depositParams.vaultId + ); + protocol = new ProtocolLido( + this._provider.connection, + this._program, + await this.getGatewayStateKey(), + this.params + ); + + break; default: throw new Error("Unsupported Protocol"); } @@ -1297,6 +1313,24 @@ export class GatewayBuilder { break; + case SupportedProtocols.Lido: + // NOTE: This is a temporary work-around, and will be removed once multiple indexes are supported in `gateway-programs` + const vault = await lido.infos.getVault( + this._provider.connection, + withdrawParams.vaultId + ); + this._metadata.vault = vault; + this.params.validatorIndex = (new lido.VaultInfoWrapper(vault as lido.VaultInfo)).getHeaviestValidatorIndex(); + + protocol = new ProtocolLido( + this._provider.connection, + this._program, + await this.getGatewayStateKey(), + this.params + ); + + break; + default: throw new Error("Unsupported Protocol"); } diff --git a/src/ids.ts b/src/ids.ts index 65343a9..6100f51 100644 --- a/src/ids.ts +++ b/src/ids.ts @@ -2,60 +2,34 @@ import { PublicKey } from "@solana/web3.js"; // Program IDs -export const GATEWAY_PROGRAM_ID = new PublicKey( - "GATEp6AEtXtwHABNWHKH9qeh3uJDZtZJ7YBNYzHsX3FS" -); +export const GATEWAY_PROGRAM_ID = new PublicKey("GATEp6AEtXtwHABNWHKH9qeh3uJDZtZJ7YBNYzHsX3FS"); -export const RAYDIUM_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPT1q4xG8F9m64cQyjqGe11cCXQq6vL4beY5hJavhQ5" -); +export const RAYDIUM_ADAPTER_PROGRAM_ID = new PublicKey("ADPT1q4xG8F9m64cQyjqGe11cCXQq6vL4beY5hJavhQ5"); -export const SABER_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPT4GbWTs9DXxo91YGBjNntYwLpXxn4gEbxfnUPfQoB" -); +export const SABER_ADAPTER_PROGRAM_ID = new PublicKey("ADPT4GbWTs9DXxo91YGBjNntYwLpXxn4gEbxfnUPfQoB"); -export const TULIP_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPT9nhC1asRcEB13FKymLTatqWGCuZHDznGgnakWKxW" -); +export const TULIP_ADAPTER_PROGRAM_ID = new PublicKey("ADPT9nhC1asRcEB13FKymLTatqWGCuZHDznGgnakWKxW"); -export const SOLEND_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTCXAFfJFVqcw73B4PWRZQjMNo7Q3Yj4g7p4zTiZnQ" -); +export const SOLEND_ADAPTER_PROGRAM_ID = new PublicKey("ADPTCXAFfJFVqcw73B4PWRZQjMNo7Q3Yj4g7p4zTiZnQ"); -export const NFT_FINANCE_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTyBr92sBCE1hdYBRvXbMpF4hKs17xyDjFPxopcsrh" -); +export const NFT_FINANCE_ADAPTER_PROGRAM_ID = new PublicKey("ADPTyBr92sBCE1hdYBRvXbMpF4hKs17xyDjFPxopcsrh"); -export const SERUM_PROGRAM_ID = new PublicKey( - "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" -); +export const SERUM_PROGRAM_ID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); export const NATIVE_SOL = new PublicKey("11111111111111111111111111111111"); -export const WSOL = new PublicKey( - "So11111111111111111111111111111111111111112" -); +export const WSOL = new PublicKey("So11111111111111111111111111111111111111112"); -export const LARIX_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTLQQ1Bwgybb2qge7QKSW7woDrhEjcLWG642qP2X4" -); +export const LARIX_ADAPTER_PROGRAM_ID = new PublicKey("ADPTLQQ1Bwgybb2qge7QKSW7woDrhEjcLWG642qP2X4"); -export const LIFINITY_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTF4WmNPebELw6UvnSVBdL7BAqs5ceg9tyrHsQfrJK" -); +export const LIFINITY_ADAPTER_PROGRAM_ID = new PublicKey("ADPTF4WmNPebELw6UvnSVBdL7BAqs5ceg9tyrHsQfrJK"); -export const ORCA_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTTyNqameXftbqsxwXhbs7v7XP8E82YMaUStPgjmU5" -); +export const ORCA_ADAPTER_PROGRAM_ID = new PublicKey("ADPTTyNqameXftbqsxwXhbs7v7XP8E82YMaUStPgjmU5"); -export const KATANA_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTwDKJTizC3V8gZXDxt5uLjJv4pBnh1nTTf9dZJnS2" -); +export const KATANA_ADAPTER_PROGRAM_ID = new PublicKey("ADPTwDKJTizC3V8gZXDxt5uLjJv4pBnh1nTTf9dZJnS2"); -export const FRANCIUM_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTax5HwQ2ZWVLmceCek8UrqMhwCy5q3SHwi8W71Kv2" -); +export const FRANCIUM_ADAPTER_PROGRAM_ID = new PublicKey("ADPTax5HwQ2ZWVLmceCek8UrqMhwCy5q3SHwi8W71Kv2"); -export const FRIKTION_ADAPTER_PROGRAM_ID = new PublicKey( - "ADPTzbsaBdXA3FqXoPHjaTjPfh9kadxxFKxonZihP1Ji" -); +export const FRIKTION_ADAPTER_PROGRAM_ID = new PublicKey("ADPTzbsaBdXA3FqXoPHjaTjPfh9kadxxFKxonZihP1Ji"); + +export const LIDO_ADAPTER_PROGRAM_ID = new PublicKey("ADPTPxbHbEBo9A8E53P2PZnmw3ZYJuwc8ArQQkbJtqhx"); diff --git a/src/protocols/lido.ts b/src/protocols/lido.ts new file mode 100644 index 0000000..972c9c8 --- /dev/null +++ b/src/protocols/lido.ts @@ -0,0 +1,148 @@ +import * as anchor from "@project-serum/anchor"; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token-v2"; +import { DepositParams, GatewayParams, IProtocolVault, PAYLOAD_SIZE, WithdrawParams } from "../types"; +import { Gateway } from "@dappio-wonderland/gateway-idls"; +import { IVaultInfo, lido } from "@dappio-wonderland/navigator"; +import { struct, nu64, u8, u32 } from "buffer-layout"; +import { getActivityIndex, sigHash, createATAWithoutCheckIx, getGatewayAuthority } from "../utils"; +import { LIDO_ADAPTER_PROGRAM_ID } from "../ids"; + +export class ProtocolLido implements IProtocolVault { + constructor( + private _connection: anchor.web3.Connection, + private _gatewayProgram: anchor.Program, + private _gatewayStateKey: anchor.web3.PublicKey, + private _gatewayParams: GatewayParams + ) {} + async deposit( + params: DepositParams, + vault: IVaultInfo, + userKey: anchor.web3.PublicKey + ): Promise<{ txs: anchor.web3.Transaction[]; input: Buffer }> { + // Handle payload input here + const inputLayout = struct([nu64("amount")]); + let payload = Buffer.alloc(PAYLOAD_SIZE); + inputLayout.encode( + { + amount: new anchor.BN(params.depositAmount), + }, + payload + ); + + // Handle transaction here + + let preInstructions = [] as anchor.web3.TransactionInstruction[]; + + const bufferArray = [lido.LIDO_ADDRESS.toBuffer(), Buffer.from("reserve_account")]; + const [reserveAccount] = anchor.web3.PublicKey.findProgramAddressSync(bufferArray, lido.LIDO_PROGRAM_ID); + const bufferArrayMint = [lido.LIDO_ADDRESS.toBuffer(), Buffer.from("mint_authority")]; + const [mintAuthority] = anchor.web3.PublicKey.findProgramAddressSync(bufferArrayMint, lido.LIDO_PROGRAM_ID); + + const recipientStSolAddress = await getAssociatedTokenAddress(vault.shareMint, userKey); + + preInstructions.push(await createATAWithoutCheckIx(userKey, vault.shareMint)); + + const remainingAccounts = [ + { pubkey: lido.LIDO_ADDRESS, isSigner: false, isWritable: true }, // 0 + { pubkey: userKey, isSigner: true, isWritable: true }, // 1 wallet.publicKey + { pubkey: recipientStSolAddress, isSigner: false, isWritable: true }, // 2 + { pubkey: vault.shareMint, isSigner: false, isWritable: true }, // 3 + { pubkey: reserveAccount, isSigner: false, isWritable: true }, // 4 + { pubkey: mintAuthority, isSigner: false, isWritable: false }, // 5 + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // 6 + { pubkey: anchor.web3.SystemProgram.programId, isSigner: false, isWritable: false }, // 7 + ]; + const txDeposit = await this._gatewayProgram.methods + .deposit() + .accounts({ + gatewayState: this._gatewayStateKey, + adapterProgramId: LIDO_ADAPTER_PROGRAM_ID, + baseProgramId: lido.LIDO_PROGRAM_ID, + activityIndex: await getActivityIndex(userKey), + gatewayAuthority: getGatewayAuthority(), + }) + .preInstructions(preInstructions) + .remainingAccounts(remainingAccounts) + .transaction(); + + return { txs: [txDeposit], input: payload }; + } + + async withdraw( + params: WithdrawParams, + vault: IVaultInfo, + userKey: anchor.web3.PublicKey + ): Promise<{ txs: anchor.web3.Transaction[]; input: Buffer }> { + // Handle payload input here + const inputLayout = struct([nu64("amount"), u32("validatorIndex")]); + let payload = Buffer.alloc(PAYLOAD_SIZE); + + const vaultInfo = vault as lido.VaultInfo; + const vaultInfoWrapper = new lido.VaultInfoWrapper(vaultInfo); + + const heaviestValidatorIndex = vaultInfoWrapper.getHeaviestValidatorIndex(); + + const heaviestValidator = vaultInfo.validators!.entries[heaviestValidatorIndex]; + + inputLayout.encode( + { + amount: new anchor.BN(params.withdrawAmount), + // Set validator index. + validatorIndex: heaviestValidatorIndex, + }, + payload + ); + + // Account to temporarily hold stSOL in + const receivingAccount = anchor.web3.Keypair.generate(); + + const senderStSolAddress = await getAssociatedTokenAddress(vault.shareMint, userKey); + + const bufferArrayStake = [lido.LIDO_ADDRESS.toBuffer(), Buffer.from("stake_authority")]; + const [stakeAuthority] = anchor.web3.PublicKey.findProgramAddressSync(bufferArrayStake, lido.LIDO_PROGRAM_ID); + + const validatorStakeSeeds = [ + lido.LIDO_ADDRESS.toBuffer(), + heaviestValidator.pubkey.toBuffer(), + Buffer.from("validator_stake_account"), + Buffer.from(heaviestValidator.entry.stakeSeeds.begin.toArray("le", 8)), + ]; + + const [validatorStakeAccount] = anchor.web3.PublicKey.findProgramAddressSync( + validatorStakeSeeds, + lido.LIDO_PROGRAM_ID + ); + + // Set up accounts + const remainingAccounts = [ + { pubkey: lido.LIDO_ADDRESS, isSigner: false, isWritable: true }, // 1 + { pubkey: userKey, isSigner: true, isWritable: false }, // 2 + { pubkey: senderStSolAddress, isSigner: false, isWritable: true }, // 3 + { pubkey: vault.shareMint, isSigner: false, isWritable: true }, // 4 + { pubkey: heaviestValidator.pubkey, isSigner: false, isWritable: false }, // 5 + { pubkey: validatorStakeAccount, isSigner: false, isWritable: true }, // 6 + { pubkey: receivingAccount.publicKey, isSigner: true, isWritable: true }, // 7 + { pubkey: stakeAuthority, isSigner: false, isWritable: false }, // 8 + { pubkey: vaultInfo.validatorList, isSigner: false, isWritable: true }, // 9 + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // 10 + { pubkey: anchor.web3.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, // 11 + { pubkey: anchor.web3.SystemProgram.programId, isSigner: false, isWritable: false }, // 12 + { pubkey: anchor.web3.StakeProgram.programId, isSigner: false, isWritable: false }, // 13 + ]; + + // Handle transaction here + const txWithdraw = await this._gatewayProgram.methods + .withdraw() + .accounts({ + gatewayState: this._gatewayStateKey, + adapterProgramId: LIDO_ADAPTER_PROGRAM_ID, + baseProgramId: lido.LIDO_PROGRAM_ID, + activityIndex: await getActivityIndex(userKey), + gatewayAuthority: getGatewayAuthority(), + }) + .remainingAccounts(remainingAccounts) + .transaction(); + + return { txs: [txWithdraw], input: payload }; + } +} diff --git a/src/types.ts b/src/types.ts index 06c25ed..d2a0c3b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -219,6 +219,7 @@ export enum SupportedProtocols { Francium = 9, Friktion = 10, Katana = 11, + Lido = 12, } export interface RouteInfoExtend extends RouteInfo { @@ -458,6 +459,10 @@ export type GatewayParams = TypeDef< { name: "poolDirection"; type: "u8"; + }, + { + name: "validatorIndex"; + type: "u32"; } ]; }; diff --git a/tests/testAdapterLido.ts b/tests/testAdapterLido.ts new file mode 100644 index 0000000..c2284f5 --- /dev/null +++ b/tests/testAdapterLido.ts @@ -0,0 +1,110 @@ +import * as anchor from "@project-serum/anchor"; +import { PublicKey, Connection } from "@solana/web3.js"; +import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; +import { GatewayBuilder, SupportedProtocols, DepositParams, WithdrawParams } from "../src"; + +describe("Gateway", () => { + const connection = new Connection("https://rpc-mainnet-fork-1.dappio.xyz", { + commitment: "confirmed", + wsEndpoint: "wss://rpc-mainnet-fork-1.dappio.xyz/ws", + }); + // const connection = new Connection("https://rpc-mainnet-fork.epochs.studio", { + // commitment: "confirmed", + // wsEndpoint: "wss://rpc-mainnet-fork.epochs.studio/ws", + // }); + // const connection = new Connection("https://solana-api.tt-prod.net", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + // const connection = new Connection("https://ssc-dao.genesysgo.net", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + // const connection = new Connection("https:////api.mainnet-beta.solana.com", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + + const options = anchor.AnchorProvider.defaultOptions(); + const wallet = NodeWallet.local(); + const provider = new anchor.AnchorProvider(connection, wallet, options); + + anchor.setProvider(provider); + + const depositAmount = 100; + const withdrawAmount = 100; + + it("Deposit in Lido Vault", async () => { + const vaultId = new PublicKey( + // "6tkFEgE6zry2gGC4yqLrTghdqtqadyT5H3H2AJd4w5rz" // RAY-USDC + "GSAqLGG3AHABTnNSzsorjbqTSbhTmtkFN2dBPxua3RGR" // RAY-SRM + ); + + const depositParams: DepositParams = { + protocol: SupportedProtocols.Lido, + vaultId: vaultId, + depositAmount: depositAmount, + }; + + const gateway = new GatewayBuilder(provider); + await gateway.deposit(depositParams); + + await gateway.finalize(); + + console.log(gateway.params); + + const txs = gateway.transactions(); + + console.log("======"); + console.log("Txs are sent..."); + for (let tx of txs) { + // tx.feePayer = wallet.publicKey; + // tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + // console.log("\n", tx.serializeMessage().toString("base64"), "\n"); + const sig = await provider.sendAndConfirm(tx, [], { + skipPreflight: false, + commitment: "confirmed", + } as unknown as anchor.web3.ConfirmOptions); + console.log(sig); + } + console.log("Txs are executed"); + console.log("======"); + }); + + it("Withdraw from Lido Vault", async () => { + const vaultId = new PublicKey( + // "6tkFEgE6zry2gGC4yqLrTghdqtqadyT5H3H2AJd4w5rz" // RAY-USDC + "GSAqLGG3AHABTnNSzsorjbqTSbhTmtkFN2dBPxua3RGR" // RAY-SRM + ); + + const withdrawParams: WithdrawParams = { + protocol: SupportedProtocols.Lido, + vaultId: vaultId, + withdrawAmount: withdrawAmount, + }; + + const gateway = new GatewayBuilder(provider); + await gateway.withdraw(withdrawParams); + + await gateway.finalize(); + + console.log(gateway.params); + + const txs = gateway.transactions(); + + console.log("======"); + console.log("Txs are sent..."); + for (let tx of txs) { + // tx.feePayer = wallet.publicKey; + // tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + // console.log("\n", tx.serializeMessage().toString("base64"), "\n"); + const sig = await provider.sendAndConfirm(tx, [], { + skipPreflight: false, + commitment: "confirmed", + } as unknown as anchor.web3.ConfirmOptions); + console.log(sig); + } + console.log("Txs are executed"); + console.log("======"); + }); +});