From 4c9f9c0af97e4e887603841220f203d75db907ab Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Mon, 27 May 2024 21:50:36 -0500 Subject: [PATCH] api: add jupiter swap --- anchor/Anchor.toml | 13 +- .../programs/glam/src/instructions/jupiter.rs | 8 +- anchor/src/client.ts | 7 +- anchor/src/client/base.ts | 17 +- anchor/src/client/jupiter.ts | 242 +++++++++--------- anchor/src/clientConfig.ts | 1 + anchor/target/idl/glam.json | 12 +- anchor/target/types/glam.ts | 24 +- anchor/tests/glam_jupiter.spec.ts | 12 +- anchor/tests/glam_wsol.spec.ts | 30 ++- anchor/tests/setup.ts | 35 +-- api/src/main.ts | 27 +- api/src/tx.ts | 36 ++- 13 files changed, 262 insertions(+), 202 deletions(-) diff --git a/anchor/Anchor.toml b/anchor/Anchor.toml index 19f5406c..e73968df 100644 --- a/anchor/Anchor.toml +++ b/anchor/Anchor.toml @@ -6,18 +6,10 @@ seeds = false skip-lint = false [programs.devnet] -custody = "Gcu1vbed9bwpfwU9PCnJw8QanQfVHfETWxAK3EZczbgo" glam = "Gco1pcjxCMYjKJjSNJ7mKV7qezeUTE7arXJgy7PAPNRc" -policy = "Gpo1jXtEFepqyPQWTG7oDJrg8rye8JL3zcsczHzXKqLt" -pricing = "Gpr1WZZXAty2L9eiMwZPC7ra69vMFojhdqDuiRHkQQQp" -strategy = "Gst1YGe5vKURSWfCqM7GhZys1oML9E9t91WRyV2rt4yS" [programs.localnet] -custody = "Gcu1vbed9bwpfwU9PCnJw8QanQfVHfETWxAK3EZczbgo" glam = "Gco1pcjxCMYjKJjSNJ7mKV7qezeUTE7arXJgy7PAPNRc" -policy = "Gpo1jXtEFepqyPQWTG7oDJrg8rye8JL3zcsczHzXKqLt" -pricing = "Gpr1WZZXAty2L9eiMwZPC7ra69vMFojhdqDuiRHkQQQp" -strategy = "Gst1YGe5vKURSWfCqM7GhZys1oML9E9t91WRyV2rt4yS" [registry] url = "https://api.apr.dev" @@ -25,14 +17,15 @@ url = "https://api.apr.dev" [provider] cluster = "localnet" #cluster = "devnet" +#cluster = "mainnet" wallet = "~/.config/solana/id.json" [scripts] -test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" +#test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_investor" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_drift" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" -#test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_jupiter" +test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_jupiter" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_openfunds" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_wsol" #test = "../node_modules/.bin/nx run --skip-nx-cache anchor:jest --verbose --testPathPattern tests/ --testNamePattern devnet" diff --git a/anchor/programs/glam/src/instructions/jupiter.rs b/anchor/programs/glam/src/instructions/jupiter.rs index c25000a6..0a4a58c0 100644 --- a/anchor/programs/glam/src/instructions/jupiter.rs +++ b/anchor/programs/glam/src/instructions/jupiter.rs @@ -19,14 +19,14 @@ impl anchor_lang::Id for Jupiter { #[derive(Accounts)] pub struct JupiterSwap<'info> { - #[account(mut)] - pub manager: Signer<'info>, + #[account(has_one = manager, has_one = treasury)] + pub fund: Box>, #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, - #[account(has_one = manager, has_one = treasury)] - pub fund: Box>, + #[account(mut)] + pub manager: Signer<'info>, pub jupiter_program: Program<'info, Jupiter>, pub system_program: Program<'info, System>, diff --git a/anchor/src/client.ts b/anchor/src/client.ts index 2b676c69..e0e01a47 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -1,12 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; import { GlamClientConfig } from "./clientConfig"; -import { BaseClient } from "./client/base"; +import { + BaseClient, + JUPITER_API_DEFAULT as _JUPITER_API_DEFAULT +} from "./client/base"; import { DriftClient } from "./client/drift"; import { JupiterClient } from "./client/jupiter"; import { MarinadeClient } from "./client/marinade"; import { WSolClient } from "./client/wsol"; +export const JUPITER_API_DEFAULT = _JUPITER_API_DEFAULT; + export class GlamClient extends BaseClient { drift: DriftClient; jupiter: JupiterClient; diff --git a/anchor/src/client/base.ts b/anchor/src/client/base.ts index 928dbc07..7bf8ec3f 100644 --- a/anchor/src/client/base.ts +++ b/anchor/src/client/base.ts @@ -1,9 +1,15 @@ import * as anchor from "@coral-xyz/anchor"; -import { Program, IdlAccounts } from "@coral-xyz/anchor"; +import { + AnchorProvider, + IdlAccounts, + Program, + Wallet +} from "@coral-xyz/anchor"; import { ComputeBudgetProgram, Connection, PublicKey, + Signer, TransactionSignature } from "@solana/web3.js"; import { @@ -18,10 +24,13 @@ import { FundModel, FundOpenfundsModel } from "../models"; type FundAccount = IdlAccounts["fundAccount"]; type FundMetadataAccount = IdlAccounts["fundMetadataAccount"]; +export const JUPITER_API_DEFAULT = "https://quote-api.jup.ag/v6"; + export class BaseClient { provider: anchor.Provider; program: GlamProgram; programId: PublicKey; + jupiterApi: string; public constructor(config?: GlamClientConfig) { this.programId = getGlamProgramId(config?.cluster || "devnet"); @@ -48,12 +57,18 @@ export class BaseClient { anchor.setProvider(this.provider); this.program = anchor.workspace.Glam as GlamProgram; } + + this.jupiterApi = config?.jupiterApi || JUPITER_API_DEFAULT; } getManager(): PublicKey { return this.provider?.publicKey || new PublicKey(0); } + getWalletSigner(): Signer { + return ((this.provider as AnchorProvider).wallet as Wallet).payer; + } + getFundModel(fund: any): FundModel { return new FundModel(fund) as FundModel; } diff --git a/anchor/src/client/jupiter.ts b/anchor/src/client/jupiter.ts index ff3b455e..f54e3cfb 100644 --- a/anchor/src/client/jupiter.ts +++ b/anchor/src/client/jupiter.ts @@ -1,10 +1,11 @@ import * as anchor from "@coral-xyz/anchor"; -import { BN } from "@coral-xyz/anchor"; import { + AddressLookupTableAccount, PublicKey, - Transaction, TransactionInstruction, - TransactionSignature + TransactionSignature, + TransactionMessage, + VersionedTransaction } from "@solana/web3.js"; import { BaseClient } from "./base"; @@ -12,7 +13,6 @@ import { BaseClient } from "./base"; const jupiterProgram = new PublicKey( "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" ); -const JUPITER_API = "https://quote-api.jup.ag/v6"; export class JupiterClient { public constructor(readonly base: BaseClient) {} @@ -21,113 +21,127 @@ export class JupiterClient { * Client methods */ - getQuote = async ( - fromMint: PublicKey, - toMint: PublicKey, - amount: number, - slippage: number, - onlyDirectRoutes: boolean - ) => { - const reponse = await fetch( - `${JUPITER_API}/quote?outputMint=${toMint.toBase58()}&inputMint=${fromMint.toBase58()}&amount=${amount}&slippage=${slippage}&onlyDirectRoutes=${onlyDirectRoutes}` - ); - return await reponse.json(); - }; - - getSwapIx = async ( - fromAccount: PublicKey, - toAccount: PublicKey, - quote: any - ): Promise => { - const data = { - quoteResponse: quote, - userPublicKey: fromAccount.toBase58(), - destinationTokenAccount: toAccount.toBase58() - }; - const response = await fetch(`${JUPITER_API}/swap-instructions`, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify(data) - }); - - const resp = await response.json(); - console.log("swap-instructions response:", resp); - return resp; - }; - public async swap( fund: PublicKey, - fromMint: PublicKey, - toMint: PublicKey, - amount: BN, - slippage: number = 0.5, - onlyDirectRoutes: boolean = true + quote?: any, + quoteResponse?: any, + swapInstruction?: any, + addressLookupTableAddresses?: any ): Promise { - const fromAccount = this.base.getTreasuryAta(fund, fromMint); - const toAccount = this.base.getTreasuryAta(fund, toMint); - - const payload = await this.jupiterPayload( - fromMint, - toMint, - fromAccount, - toAccount, - amount, - slippage, - onlyDirectRoutes + const tx = await this.swapTxBuilder( + fund, + this.base.getManager(), + quote, + quoteResponse, + swapInstruction, + addressLookupTableAddresses ); - - return this.swapTxBuilder(fund, this.base.getManager(), payload.data).rpc(); + tx.sign([this.base.getWalletSigner()]); + return await this.base.provider.connection.sendTransaction(tx); } /* * Tx Builders */ - swapTxBuilder(fund: PublicKey, manager: PublicKey, data: Buffer): any { - return this.base.program.methods.jupiterSwap(data).accounts({ - fund, - manager, - treasury: this.base.getTreasuryPDA(fund), - jupiterProgram - }); + async swapTxBuilder( + fund: PublicKey, + manager: PublicKey, + quote?: any, + quoteResponse?: any, + swapInstruction?: any, + addressLookupTableAddresses?: string[] + ): Promise /* MethodsBuilder */ { + if (swapInstruction === undefined) { + if (quoteResponse === undefined) { + /* Fetch quoteResponse is not specified */ + console.log("Fetching quoteResponse..."); + quoteResponse = await this.getQuoteResponse(quote); + } + /* Fetch swapInstruction is not specified */ + console.log("Fetching swapInstruction..."); + [swapInstruction, addressLookupTableAddresses] = + await this.getSwapInstruction({ + userPublicKey: manager, + quoteResponse + }); + } + // console.log("swapInstruction", swapInstruction); + /* Create the tx for jupiterSwap using the swapInstruction */ + const tx = await this.base.program.methods + .jupiterSwap(swapInstruction.data) + .accounts({ + fund, + manager, + treasury: this.base.getTreasuryPDA(fund), + jupiterProgram + }) + .remainingAccounts(swapInstruction.keys) + .transaction(); + + const connection = this.base.provider.connection; + const lookupTableAccounts = ( + await Promise.all( + (addressLookupTableAddresses || []).map( + async (address) => + ( + await connection.getAddressLookupTable(new PublicKey(address)) + ).value + ) + ) + ).filter((x) => !!x) as AddressLookupTableAccount[]; + + const messageV0 = new TransactionMessage({ + payerKey: manager, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: tx.instructions + }).compileToV0Message(lookupTableAccounts); + + return new VersionedTransaction(messageV0); } - async jupiterPayload( - fromMint: PublicKey, - toMint: PublicKey, - fromAccount: PublicKey, - toAccount: PublicKey, - amount: BN, - slippage: number, - onlyDirectRoutes: boolean - ): Promise { - const quote = await this.getQuote( - fromMint, - toMint, - amount, - slippage, - onlyDirectRoutes - ); - console.log("quote response:", quote); - - const { swapInstruction } = await this.getSwapIx( - fromAccount, - toAccount, - quote + public async getQuoteResponse(quote: any): Promise { + const res = await fetch( + `${this.base.jupiterApi}/quote?` + + new URLSearchParams(Object.entries(quote)) ); + const quoteResponse = await res.json(); + // console.log("quoteResponse", quoteResponse); + return quoteResponse; + } - return new TransactionInstruction({ - programId: new PublicKey(swapInstruction.programId), - keys: swapInstruction.accounts.map((key: any) => ({ - pubkey: new PublicKey(key.pubkey), - isSigner: key.isSigner, - isWritable: key.isWritable - })), - data: Buffer.from(swapInstruction.data, "base64") + public async getSwapInstructions(quoteResponse: any): Promise { + const res = await fetch(`${this.base.jupiterApi}/swap-instructions`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(quoteResponse) }); + + const instructions = await res.json(); + return instructions; + } + + async getSwapInstruction( + quoteResponse: any + ): Promise<[TransactionInstruction, string[]]> { + const { swapInstruction, addressLookupTableAddresses } = + await this.getSwapInstructions(quoteResponse); + + return [ + new TransactionInstruction({ + programId: new PublicKey(swapInstruction.programId), + keys: swapInstruction.accounts.map((key: any) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable + })), + data: Buffer.from(swapInstruction.data, "base64") + }), + addressLookupTableAddresses + ]; } /* @@ -136,29 +150,19 @@ export class JupiterClient { public async swapTx( fund: PublicKey, - fromMint: PublicKey, - toMint: PublicKey, - amount: BN, - slippage: number = 0.5, - onlyDirectRoutes: boolean = true - ): Promise { - const fromAccount = this.base.getTreasuryAta(fund, fromMint); - const toAccount = this.base.getTreasuryAta(fund, toMint); - - const payload = await this.jupiterPayload( - fromMint, - toMint, - fromAccount, - toAccount, - amount, - slippage, - onlyDirectRoutes - ); - - return this.swapTxBuilder( + manager: PublicKey, + quote?: any, + quoteResponse?: any, + swapInstruction?: any, + addressLookupTableAddresses?: any + ): Promise { + return await this.swapTxBuilder( fund, - this.base.getManager(), - payload.data - ).transaction(); + manager, + quote, + quoteResponse, + swapInstruction, + addressLookupTableAddresses + ); } } diff --git a/anchor/src/clientConfig.ts b/anchor/src/clientConfig.ts index 33aa930c..131cb5be 100644 --- a/anchor/src/clientConfig.ts +++ b/anchor/src/clientConfig.ts @@ -6,4 +6,5 @@ export type ClusterOrCustom = Cluster | "custom"; export type GlamClientConfig = { provider?: Provider; cluster?: ClusterOrCustom; + jupiterApi?: string; }; diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index 18553562..7cd882e1 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -825,9 +825,9 @@ "name": "jupiterSwap", "accounts": [ { - "name": "manager", - "isMut": true, - "isSigner": true + "name": "fund", + "isMut": false, + "isSigner": false }, { "name": "treasury", @@ -835,9 +835,9 @@ "isSigner": false }, { - "name": "fund", - "isMut": false, - "isSigner": false + "name": "manager", + "isMut": true, + "isSigner": true }, { "name": "jupiterProgram", diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index eeac6907..837eb22e 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -825,9 +825,9 @@ export type Glam = { "name": "jupiterSwap", "accounts": [ { - "name": "manager", - "isMut": true, - "isSigner": true + "name": "fund", + "isMut": false, + "isSigner": false }, { "name": "treasury", @@ -835,9 +835,9 @@ export type Glam = { "isSigner": false }, { - "name": "fund", - "isMut": false, - "isSigner": false + "name": "manager", + "isMut": true, + "isSigner": true }, { "name": "jupiterProgram", @@ -3861,9 +3861,9 @@ export const IDL: Glam = { "name": "jupiterSwap", "accounts": [ { - "name": "manager", - "isMut": true, - "isSigner": true + "name": "fund", + "isMut": false, + "isSigner": false }, { "name": "treasury", @@ -3871,9 +3871,9 @@ export const IDL: Glam = { "isSigner": false }, { - "name": "fund", - "isMut": false, - "isSigner": false + "name": "manager", + "isMut": true, + "isSigner": true }, { "name": "jupiterProgram", diff --git a/anchor/tests/glam_jupiter.spec.ts b/anchor/tests/glam_jupiter.spec.ts index e37bfd0b..cb9f2db5 100644 --- a/anchor/tests/glam_jupiter.spec.ts +++ b/anchor/tests/glam_jupiter.spec.ts @@ -29,12 +29,12 @@ describe("glam_jupiter", () => { it("Swap", async () => { try { - const txId = await glamClient.jupiter.swap( - fundPDA, - wSol, - mSol, - 500_000_000 - ); + const txId = await glamClient.jupiter.swap(fundPDA, { + inputMint: wSol, + outputMint: mSol, + amount: 500_000_000, + maxAccounts: 10 + }); console.log("swap txId", txId); } catch (e) { console.error(e); diff --git a/anchor/tests/glam_wsol.spec.ts b/anchor/tests/glam_wsol.spec.ts index a5b7101d..8cf84e06 100644 --- a/anchor/tests/glam_wsol.spec.ts +++ b/anchor/tests/glam_wsol.spec.ts @@ -1,4 +1,9 @@ import * as anchor from "@coral-xyz/anchor"; +import { + Transaction, + SystemProgram, + sendAndConfirmTransaction +} from "@solana/web3.js"; import { createFundForTest, sleep } from "./setup"; import { GlamClient } from "../src"; @@ -12,26 +17,25 @@ describe("glam_wsol", () => { fundPDA = fundData.fundPDA; const connection = glamClient.provider.connection; - // air drop to treasury and delay 1s for confirmation - const airdropTx = await connection.requestAirdrop( - fundData.treasuryPDA, - 10_000_000_000 + // transfer 0.1 SOL to treasury + const tranferTx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: glamClient.getManager(), + toPubkey: glamClient.getTreasuryPDA(fundPDA), + lamports: 100_000_000 + }) ); - await connection.confirmTransaction({ - ...(await connection.getLatestBlockhash()), - signature: airdropTx - }); + await sendAndConfirmTransaction(connection, tranferTx, [ + glamClient.getWalletSigner() + ]); }); it("wSOL wrap", async () => { try { - let tx = await glamClient.wsol.wrap( - fundPDA, - new anchor.BN(3_000_000_000) - ); + let tx = await glamClient.wsol.wrap(fundPDA, new anchor.BN(30_000_000)); console.log("Wrap #1:", tx); - tx = await glamClient.wsol.wrap(fundPDA, new anchor.BN(2_000_000_000)); + tx = await glamClient.wsol.wrap(fundPDA, new anchor.BN(20_000_000)); console.log("Wrap #2:", tx); } catch (error) { console.log("Error", error); diff --git a/anchor/tests/setup.ts b/anchor/tests/setup.ts index bfe59c53..f97baf31 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -6,14 +6,17 @@ const usdc = new PublicKey("8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2"); // 6 const eth = new PublicKey("So11111111111111111111111111111111111111112"); // 6 decimals const btc = new PublicKey("3BZPwbcqB5kKScF3TEXxwNfx5ipV13kbRVDvfVp5c6fv"); // 9 decimals +const wsol = new PublicKey("So11111111111111111111111111111111111111112"); // 9 decimals +const msol = new PublicKey("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"); // 9 decimals + export const shareClass0Allowlist = [ - new PublicKey("a19a3us1Rm3YAV4NjjQzsaZ2brJWihsS1mf1fe94Ycj"), - new PublicKey("a1fwSFaH4w3LN8F2VNCz5WRb4KZTPZxgULG7vpNdB74"), - new PublicKey("a1sGZyirTFTv1SYUDHgCy3wWiTWXLRTa2vJSeDRDu9x") + // new PublicKey("a19a3us1Rm3YAV4NjjQzsaZ2brJWihsS1mf1fe94Ycj"), + // new PublicKey("a1fwSFaH4w3LN8F2VNCz5WRb4KZTPZxgULG7vpNdB74"), + // new PublicKey("a1sGZyirTFTv1SYUDHgCy3wWiTWXLRTa2vJSeDRDu9x") ]; export const shareClass0Blocklist = [ - new PublicKey("b182JJfadsQBao9wBYdSSiUxA1vo4Bb1ETXjyrsBumP"), - new PublicKey("b1NWY3dDonmeFXBZRHi13BKusrbWJeYDR2mjUgNHZYH") + // new PublicKey("b182JJfadsQBao9wBYdSSiUxA1vo4Bb1ETXjyrsBumP"), + // new PublicKey("b1NWY3dDonmeFXBZRHi13BKusrbWJeYDR2mjUgNHZYH") ]; export const sleep = async (ms: number) => { @@ -24,20 +27,20 @@ export const fundTestExample = { shareClasses: [ { // Glam Token - name: "Glam Investment Fund BTC-SOL", + name: "Glam Fund SOL-mSOL", symbol: "GBS", asset: usdc, allowlist: shareClass0Allowlist, blocklist: shareClass0Blocklist, // Glam - lockUpTime: 40 * 24 * 60 * 60, + lockUpTime: 4 * 60 * 60, requiresMemoOnTransfer: true, // Openfunds Share Class - fullShareClassName: "Glam Investment Fund BTC-SOL", + fullShareClassName: "Glam Fund SOL-mSOL", isin: "XS1082172823", cusip: "demo", valor: "demo", - shareClassCurrency: "USDC", + shareClassCurrency: "SOL", shareClassLifecycle: "active", investmentStatus: "open", shareClassDistributionPolicy: "accumulating", @@ -45,13 +48,13 @@ export const fundTestExample = { minimalInitialSubscriptionCategory: "amount", minimalInitialSubscriptionInShares: "0", minimalInitialSubscriptionInAmount: "1000", - currencyOfMinimalSubscription: "USDC", + currencyOfMinimalSubscription: "SOL", minimalRedemptionCategory: "shares", minimalInitialRedemptionInShares: "1", maximumInitialRedemptionInShares: "1000", minimalInitialRedemptionInAmount: "0", maximumInitialRedemptionInAmount: null, - currencyOfMinimalOrMaximumRedemption: "USDC", + currencyOfMinimalOrMaximumRedemption: "SOL", shareClassDividendType: "both", srri: "4", hasLockUpForRedemption: true, @@ -64,14 +67,14 @@ export const fundTestExample = { ], // Glam isEnabled: true, - assets: [usdc, btc, eth], - assetsWeights: [0, 60, 40], + assets: [wsol, msol], + assetsWeights: [50, 50], // Openfunds (Fund) fundDomicileAlpha2: "XS", - legalFundNameIncludingUmbrella: "Glam Investment Fund BTC-SOL", + legalFundNameIncludingUmbrella: "Glam Fund SOL-mSOL", fundLaunchDate: new Date().toISOString().split("T")[0], investmentObjective: "demo", - fundCurrency: "USDC", + fundCurrency: "SOL", openEndedOrClosedEndedFundStructure: "open-ended fund", fiscalYearEnd: "12-31", legalForm: "other", @@ -85,7 +88,7 @@ export const fundTestExample = { }, // Openfunds Manager (simplified) manager: { - portfolioManagerName: "0x0ece.sol" + portfolioManagerName: "glam.sol" } }; diff --git a/api/src/main.ts b/api/src/main.ts index 0c5ba862..708b5cf9 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -13,11 +13,12 @@ import { getPythProgramKeyForCluster } from "@pythnetwork/client"; -import { GlamClient } from "@glam/anchor"; +import { GlamClient, JUPITER_API_DEFAULT } from "@glam/anchor"; import { validatePubkey } from "./validation"; import { priceHistory, fundPerformance } from "./prices"; import { openfunds } from "./openfunds"; import { + jupiterSwapTx, marinadeDelayedUnstakeTx, marinadeDelayedUnstakeClaimTx, wsolWrapTx, @@ -31,15 +32,11 @@ if (process.env.NODE_ENV !== "production") { const BASE_URL = "https://api.glam.systems"; const SOLANA_RPC = process.env.SOLANA_RPC || "http://localhost:8899"; const SOLANA_MAINNET_KEY = process.env.SOLANA_MAINNET_KEY || ""; +const FORCE_MAINNET = process.env.MAINNET === "1"; +const JUPITER_API = process.env.JUPITER_API || JUPITER_API_DEFAULT; /* GlamClient for devnet and testnet */ -const devnetConnection = new Connection(SOLANA_RPC, "confirmed"); -const devnetProvider = new AnchorProvider(devnetConnection, null, { - commitment: "confirmed" -}); -const devnetClient = new GlamClient({ provider: devnetProvider }); - const mainnetConnection = new Connection( `https://mainnet.helius-rpc.com/?api-key=${SOLANA_MAINNET_KEY}`, "confirmed" @@ -47,7 +44,18 @@ const mainnetConnection = new Connection( const mainnetProvider = new AnchorProvider(mainnetConnection, null, { commitment: "confirmed" }); -const mainnetClient = new GlamClient({ provider: mainnetProvider }); +const mainnetClient = new GlamClient({ + provider: mainnetProvider, + jupiterApi: JUPITER_API +}); + +const devnetConnection = new Connection(SOLANA_RPC, "confirmed"); +const devnetProvider = new AnchorProvider(devnetConnection, null, { + commitment: "confirmed" +}); +const devnetClient = FORCE_MAINNET + ? mainnetClient + : new GlamClient({ provider: devnetProvider }); /* Pyth client */ @@ -108,8 +116,7 @@ app.get("/openfunds/:pubkey", async (req, res) => { */ app.post("/tx/jupiter/swap", async (req, res) => { - // TODO: implement - return res.send("Not implemented"); + return jupiterSwapTx(req.client, req, res); }); app.post("/tx/marinade/unstake", async (req, res) => { diff --git a/api/src/tx.ts b/api/src/tx.ts index c6166947..c5b5b13a 100644 --- a/api/src/tx.ts +++ b/api/src/tx.ts @@ -1,5 +1,33 @@ import { validatePubkey, validateBN } from "./validation"; +/* + * Marinade + */ + +export const jupiterSwapTx = async (client, req, res) => { + const fund = validatePubkey(req.body.fund); + const manager = validatePubkey(req.body.manager); + + if (fund === undefined || manager === undefined) { + return res.sendStatus(400); + } + + let tx; + try { + tx = await client.jupiter.swapTx( + fund, + manager, + req.body.quote, + req.body.quoteResponse, + req.body.swapInstruction + ); + } catch (err) { + console.log(err); + return res.status(400).send({ error: err.message }); + } + return await serializeTx(tx, manager, client, res); +}; + /* * Marinade */ @@ -75,15 +103,15 @@ const serializeTx = async (tx, manager, client, res) => { await client.provider.connection.getLatestBlockhash() ).blockhash; - serializedTx = tx - .serialize({ + serializedTx = new Buffer( + tx.serialize({ requireAllSignatures: false, verifySignatures: false }) - .toString("hex"); + ).toString("hex"); } catch (err) { console.log(err); - return res.sendStatus(400); + return res.status(400).send({ error: err.message }); } return res.send( JSON.stringify({