diff --git a/anchor/src/client.ts b/anchor/src/client.ts index 6b12ff5d..3886308e 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -1,15 +1,12 @@ import { GlamClientConfig } from "./clientConfig"; -import { - BaseClient, - JUPITER_API_DEFAULT as _JUPITER_API_DEFAULT -} from "./client/base"; +import { BaseClient } from "./client/base"; import { DriftClient } from "./client/drift"; import { InvestorClient } from "./client/investor"; import { JupiterClient } from "./client/jupiter"; import { MarinadeClient } from "./client/marinade"; import { WSolClient } from "./client/wsol"; -export const JUPITER_API_DEFAULT = _JUPITER_API_DEFAULT; +export { JUPITER_API_DEFAULT } from "./client/base"; export class GlamClient extends BaseClient { drift: DriftClient; diff --git a/anchor/src/client/jupiter.ts b/anchor/src/client/jupiter.ts index ec2541a6..7a9c8b13 100644 --- a/anchor/src/client/jupiter.ts +++ b/anchor/src/client/jupiter.ts @@ -16,8 +16,8 @@ const jupiterProgram = new PublicKey( ); interface QuoteParams { - inputMint: PublicKey; - outputMint: PublicKey; + inputMint: string; + outputMint: string; amount: number; autoSlippage?: boolean; autoSlippageCollisionUsdValue?: number; @@ -57,13 +57,16 @@ export class JupiterClient { swapInstruction?: any, addressLookupTableAddresses?: any ): Promise { - const outputMint = - quoteParams?.outputMint || new PublicKey(quoteResponse!.outputMint); - + // TODO: should not allow client side to specify destination token account + const outputMint = quoteParams?.outputMint || quoteResponse!.outputMint; + const destinationTokenAccount = this.base.getTreasuryAta( + fund, + new PublicKey(outputMint) + ); const tx = await this.swapTxBuilder( fund, this.base.getManager(), - this.base.getTreasuryAta(fund, outputMint), + destinationTokenAccount, quoteParams, quoteResponse, swapInstruction, @@ -178,7 +181,7 @@ export class JupiterClient { .accounts({ fund, manager, - inputAta: this.base.getManagerAta(inputMint), + inputAta: this.base.getManagerAta(new PublicKey(inputMint)), treasury: this.base.getTreasuryPDA(fund), outputAta: destinationTokenAccount, inputMint, @@ -191,8 +194,18 @@ export class JupiterClient { const addressLookupTableAccounts = await this.getAdressLookupTableAccounts( addressLookupTableAddresses ); + + let payerPublicKey; + try { + payerPublicKey = await this.base.getWalletSigner().publicKey; + } catch (e) { + console.log("Cannot get wallet signer:", e); + console.log("Default to fund manager as payer"); + payerPublicKey = manager; + } + const messageV0 = new TransactionMessage({ - payerKey: this.base.getWalletSigner().publicKey, + payerKey: payerPublicKey, recentBlockhash: ( await this.base.provider.connection.getLatestBlockhash() ).blockhash, @@ -242,15 +255,23 @@ export class JupiterClient { public async swapTx( fund: PublicKey, manager: PublicKey, - quote?: any, - quoteResponse?: any, + quoteParams?: QuoteParams, + quoteResponse?: QuoteResponse, swapInstruction?: any, addressLookupTableAddresses?: any ): Promise { + // TODO: should not allow client side to specify destination token account + const outputMint = quoteParams?.outputMint || quoteResponse!.outputMint; + const destinationTokenAccount = this.base.getTreasuryAta( + fund, + new PublicKey(outputMint) + ); + return await this.swapTxBuilder( fund, manager, - quote, + destinationTokenAccount, + quoteParams, quoteResponse, swapInstruction, addressLookupTableAddresses diff --git a/anchor/tests/glam_jupiter.spec.ts b/anchor/tests/glam_jupiter.spec.ts index e666ed7b..d463b133 100644 --- a/anchor/tests/glam_jupiter.spec.ts +++ b/anchor/tests/glam_jupiter.spec.ts @@ -29,8 +29,8 @@ describe("glam_jupiter", () => { const amount = 50_000_000; try { const txId = await glamClient.jupiter.swap(fundPDA, { - inputMint: usdc, - outputMint: msol, + inputMint: usdc.toBase58(), + outputMint: msol.toBase58(), amount, autoSlippage: true, autoSlippageCollisionUsdValue: 1000, @@ -56,8 +56,8 @@ describe("glam_jupiter", () => { const amount = 50_000_000; try { const txId = await glamClient.jupiter.swap(fundPDA, { - inputMint: wsol, - outputMint: msol, + inputMint: wsol.toBase58(), + outputMint: msol.toBase58(), amount, autoSlippage: true, autoSlippageCollisionUsdValue: 1000, diff --git a/api/.env.local.example b/api/.env.local.example new file mode 100644 index 00000000..ff03fe2a --- /dev/null +++ b/api/.env.local.example @@ -0,0 +1,3 @@ +# default RPC is localhost:8899 +SOLANA_RPC=https://api.mainnet-beta.solana.com +SOLANA_CLUSTER=mainnet-beta diff --git a/api/.env.yaml.example b/api/.env.yaml.example new file mode 100644 index 00000000..d976e8d4 --- /dev/null +++ b/api/.env.yaml.example @@ -0,0 +1,3 @@ +env_variables: + SOLANA_RPC: https://api.mainnet-beta.solana.com + SOLANA_CLUSTER: mainnet-beta diff --git a/api/src/main.ts b/api/src/main.ts index ed0a8e81..85fcfc81 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -16,34 +16,31 @@ if (process.env.NODE_ENV !== "production") { require("dotenv").config({ path: ".env.local", override: true }); } -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 SOLANA_CLUSTER = (process.env.SOLANA_CLUSTER || "custom") as any; const JUPITER_API = process.env.JUPITER_API || JUPITER_API_DEFAULT; -/* GlamClient for devnet and testnet */ +/* GlamClient setup */ -const mainnetConnection = new Connection( - `https://mainnet.helius-rpc.com/?api-key=${SOLANA_MAINNET_KEY}`, - "confirmed" -); -const mainnetProvider = new AnchorProvider(mainnetConnection, null, { - commitment: "confirmed" -}); -const mainnetClient = new GlamClient({ - cluster: "mainnet-beta", - provider: mainnetProvider, +// Default client is configured by environment variables +const defaultClient = new GlamClient({ + cluster: SOLANA_CLUSTER, + provider: new AnchorProvider( + new Connection(SOLANA_RPC, "confirmed"), + null, + {} + ), jupiterApi: JUPITER_API }); -const devnetConnection = new Connection(SOLANA_RPC, "confirmed"); -const devnetProvider = new AnchorProvider(devnetConnection, null, { - commitment: "confirmed" +const devnetClient = new GlamClient({ + cluster: "devnet", + provider: new AnchorProvider( + new Connection("https://api.devnet.solana.com", "confirmed"), + null, + {} + ) }); -const devnetClient = FORCE_MAINNET - ? mainnetClient - : new GlamClient({ provider: devnetProvider }); /* Express app */ @@ -52,9 +49,9 @@ app.use(cors({ origin: "*", methods: "GET" })); app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, "assets"))); app.use((req, res, next) => { - if (req.hostname === "api.glam.systems") { - req.client = mainnetClient; - } else { + req.client = defaultClient; + // Use devnet client if running on GAE and not on api.glam.systems + if (process.env.GAE_SERVICE && req.hostname !== "api.glam.systems") { req.client = devnetClient; } next(); diff --git a/api/src/routers/tx.ts b/api/src/routers/tx.ts index 39adca61..88ef5a7c 100644 --- a/api/src/routers/tx.ts +++ b/api/src/routers/tx.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import { validatePubkey, validateBN } from "../validation"; +import { Transaction, VersionedTransaction } from "@solana/web3.js"; /* * Marinade @@ -13,20 +14,19 @@ const jupiterSwapTx = async (client, req, res) => { return res.sendStatus(400); } - let tx; try { - tx = await client.jupiter.swapTx( + const tx = await client.jupiter.swapTx( fund, manager, req.body.quote, req.body.quoteResponse, req.body.swapInstruction ); + return serializeVersionedTx(tx, res); } catch (err) { console.log(err); return res.status(400).send({ error: err.message }); } - return await serializeTx(tx, manager, client, res); }; /* @@ -60,66 +60,41 @@ const marinadeDelayedUnstakeClaimTx = async (client, req, res) => { return await serializeTx(tx, manager, client, res); }; -/* - * wSOL - */ - -const wsolWrapTx = async (client, req, res) => { - const fund = validatePubkey(req.body.fund); - const manager = validatePubkey(req.body.manager); - const amount = validateBN(req.body.amount); - - if (fund === undefined || manager === undefined || amount === undefined) { - return res.sendStatus(400); - } - - console.log("client", client); - const tx = await client.wsol.wrapTx(fund, manager, amount); - - return await serializeTx(tx, manager, client, res); -}; - -const wsolUnwrapTx = 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); - } - - const tx = await client.wsol.unwrapTx(fund, manager); - - return await serializeTx(tx, manager, client, res); -}; - /* * Common */ -const serializeTx = async (tx, manager, client, res) => { +const serializeTx = async (tx: Transaction, manager, client, res) => { tx.feePayer = manager; - let serializedTx = ""; try { tx.recentBlockhash = ( await client.provider.connection.getLatestBlockhash() ).blockhash; - - serializedTx = new Buffer( + const serializedTx = Buffer.from( tx.serialize({ requireAllSignatures: false, verifySignatures: false }) ).toString("hex"); + return res.send({ tx: serializedTx, versioned: false }); + } catch (err) { + console.log(err); + return res.status(400).send({ error: err.message }); + } +}; + +const serializeVersionedTx = async (tx: VersionedTransaction, res) => { + try { + const serializedTx = Buffer.from(tx.serialize()).toString("hex"); + return res.send({ + tx: serializedTx, + versioned: true + }); } catch (err) { console.log(err); return res.status(400).send({ error: err.message }); } - return res.send( - JSON.stringify({ - tx: serializedTx - }) + "\n" - ); }; /* @@ -144,13 +119,34 @@ router.post("/tx/marinade/unstake/claim", async (req, res) => { }); router.post("/tx/wsol/wrap", async (req, res) => { + const fund = validatePubkey(req.body.fund); + const manager = validatePubkey(req.body.manager); + const amount = validateBN(req.body.amount); + + console.log(fund, manager, amount); + + if (!fund || !manager || !amount) { + return res.sendStatus(400); + } + res.set("content-type", "application/json"); - return wsolWrapTx(req.client, req, res); + const tx = await req.client.wsol.wrapTx(fund, manager, amount); + + return await serializeTx(tx, manager, req.client, res); }); router.post("/tx/wsol/unwrap", async (req, res) => { + const fund = validatePubkey(req.body.fund); + const manager = validatePubkey(req.body.manager); + + if (!fund || !manager) { + return res.sendStatus(400); + } + res.set("content-type", "application/json"); - return wsolUnwrapTx(req.client, req, res); + const tx = await req.client.wsol.unwrapTx(fund, manager); + + return await serializeTx(tx, manager, req.client, res); }); export default router; diff --git a/api/src/validation.ts b/api/src/validation.ts index 82496ea7..4267e8b4 100644 --- a/api/src/validation.ts +++ b/api/src/validation.ts @@ -16,6 +16,7 @@ export const validateBN = (num: string) => { try { res = new BN(num); } catch (_e) { + console.error(_e); return undefined; } return res;