From b91c5cd9a5a1f4fde0359714160f008fc8efd3d4 Mon Sep 17 00:00:00 2001 From: Andrew Kirillov Date: Mon, 9 Feb 2026 11:27:00 -0800 Subject: [PATCH] feat(external-match): external order validation + exact output example --- .../external-match/exact-output/README.md | 7 ++ examples/external-match/exact-output/env.ts | 27 ++++++ examples/external-match/exact-output/index.ts | 93 +++++++++++++++++++ .../external-match/exact-output/package.json | 16 ++++ .../external-match/exact-output/tsconfig.json | 19 ++++ packages/core/src/utils.d.ts | 92 +++++++++--------- packages/external-match/CHANGELOG.md | 6 ++ packages/external-match/package.json | 2 +- packages/external-match/src/client.ts | 43 +++++++++ 9 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 examples/external-match/exact-output/README.md create mode 100644 examples/external-match/exact-output/env.ts create mode 100644 examples/external-match/exact-output/index.ts create mode 100644 examples/external-match/exact-output/package.json create mode 100644 examples/external-match/exact-output/tsconfig.json diff --git a/examples/external-match/exact-output/README.md b/examples/external-match/exact-output/README.md new file mode 100644 index 00000000..26de9f9b --- /dev/null +++ b/examples/external-match/exact-output/README.md @@ -0,0 +1,7 @@ +# Exact External Match Output Example + +```bash +bun run index.ts +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/renegade-fi/typescript-sdk/tree/main/examples/external-match/exact-output) diff --git a/examples/external-match/exact-output/env.ts b/examples/external-match/exact-output/env.ts new file mode 100644 index 00000000..11f41a54 --- /dev/null +++ b/examples/external-match/exact-output/env.ts @@ -0,0 +1,27 @@ +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { arbitrumSepolia } from "viem/chains"; + +const chainId = arbitrumSepolia.id; +const privateKey = process.env.PKEY; +if (!privateKey) { + throw new Error("PKEY is not set"); +} +const account = privateKeyToAccount(privateKey as `0x${string}`); +const owner = account.address; + +const API_KEY = process.env.API_KEY; +const API_SECRET = process.env.API_SECRET; + +const publicClient = createPublicClient({ + chain: arbitrumSepolia, + transport: http(), +}); + +const walletClient = createWalletClient({ + account, + chain: arbitrumSepolia, + transport: http(), +}); + +export { account, chainId, publicClient, walletClient, API_KEY, API_SECRET, owner }; diff --git a/examples/external-match/exact-output/index.ts b/examples/external-match/exact-output/index.ts new file mode 100644 index 00000000..2e1e6ea5 --- /dev/null +++ b/examples/external-match/exact-output/index.ts @@ -0,0 +1,93 @@ +import { ExternalMatchClient, OrderSide } from "@renegade-fi/renegade-sdk"; +import { erc20Abi } from "viem"; +import { API_KEY, API_SECRET, owner, publicClient, walletClient } from "./env"; + +if (!API_KEY) { + throw new Error("API_KEY is not set"); +} + +if (!API_SECRET) { + throw new Error("API_SECRET is not set"); +} + +const client = ExternalMatchClient.newArbitrumSepoliaClient(API_KEY, API_SECRET); + +const WETH_ADDRESS = "0xc3414a7ef14aaaa9c4522dfc00a4e66e74e9c25a"; +const USDC_ADDRESS = "0xdf8d259c04020562717557f2b5a3cf28e92707d1"; + +// Use exact_quote_output to specify the exact amount of USDC to receive from a sell. +// Unlike quote_amount (which specifies a maximum input), exact_quote_output guarantees +// the exact output amount with no slippage. +const order = { + base_mint: WETH_ADDRESS, + quote_mint: USDC_ADDRESS, + side: OrderSide.SELL, + exact_quote_output: BigInt(2_000_000), // Exactly 2 USDC output +}; + +console.log("Fetching quote with exact output..."); + +const quote = await client.requestQuote(order); + +if (!quote) { + console.error("No quote available, exiting..."); + process.exit(1); +} + +console.log("Assembling quote..."); + +const bundle = await client.assembleQuote(quote); + +if (!bundle) { + console.error("No bundle available, exiting..."); + process.exit(1); +} + +const tx = bundle.match_bundle.settlement_tx; + +// --- Allowance Check --- // + +const isSell = bundle.match_bundle.match_result.direction === "Sell"; +const address = isSell + ? (bundle.match_bundle.match_result.base_mint as `0x${string}`) + : (bundle.match_bundle.match_result.quote_mint as `0x${string}`); +const amount = isSell + ? bundle.match_bundle.match_result.base_amount + : bundle.match_bundle.match_result.quote_amount; +const spender = tx.to as `0x${string}`; + +console.log("Checking allowance..."); + +const allowance = await publicClient.readContract({ + address, + abi: erc20Abi, + functionName: "allowance", + args: [owner, spender], +}); + +if (allowance < amount) { + console.log("Allowance is less than amount, approving..."); + const approveTx = await walletClient.writeContract({ + address, + abi: erc20Abi, + functionName: "approve", + args: [spender, amount], + }); + console.log("Submitting approve transaction..."); + await publicClient.waitForTransactionReceipt({ + hash: approveTx, + }); + console.log("Successfully submitted approve transaction", approveTx); +} + +// --- Submit Bundle --- // + +console.log("Submitting bundle..."); + +const hash = await walletClient.sendTransaction({ + to: tx.to as `0x${string}`, + data: tx.data as `0x${string}`, + type: "eip1559", +}); + +console.log("Successfully submitted transaction", hash); diff --git a/examples/external-match/exact-output/package.json b/examples/external-match/exact-output/package.json new file mode 100644 index 00000000..9b6c5015 --- /dev/null +++ b/examples/external-match/exact-output/package.json @@ -0,0 +1,16 @@ +{ + "name": "example-external-match-exact-output", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@renegade-fi/renegade-sdk": "latest", + "viem": "latest" + }, + "devDependencies": { + "tsx": "^4", + "typescript": "^5" + } +} diff --git a/examples/external-match/exact-output/tsconfig.json b/examples/external-match/exact-output/tsconfig.json new file mode 100644 index 00000000..7f285754 --- /dev/null +++ b/examples/external-match/exact-output/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/core/src/utils.d.ts b/packages/core/src/utils.d.ts index 24c66458..4c8402cd 100644 --- a/packages/core/src/utils.d.ts +++ b/packages/core/src/utils.d.ts @@ -1,6 +1,50 @@ /* tslint:disable */ /* eslint-disable */ /** +* @param {string} seed +* @returns {any} +*/ +export function derive_blinder_share(seed: string): any; +/** +* @param {string} seed +* @returns {any} +*/ +export function wallet_id(seed: string): any; +/** +* @param {string} wallet_str +* @returns {any} +*/ +export function wallet_nullifier(wallet_str: string): any; +/** +* @param {Function} sign_message +* @returns {Promise} +*/ +export function generate_wallet_secrets(sign_message: Function): Promise; +/** +* @param {string} seed +* @param {bigint} nonce +* @returns {any} +*/ +export function derive_sk_root_from_seed(seed: string, nonce: bigint): any; +/** +* @param {string} seed +* @param {bigint} nonce +* @returns {any} +*/ +export function get_pk_root(seed: string, nonce: bigint): any; +/** +* @param {string | undefined} [seed] +* @param {bigint | undefined} [nonce] +* @param {string | undefined} [public_key] +* @returns {any[]} +*/ +export function get_pk_root_scalars(seed?: string, nonce?: bigint, public_key?: string): any[]; +/** +* @param {string} seed +* @returns {any} +*/ +export function get_symmetric_key(seed: string): any; +/** * @param {string} wallet_id * @param {string} blinder_seed * @param {string} share_seed @@ -9,7 +53,7 @@ * @param {string} symmetric_key * @returns {Promise} */ -export function find_external_wallet(wallet_id: string, blinder_seed: string, share_seed: string, pk_root: string, sk_match: string, symmetric_key: string): Promise; +export function create_external_wallet(wallet_id: string, blinder_seed: string, share_seed: string, pk_root: string, sk_match: string, symmetric_key: string): Promise; /** * @param {string} seed * @returns {any} @@ -144,27 +188,7 @@ export function assemble_external_match(do_gas_estimation: boolean, allow_shared * @param {string} symmetric_key * @returns {Promise} */ -export function create_external_wallet(wallet_id: string, blinder_seed: string, share_seed: string, pk_root: string, sk_match: string, symmetric_key: string): Promise; -/** -* @param {string} seed -* @returns {any} -*/ -export function derive_blinder_share(seed: string): any; -/** -* @param {string} seed -* @returns {any} -*/ -export function wallet_id(seed: string): any; -/** -* @param {string} wallet_str -* @returns {any} -*/ -export function wallet_nullifier(wallet_str: string): any; -/** -* @param {Function} sign_message -* @returns {Promise} -*/ -export function generate_wallet_secrets(sign_message: Function): Promise; +export function find_external_wallet(wallet_id: string, blinder_seed: string, share_seed: string, pk_root: string, sk_match: string, symmetric_key: string): Promise; /** * @param {string} path * @param {any} headers @@ -178,27 +202,3 @@ export function create_request_signature(path: string, headers: any, body: strin * @returns {string} */ export function b64_to_hex_hmac_key(b64_key: string): string; -/** -* @param {string} seed -* @param {bigint} nonce -* @returns {any} -*/ -export function derive_sk_root_from_seed(seed: string, nonce: bigint): any; -/** -* @param {string} seed -* @param {bigint} nonce -* @returns {any} -*/ -export function get_pk_root(seed: string, nonce: bigint): any; -/** -* @param {string | undefined} [seed] -* @param {bigint | undefined} [nonce] -* @param {string | undefined} [public_key] -* @returns {any[]} -*/ -export function get_pk_root_scalars(seed?: string, nonce?: bigint, public_key?: string): any[]; -/** -* @param {string} seed -* @returns {any} -*/ -export function get_symmetric_key(seed: string): any; diff --git a/packages/external-match/CHANGELOG.md b/packages/external-match/CHANGELOG.md index cb7f04cb..06ac2f12 100644 --- a/packages/external-match/CHANGELOG.md +++ b/packages/external-match/CHANGELOG.md @@ -1,5 +1,11 @@ # @renegade-fi/renegade-sdk +## 1.0.1 + +### Patch Changes + +- Add external order validation logic w/ an exact-output external match example + ## 1.0.0 ### Major Changes diff --git a/packages/external-match/package.json b/packages/external-match/package.json index 3ff63a57..36e740d5 100644 --- a/packages/external-match/package.json +++ b/packages/external-match/package.json @@ -1,6 +1,6 @@ { "name": "@renegade-fi/renegade-sdk", - "version": "1.0.0", + "version": "1.0.1", "description": "A TypeScript client for interacting with the Renegade Darkpool API", "repository": { "type": "git", diff --git a/packages/external-match/src/client.ts b/packages/external-match/src/client.ts index 91ce93b8..c2d3e4c6 100644 --- a/packages/external-match/src/client.ts +++ b/packages/external-match/src/client.ts @@ -361,6 +361,40 @@ export class AssembleMalleableExternalMatchOptions extends AssembleExternalMatch } } +/** + * Validate that an external order has the required fields and exactly one sizing field set. + */ +function validateExternalOrder(order: ExternalOrder): void { + if (!order.base_mint) { + throw new ExternalMatchClientError("base_mint must be set"); + } + if (!order.quote_mint) { + throw new ExternalMatchClientError("quote_mint must be set"); + } + if (!order.side) { + throw new ExternalMatchClientError("side must be set"); + } + + const sizingFields = [ + order.base_amount, + order.quote_amount, + order.exact_base_output, + order.exact_quote_output, + ]; + const numSet = sizingFields.filter((f) => f !== undefined && f !== BigInt(0)).length; + + if (numSet === 0) { + throw new ExternalMatchClientError( + "exactly one of base_amount, quote_amount, exact_base_output, or exact_quote_output must be set", + ); + } + if (numSet > 1) { + throw new ExternalMatchClientError( + "exactly one of base_amount, quote_amount, exact_base_output, or exact_quote_output must be set", + ); + } +} + /** * Error thrown by the ExternalMatchClient. */ @@ -501,6 +535,7 @@ export class ExternalMatchClient { order: ExternalOrder, options: RequestQuoteOptions, ): Promise { + validateExternalOrder(order); const request: ExternalQuoteRequest = { external_order: order, }; @@ -527,6 +562,7 @@ export class ExternalMatchClient { order: ExternalOrder, options: RequestExternalMatchOptions, ): Promise { + validateExternalOrder(order); const request: ExternalMatchRequest = { do_gas_estimation: options.doGasEstimation, receiver_address: options.receiverAddress, @@ -569,6 +605,7 @@ export class ExternalMatchClient { order: ExternalOrder, options: RequestExternalMatchOptions, ): Promise { + validateExternalOrder(order); const request: ExternalMatchRequest = { do_gas_estimation: options.doGasEstimation, receiver_address: options.receiverAddress, @@ -610,6 +647,9 @@ export class ExternalMatchClient { quote: SignedExternalQuote, options: AssembleExternalMatchOptions, ): Promise { + if (options.updatedOrder) { + validateExternalOrder(options.updatedOrder); + } const signedQuote: ApiSignedExternalQuote = { quote: quote.quote, signature: quote.signature, @@ -655,6 +695,9 @@ export class ExternalMatchClient { quote: SignedExternalQuote, options: AssembleMalleableExternalMatchOptions, ): Promise { + if (options.updatedOrder) { + validateExternalOrder(options.updatedOrder); + } const signedQuote: ApiSignedExternalQuote = { quote: quote.quote, signature: quote.signature,