Created
December 5, 2025 04:24
-
-
Save tubackkhoa/2fb853ee9533cecd80615c6fada15b3e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import "dotenv/config"; | |
| import { VersionedTransaction, Keypair } from "@solana/web3.js"; | |
| import nacl from "tweetnacl"; | |
| import { z } from "zod"; | |
| import bs58 from "bs58"; | |
| export const TriggerPriceOrderSchema = z.object({ | |
| id: z.string(), | |
| user: z.string(), | |
| inToken: z.string(), | |
| outToken: z.string(), | |
| inAmount: z.string(), | |
| triggerPrice: z.string(), | |
| outAmountMin: z.string().optional(), | |
| status: z.enum(["open", "filled", "cancelled", "failed"]), | |
| createdAt: z.number(), | |
| updatedAt: z.number(), | |
| // ... more fields | |
| }); | |
| export const TriggerOrderSchema = z.object({ | |
| /* DCA/TWAP fields */ | |
| }); | |
| export const TriggerPriceOrderHistorySchema = z.object({ | |
| /* history fields */ | |
| }); | |
| const LITE_API_BASE = | |
| process.env.NEXT_PUBLIC_JUPITER_TRIGGER_API_URL ?? "https://lite-api.jup.ag/trigger/v1"; | |
| const TRIGGER_API_BASE = | |
| typeof window !== "undefined" | |
| ? (localStorage.getItem("TRIGGER_API_URL") ?? "https://trigger.jup.ag") | |
| : "https://trigger.jup.ag"; | |
| /** | |
| * Jupiter Limit Orderbook V2 SDK | |
| * Supports Trigger Orders (DCA/TWAP) + Advanced Limit Orders (Price Triggers, OCO, OTOCO) | |
| */ | |
| export class JupiterLimitV2 { | |
| private static cookies: string; | |
| public static setToken(token: string) { | |
| this.cookies = ["privy-token=" + token, "privy-session=privy.jup.ag"].join("; "); | |
| } | |
| private static getHeaders(includeCredentials = true): RequestInit { | |
| return { | |
| headers: { | |
| Accept: "application/json", | |
| "Content-Type": "application/json", | |
| Cookie: this.cookies, // This is the magic | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", | |
| }, | |
| credentials: includeCredentials ? "include" : "omit", | |
| }; | |
| } | |
| // === Lite API (Public Trigger Orders - DCA/TWAP) === | |
| static async getTriggerOrders( | |
| params: Record<string, string | number | boolean>, | |
| options?: RequestInit, | |
| ) { | |
| const url = new URL(`${LITE_API_BASE}/getTriggerOrders`); | |
| url.search = new URLSearchParams(params as Record<string, string>).toString(); | |
| return fetch(url, { ...this.getHeaders(), ...options }) | |
| .then((res) => res.json()) | |
| .then((data) => z.array(TriggerOrderSchema).parse(data)); | |
| } | |
| static async createOrder(body: any, options?: RequestInit) { | |
| return fetch(`${LITE_API_BASE}/createOrder`, { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async cancelOrders(body: { orderIds: string[] }, options?: RequestInit) { | |
| return fetch(`${LITE_API_BASE}/cancelOrders`, { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async authenticatePrivy(wallet: Keypair) { | |
| const address = wallet.publicKey.toBase58(); | |
| const { nonce } = await fetch("https://privy.jup.ag/api/v1/siws/init", { | |
| headers: { | |
| Accept: "application/json", | |
| "Content-Type": "application/json", | |
| "privy-app-id": "cm8pkejey0052l1ljvgxjztau", | |
| "privy-ca-id": "7f61aba2-152f-4e8d-a1cb-0398d30a3c60", | |
| "privy-client": "react-auth:2.25.0", | |
| Origin: "https://jup.ag/", | |
| }, | |
| body: `{"address":"${address}"}`, | |
| method: "POST", | |
| credentials: "include", | |
| }).then((res) => res.json()); | |
| console.log(nonce); | |
| const message = `jup.ag wants you to sign in with your Solana account:\n${address}\n\nYou are proving you own ${address}.\n\nURI: https://jup.ag\nVersion: 1\nChain ID: mainnet\nNonce: ${nonce}\nIssued At: 2025-12-05T03:56:44.926Z\nResources:\n- https://privy.io`; | |
| // MUST convert to bytes first | |
| const messageBytes = new TextEncoder().encode(message); | |
| // Sign using ed25519 | |
| const signature = nacl.sign.detached(messageBytes, wallet.secretKey); | |
| // Convert to base64 for Privy | |
| const signatureBase64 = Buffer.from(signature).toString("base64"); | |
| return fetch("https://privy.jup.ag/api/v1/siws/authenticate", { | |
| method: "POST", | |
| headers: { | |
| Accept: "application/json", | |
| "Content-Type": "application/json", | |
| "privy-app-id": "cm8pkejey0052l1ljvgxjztau", | |
| Origin: "https://jup.ag/", | |
| }, | |
| body: JSON.stringify({ | |
| message, | |
| signature: signatureBase64, | |
| walletClientType: "jupiter", | |
| connectorType: "solana_adapter", | |
| mode: "login-or-sign-up", | |
| message_type: "plain", | |
| }), | |
| }).then(async (res) => { | |
| const data = await res.json(); | |
| console.log(data); | |
| // When successful, store the token automatically | |
| if (data?.token) JupiterLimitV2.setToken(data.token); | |
| return data; | |
| }); | |
| } | |
| static async execute(body: any, options?: RequestInit) { | |
| return fetch(`${LITE_API_BASE}/execute`, { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async getFeeStructure(options?: RequestInit) { | |
| return fetch(`${LITE_API_BASE}/getFees`, { | |
| ...this.getHeaders(false), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| // === Trigger API (Advanced Limit Orders - Price Triggers) === | |
| private static triggerUrl(path: string) { | |
| return `${TRIGGER_API_BASE}${path}`; | |
| } | |
| // Privy Wallet (optional auth) | |
| static async checkPrivyWalletExists(userId: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/privy/wallets/${userId}/exists`), { | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async getPrivyWallets(userId: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/privy/wallets/${userId}`), { | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async registerPrivyWallet(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/privy/wallets/register"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => TriggerPriceOrderSchema.parse(data)); // adjust schema | |
| } | |
| // Price Trigger Orders | |
| static async getPriceOrderById(orderId: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/${orderId}`), { | |
| ...this.getHeaders(), | |
| ...options, | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => TriggerPriceOrderSchema.parse(data)); | |
| } | |
| static async getPriceOrdersByUser(userAddress: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/by-user/${userAddress}`), { | |
| ...this.getHeaders(), | |
| ...options, | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => z.array(TriggerPriceOrderSchema).parse(data)); | |
| } | |
| static async getPriceOrderHistoryByUser( | |
| userAddress: string, | |
| params?: Record<string, string | number>, | |
| options?: RequestInit, | |
| ) { | |
| const url = new URL(this.triggerUrl(`/orders/history/by-user/${userAddress}`)); | |
| if (params) url.search = new URLSearchParams(params as any).toString(); | |
| return fetch(url, { ...this.getHeaders(), ...options }) | |
| .then((res) => res.json()) | |
| .then((data) => TriggerPriceOrderHistorySchema.array().parse(data)); | |
| } | |
| static async createPriceOrder(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/orders/price"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => TriggerPriceOrderSchema.parse(data)); | |
| } | |
| static async updatePriceOrder(orderId: string, body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/${orderId}`), { | |
| method: "PATCH", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => TriggerPriceOrderSchema.parse(data)); | |
| } | |
| static async cancelPriceOrder(orderId: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/${orderId}/cancel`), { | |
| method: "POST", | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async craftTransferTx(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/orders/price/craft-transfer"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async craftWithdrawalTx(orderId: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/${orderId}/craft-withdrawal`), { | |
| method: "POST", | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async confirmWithdrawal(orderId: string, body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/${orderId}/confirm-withdrawal`), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| // Advanced Order Types | |
| static async createOCOOrder(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/orders/price/oco"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async createOTOCOOrder(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/orders/price/otoco"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async cancelAllUserOrders(userAddress: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/cancel-all/${userAddress}`), { | |
| method: "POST", | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async withdrawAllCancelled(userAddress: string, options?: RequestInit) { | |
| return fetch(this.triggerUrl(`/orders/price/withdraw-all-cancelled/${userAddress}`), { | |
| method: "POST", | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| static async confirmMultipleWithdrawals(body: any, options?: RequestInit) { | |
| return fetch(this.triggerUrl("/orders/price/confirm-multiple-withdrawals"), { | |
| method: "POST", | |
| body: JSON.stringify(body), | |
| ...this.getHeaders(), | |
| ...options, | |
| }).then((res) => res.json()); | |
| } | |
| } | |
| (async () => { | |
| const wallet = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY_BASE58!)); | |
| await JupiterLimitV2.authenticatePrivy(wallet); | |
| const privyUser = await JupiterLimitV2.getPrivyWallets(wallet.publicKey.toBase58()); | |
| console.log(privyUser); | |
| const inputAmount = "100000000"; // 0.01 SOL | |
| const inputMint = "So11111111111111111111111111111111111111112"; // SOL | |
| const outputMint = "Es9vMFrzaCER3H1WXy3PaY5E9sDcXavk6i7p2N6zk9W"; // USDT | |
| // Step 1: Craft the transfer transaction (Jupiter gives you the unsigned tx) | |
| const craftRes = await JupiterLimitV2.craftTransferTx({ | |
| senderAddress: wallet.publicKey.toBase58(), | |
| receiverAddress: privyUser.privyWalletPubkey, | |
| mint: inputMint, | |
| amount: inputAmount, | |
| prepareOrderAtas: { inputMint, outputMint }, | |
| }); | |
| console.log(craftRes); | |
| // Step 2: Deserialize → Sign → Re-encode | |
| const versionedTx = VersionedTransaction.deserialize(Buffer.from(craftRes.transaction, "base64")); | |
| versionedTx.sign([wallet]); | |
| const signedTransferTx = bs58.encode(versionedTx.serialize()); | |
| console.log(signedTransferTx); | |
| // Step 3: Now create the actual OCO order | |
| const ocoOrder = await JupiterLimitV2.createOCOOrder({ | |
| userPubkey: wallet.publicKey.toBase58(), | |
| privyWalletId: privyUser.privyWalletId, | |
| privyWalletPubkey: privyUser.privyWalletPubkey, | |
| inputMint, | |
| inputAmount, | |
| outputMint, | |
| triggerMint: inputMint, // usually SOL or USDC | |
| aboveTriggerPriceUsd: "220", // Take profit | |
| belowTriggerPriceUsd: "160", // Stop loss | |
| expiresAt: Math.floor(Date.now() / 1000) + 30 * 24 * 3600, // 30 days | |
| signedTransferTx, // This is now correctly formatted | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment