diff --git a/.env.example b/.env.example index ff536b3..4b71409 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,4 @@ OKX_API_KEY= OKX_SECRET_KEY= OKX_API_PASSPHRASE= OKX_PROJECT_ID= +SQUID_INTEGRATOR_ID= diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ba61bcb..7464d1e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,6 +35,7 @@ jobs: OKX_SECRET_KEY: "ci-dummy-secret" OKX_API_PASSPHRASE: "ci-dummy-passphrase" OKX_PROJECT_ID: "ci-dummy-project" + SQUID_INTEGRATOR_ID: "ci-dummy-squid-integrator" run: | docker run -d --rm --name test-container -p 3000:3000 \ -e PANORA_API_KEY \ @@ -42,6 +43,7 @@ jobs: -e OKX_SECRET_KEY \ -e OKX_API_PASSPHRASE \ -e OKX_PROJECT_ID \ + -e SQUID_INTEGRATOR_ID \ core-ts:test echo "Waiting for container to start..." sleep 4 # Adjust sleep time if needed diff --git a/AGENTS.md b/AGENTS.md index 1ca6e99..7451489 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ - Use shared quote fixtures from `packages/swapper/src/testkit/mock.ts`; for provider-specific scenarios, extend via overrides instead of adding new provider-local `testkit.ts` files. ## Architecture Overview -- Providers implemented today: `stonfi_v2`, `mayan`, `cetus`, `relay`, `orca`, `okx`. +- Providers implemented today: `stonfi_v2`, `mayan`, `cetus`, `relay`, `orca`, `okx`, `squid`. - API endpoints: `GET /` (providers, version), `POST /:providerId/quote`, `POST /:providerId/quote_data`. - Keep provider interface consistent for quotes and transaction building. - OKX Solana provider notes: [`packages/swapper/src/okx/README.md`](packages/swapper/src/okx/README.md). diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a4e8bae..9f94185 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -9,6 +9,7 @@ import { OrcaWhirlpoolProvider, PanoraProvider, OkxProvider, + SquidProvider, SwapperException, } from "@gemwallet/swapper"; import { Quote, QuoteRequest, SwapQuoteData } from "@gemwallet/types"; @@ -43,6 +44,7 @@ const providers: Record = { orca: new OrcaWhirlpoolProvider(solanaRpc), panora: new PanoraProvider(), okx: new OkxProvider(solanaRpc), + squid: new SquidProvider(), }; app.get("/", (_, res) => { diff --git a/packages/swapper/src/cosmos_fee.ts b/packages/swapper/src/cosmos_fee.ts new file mode 100644 index 0000000..6dd282c --- /dev/null +++ b/packages/swapper/src/cosmos_fee.ts @@ -0,0 +1,29 @@ +import { Chain, QuoteRequest } from "@gemwallet/types"; + +import { SwapperException } from "./error"; + +// Swap tx fees: preload_gas_limit(2M) × 1.3 × base_fee / 200K +const COSMOS_SWAP_FEES: Record = { + [Chain.Cosmos]: BigInt("39000"), + [Chain.Osmosis]: BigInt("130000"), + [Chain.Celestia]: BigInt("39000"), + [Chain.Injective]: BigInt("1300000000000000"), + [Chain.Sei]: BigInt("1300000"), + [Chain.Noble]: BigInt("325000"), +}; + +export function resolveCosmosMaxAmount(quoteRequest: QuoteRequest): string { + if (!quoteRequest.use_max_amount) { + return quoteRequest.from_value; + } + const chain = quoteRequest.from_asset.id.split("_")[0]; + const reservedFee = COSMOS_SWAP_FEES[chain]; + if (!reservedFee) { + return quoteRequest.from_value; + } + const amount = BigInt(quoteRequest.from_value); + if (amount <= reservedFee) { + throw new SwapperException({ type: "input_amount_error", min_amount: reservedFee.toString() }); + } + return (amount - reservedFee).toString(); +} diff --git a/packages/swapper/src/index.ts b/packages/swapper/src/index.ts index e617e78..51f8a79 100644 --- a/packages/swapper/src/index.ts +++ b/packages/swapper/src/index.ts @@ -7,4 +7,5 @@ export * from "./referrer"; export * from "./orca"; export * from "./panora"; export * from "./okx"; +export * from "./squid"; export * from "./error"; diff --git a/packages/swapper/src/okx/constants.ts b/packages/swapper/src/okx/constants.ts index e2a4526..1535f80 100644 --- a/packages/swapper/src/okx/constants.ts +++ b/packages/swapper/src/okx/constants.ts @@ -32,7 +32,7 @@ export const SOLANA_NATIVE_TOKEN_ADDRESS = "11111111111111111111111111111111"; export const SOLANA_DEX_IDS_PARAM = SOLANA_DEX_IDS.join(","); export const DEFAULT_SLIPPAGE_PERCENT = "1"; const EVM_GAS_LIMITS: Partial> = { - [Chain.Base]: "800000", + [Chain.Base]: "920000", [Chain.Manta]: "600000", [Chain.Mantle]: "2000000000", [Chain.XLayer]: "800000", diff --git a/packages/swapper/src/protobuf.test.ts b/packages/swapper/src/protobuf.test.ts new file mode 100644 index 0000000..ba6834b --- /dev/null +++ b/packages/swapper/src/protobuf.test.ts @@ -0,0 +1,17 @@ +import { Long } from "./protobuf"; + +describe("Long", () => { + it("converts Long.js objects to uint64 strings", () => { + expect(Long.toUint64({ low: -72998656, high: 412955876 })).toBe("1773631986332999936"); + }); + + it("leaves plain values unchanged", () => { + expect(Long.deepConvert(42)).toBe(42); + expect(Long.deepConvert("hello")).toBe("hello"); + }); + + it("recurses into nested objects", () => { + const input = { a: { low: 1, high: 0 }, b: "keep" }; + expect(Long.deepConvert(input)).toEqual({ a: "1", b: "keep" }); + }); +}); diff --git a/packages/swapper/src/protobuf.ts b/packages/swapper/src/protobuf.ts new file mode 100644 index 0000000..1d11d61 --- /dev/null +++ b/packages/swapper/src/protobuf.ts @@ -0,0 +1,25 @@ +interface LongValue { + low: number; + high: number; +} + +function isLong(v: unknown): v is LongValue { + return typeof v === "object" && v !== null && "low" in v && "high" in v; +} + +export const Long = { + toUint64(l: LongValue): string { + const low = l.low >>> 0; + const high = l.high >>> 0; + return (BigInt(high) * BigInt(0x100000000) + BigInt(low)).toString(); + }, + + deepConvert(obj: unknown): unknown { + if (isLong(obj)) return this.toUint64(obj); + if (Array.isArray(obj)) return obj.map((v) => this.deepConvert(v)); + if (typeof obj === "object" && obj !== null) { + return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.deepConvert(v)])); + } + return obj; + }, +}; diff --git a/packages/swapper/src/squid/client.ts b/packages/swapper/src/squid/client.ts new file mode 100644 index 0000000..447fd9e --- /dev/null +++ b/packages/swapper/src/squid/client.ts @@ -0,0 +1,39 @@ +import { SwapperException } from "../error"; +import type { SquidRouteRequest, SquidRouteResponse, SquidErrorResponse } from "./model"; + +const SQUID_API_BASE_URL = "https://v2.api.squidrouter.com"; + +export async function fetchRoute(params: SquidRouteRequest, integratorId: string): Promise { + const response = await fetch(`${SQUID_API_BASE_URL}/v2/route`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-integrator-id": integratorId, + }, + body: JSON.stringify(params), + }); + const responseText = await response.text(); + + if (!response.ok) { + let detail: string; + try { + const errorBody = JSON.parse(responseText) as SquidErrorResponse; + detail = errorBody.errors?.[0]?.message || errorBody.message || response.statusText; + } catch { + detail = responseText || response.statusText; + } + throw new SwapperException({ + type: "compute_quote_error", + message: `Squid API error ${response.status}: ${detail}`, + }); + } + + try { + return JSON.parse(responseText) as SquidRouteResponse; + } catch { + throw new SwapperException({ + type: "compute_quote_error", + message: "Squid API returned an invalid response", + }); + } +} diff --git a/packages/swapper/src/squid/index.ts b/packages/swapper/src/squid/index.ts new file mode 100644 index 0000000..eed8486 --- /dev/null +++ b/packages/swapper/src/squid/index.ts @@ -0,0 +1,3 @@ +export * from "./model"; +export * from "./client"; +export * from "./provider"; diff --git a/packages/swapper/src/squid/integration.test.ts b/packages/swapper/src/squid/integration.test.ts new file mode 100644 index 0000000..0f646f2 --- /dev/null +++ b/packages/swapper/src/squid/integration.test.ts @@ -0,0 +1,65 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +require("dotenv").config({ path: "../../.env" }); + +import { BigIntMath } from "../bigint_math"; +import { timedPromise } from "../debug"; +import { SQUID_ATOM_TO_OSMO_REQUEST, SQUID_OSMO_TO_ATOM_REQUEST, OSMOSIS_TEST_ADDRESS } from "../testkit/mock"; + +const runIntegration = process.env.INTEGRATION_TEST === "1"; +const describeIntegration = runIntegration ? describe : describe.skip; + +describeIntegration("Squid live integration", () => { + jest.setTimeout(60_000); + + let provider: import("./provider").SquidProvider; + + beforeAll(async () => { + const integratorId = process.env.SQUID_INTEGRATOR_ID; + if (!integratorId) { + throw new Error("Missing required environment variable: SQUID_INTEGRATOR_ID"); + } + const { SquidProvider } = await import("./provider"); + provider = new SquidProvider(integratorId); + }); + + it("fetches a live quote for 10 OSMO -> ATOM", async () => { + const quote = await timedPromise("squid quote OSMO->ATOM", provider.get_quote(SQUID_OSMO_TO_ATOM_REQUEST)); + + expect(BigInt(quote.output_value) > 0).toBe(true); + expect(quote.output_value).toMatch(/^\d+$/); + expect(quote.output_min_value).toMatch(/^\d+$/); + expect(quote.eta_in_seconds).toBeGreaterThan(0); + + const outputValue = BigIntMath.formatDecimals(quote.output_value, 6); + console.log("Squid 10 OSMO -> ATOM output:", outputValue); + console.log("Squid ETA:", quote.eta_in_seconds, "seconds"); + }); + + it("fetches quote_data for OSMO -> ATOM (MsgExecuteContract)", async () => { + const quote = await provider.get_quote(SQUID_OSMO_TO_ATOM_REQUEST); + const data = await provider.get_quote_data(quote); + + expect(data.dataType).toBe("contract"); + expect(data.gasLimit).toBeTruthy(); + + const msg = JSON.parse(data.data); + expect(msg.typeUrl).toBe("/cosmwasm.wasm.v1.MsgExecuteContract"); + expect(msg.value.contract).toBeTruthy(); + expect(msg.value.sender).toBe(OSMOSIS_TEST_ADDRESS); + console.log("OSMO->ATOM data:", JSON.stringify(msg, null, 2)); + }); + + it("fetches quote_data for ATOM -> OSMO (MsgTransfer)", async () => { + const quote = await provider.get_quote(SQUID_ATOM_TO_OSMO_REQUEST); + const data = await provider.get_quote_data(quote); + + expect(data.dataType).toBe("contract"); + expect(data.gasLimit).toBeTruthy(); + + const msg = JSON.parse(data.data); + expect(msg.typeUrl).toBe("/ibc.applications.transfer.v1.MsgTransfer"); + expect(msg.value.sourcePort).toBe("transfer"); + expect(typeof msg.value.timeoutTimestamp).toBe("string"); + console.log("ATOM->OSMO data:", JSON.stringify(msg, null, 2)); + }); +}); diff --git a/packages/swapper/src/squid/model.ts b/packages/swapper/src/squid/model.ts new file mode 100644 index 0000000..e19baa6 --- /dev/null +++ b/packages/swapper/src/squid/model.ts @@ -0,0 +1,41 @@ +export interface SquidRouteRequest { + fromChain: string; + toChain: string; + fromToken: string; + toToken: string; + fromAmount: string; + fromAddress: string; + toAddress: string; + slippageConfig: { + autoMode: number; + }; + quoteOnly?: boolean; +} + +export interface SquidEstimate { + toAmount: string; + toAmountMin: string; + estimatedRouteDuration: number; +} + +export interface SquidTransactionRequest { + target: string; + data: string; + value: string; + gasLimit: string; + gasPrice: string; +} + +export interface SquidRoute { + estimate: SquidEstimate; + transactionRequest?: SquidTransactionRequest; +} + +export interface SquidRouteResponse { + route: SquidRoute; +} + +export interface SquidErrorResponse { + errors?: { message: string }[]; + message?: string; +} diff --git a/packages/swapper/src/squid/provider.test.ts b/packages/swapper/src/squid/provider.test.ts new file mode 100644 index 0000000..b9ee179 --- /dev/null +++ b/packages/swapper/src/squid/provider.test.ts @@ -0,0 +1,169 @@ +import { AssetId, Chain } from "@gemwallet/types"; + +import { createQuoteRequest, OSMOSIS_TEST_ADDRESS, SQUID_OSMO_TO_ATOM_REQUEST } from "../testkit/mock"; +import { SquidProvider } from "./provider"; + +describe("SquidProvider", () => { + const provider = new SquidProvider("test-integrator"); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("mapChainToSquidChainId", () => { + it("maps supported Cosmos chains", () => { + expect(provider.mapChainToSquidChainId(Chain.Cosmos)).toBe("cosmoshub-4"); + expect(provider.mapChainToSquidChainId(Chain.Osmosis)).toBe("osmosis-1"); + expect(provider.mapChainToSquidChainId(Chain.Celestia)).toBe("celestia"); + expect(provider.mapChainToSquidChainId(Chain.Injective)).toBe("injective-1"); + expect(provider.mapChainToSquidChainId(Chain.Sei)).toBe("pacific-1"); + expect(provider.mapChainToSquidChainId(Chain.Noble)).toBe("noble-1"); + }); + + it("throws for unsupported chains", () => { + expect(() => provider.mapChainToSquidChainId(Chain.Solana)).toThrow(); + }); + }); + + describe("mapAssetToSquidToken", () => { + it("maps native Cosmos assets to denoms", () => { + expect(provider.mapAssetToSquidToken(AssetId.fromString("cosmos"))).toBe("uatom"); + expect(provider.mapAssetToSquidToken(AssetId.fromString("osmosis"))).toBe("uosmo"); + expect(provider.mapAssetToSquidToken(AssetId.fromString("celestia"))).toBe("utia"); + expect(provider.mapAssetToSquidToken(AssetId.fromString("noble"))).toBe("uusdc"); + }); + + it("returns tokenId for non-native assets", () => { + const ibc = "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"; + expect(provider.mapAssetToSquidToken(AssetId.fromString(`osmosis_${ibc}`))).toBe(ibc); + }); + }); + + describe("get_quote", () => { + it("constructs a valid quote request", async () => { + const mockRoute = { + route: { + estimate: { + toAmount: "500000", + toAmountMin: "495000", + estimatedRouteDuration: 60, + }, + transactionRequest: {}, + params: {}, + }, + }; + + const fetchSpy = jest.spyOn(global, "fetch").mockResolvedValueOnce({ + ok: true, + text: async () => JSON.stringify(mockRoute), + } as Response); + + const request = createQuoteRequest(SQUID_OSMO_TO_ATOM_REQUEST); + const quote = await provider.get_quote(request); + + expect(quote.output_value).toBe("500000"); + expect(quote.output_min_value).toBe("495000"); + expect(quote.eta_in_seconds).toBe(60); + expect(quote.route_data).toEqual({}); + + const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string); + expect(body.fromChain).toBe("osmosis-1"); + expect(body.toChain).toBe("cosmoshub-4"); + expect(body.fromToken).toBe("uosmo"); + expect(body.toToken).toBe("uatom"); + expect(body.quoteOnly).toBe(true); + }); + }); + + describe("get_quote_data", () => { + it("fetches and returns cosmos transaction data", async () => { + const cosmosMsg = JSON.stringify({ + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: { + sender: OSMOSIS_TEST_ADDRESS, + contract: "osmo1squid_multicall_contract", + msg: "{}", + funds: [{ denom: "uosmo", amount: "1000000" }], + }, + }); + + const mockRoute = { + route: { + estimate: { + toAmount: "500000", + toAmountMin: "495000", + estimatedRouteDuration: 60, + }, + transactionRequest: { + routeType: "COSMOS_ONLY", + target: "", + data: cosmosMsg, + value: "0", + gasLimit: "500000", + gasPrice: "0.025uosmo", + }, + params: {}, + }, + }; + + jest.spyOn(global, "fetch").mockResolvedValueOnce({ + ok: true, + text: async () => JSON.stringify(mockRoute), + } as Response); + + const quote = { + quote: createQuoteRequest(SQUID_OSMO_TO_ATOM_REQUEST), + output_value: "500000", + output_min_value: "495000", + route_data: mockRoute.route, + eta_in_seconds: 60, + }; + + const data = await provider.get_quote_data(quote); + + expect(JSON.parse(data.data)).toEqual(JSON.parse(cosmosMsg)); + expect(data.gasLimit).toBe("500000"); + expect(data.dataType).toBe("contract"); + }); + + it("normalizes Long.js timeoutTimestamp to string", async () => { + const dataWithLong = JSON.stringify({ + typeUrl: "/ibc.applications.transfer.v1.MsgTransfer", + value: { + sourcePort: "transfer", + sourceChannel: "channel-141", + token: { denom: "uatom", amount: "1000000" }, + sender: "cosmos1test", + receiver: "osmo1test", + timeoutTimestamp: { low: -72998656, high: 412955876, unsigned: false }, + memo: "", + }, + }); + + const mockRoute = { + route: { + estimate: { toAmount: "500000", toAmountMin: "495000", estimatedRouteDuration: 60 }, + transactionRequest: { target: "", data: dataWithLong, value: "0", gasLimit: "500000", gasPrice: "0.03uatom" }, + }, + }; + + jest.spyOn(global, "fetch").mockResolvedValueOnce({ + ok: true, + text: async () => JSON.stringify(mockRoute), + } as Response); + + const quote = { + quote: createQuoteRequest(SQUID_OSMO_TO_ATOM_REQUEST), + output_value: "500000", + output_min_value: "495000", + route_data: mockRoute.route, + eta_in_seconds: 60, + }; + + const data = await provider.get_quote_data(quote); + const parsed = JSON.parse(data.data); + + expect(parsed.value.timeoutTimestamp).toBe("1773631986332999936"); + }); + }); +}); diff --git a/packages/swapper/src/squid/provider.ts b/packages/swapper/src/squid/provider.ts new file mode 100644 index 0000000..0b97a2b --- /dev/null +++ b/packages/swapper/src/squid/provider.ts @@ -0,0 +1,112 @@ +import { QuoteRequest, Quote, SwapQuoteData, AssetId, Chain, SwapQuoteDataType } from "@gemwallet/types"; + +import { resolveCosmosMaxAmount } from "../cosmos_fee"; +import { SwapperException } from "../error"; +import { Protocol } from "../protocol"; +import { Long } from "../protobuf"; +import { fetchRoute } from "./client"; +import type { SquidRouteRequest } from "./model"; + +export class SquidProvider implements Protocol { + private readonly integratorId: string; + + constructor(integratorId?: string) { + const resolvedIntegratorId = integratorId ?? process.env.SQUID_INTEGRATOR_ID; + if (!resolvedIntegratorId) { + throw new Error("Squid integrator ID is required"); + } + this.integratorId = resolvedIntegratorId; + } + + mapChainToSquidChainId(chain: Chain): string { + switch (chain) { + case Chain.Cosmos: + return "cosmoshub-4"; + case Chain.Osmosis: + return "osmosis-1"; + case Chain.Celestia: + return "celestia"; + case Chain.Injective: + return "injective-1"; + case Chain.Sei: + return "pacific-1"; + case Chain.Noble: + return "noble-1"; + default: + throw new SwapperException({ type: "not_supported_chain" }); + } + } + + mapAssetToSquidToken(assetId: AssetId): string { + if (assetId.isNative()) { + switch (assetId.chain) { + case Chain.Cosmos: + return "uatom"; + case Chain.Osmosis: + return "uosmo"; + case Chain.Celestia: + return "utia"; + case Chain.Injective: + return "inj"; + case Chain.Sei: + return "usei"; + case Chain.Noble: + return "uusdc"; + default: + throw new SwapperException({ type: "not_supported_asset" }); + } + } + return assetId.getTokenId(); + } + + private buildRouteRequest(quoteRequest: QuoteRequest, quoteOnly: boolean): SquidRouteRequest { + const fromAsset = AssetId.fromString(quoteRequest.from_asset.id); + const toAsset = AssetId.fromString(quoteRequest.to_asset.id); + + return { + fromChain: this.mapChainToSquidChainId(fromAsset.chain), + toChain: this.mapChainToSquidChainId(toAsset.chain), + fromToken: this.mapAssetToSquidToken(fromAsset), + toToken: this.mapAssetToSquidToken(toAsset), + fromAmount: resolveCosmosMaxAmount(quoteRequest), + fromAddress: quoteRequest.from_address, + toAddress: quoteRequest.to_address, + slippageConfig: { + autoMode: 1, + }, + quoteOnly, + }; + } + + async get_quote(quoteRequest: QuoteRequest): Promise { + const params = this.buildRouteRequest(quoteRequest, true); + const { route } = await fetchRoute(params, this.integratorId); + + return { + quote: quoteRequest, + output_value: route.estimate.toAmount, + output_min_value: route.estimate.toAmountMin, + route_data: {}, + eta_in_seconds: route.estimate.estimatedRouteDuration, + }; + } + + async get_quote_data(quote: Quote): Promise { + const { route } = await fetchRoute(this.buildRouteRequest(quote.quote, false), this.integratorId); + const tx = route.transactionRequest; + + if (!tx) { + throw new SwapperException({ type: "invalid_route" }); + } + + const data = JSON.stringify(Long.deepConvert(JSON.parse(tx.data))); + + return { + to: tx.target, + value: tx.value, + data, + dataType: SwapQuoteDataType.Contract, + gasLimit: tx.gasLimit, + }; + } +} diff --git a/packages/swapper/src/testkit/mock.ts b/packages/swapper/src/testkit/mock.ts index c5ae0db..bde052f 100644 --- a/packages/swapper/src/testkit/mock.ts +++ b/packages/swapper/src/testkit/mock.ts @@ -174,3 +174,29 @@ export function createSolanaUsdcQuoteRequest(overrides: Partial = export function createAptosUsdcQuoteRequest(overrides: Partial = {}): QuoteRequest { return createQuoteRequest(APTOS_USDC_REQUEST_TEMPLATE, overrides); } + +export const COSMOS_TEST_ADDRESS = "cosmos1tkvyjqeq204rmrrz3w4hcrs336qahsfwmugljt"; +export const OSMOSIS_TEST_ADDRESS = "osmo1tkvyjqeq204rmrrz3w4hcrs336qahsfwn8m0ye"; + +export const ATOM_ASSET = { id: Chain.Cosmos, symbol: "ATOM", decimals: 6 }; +export const OSMO_ASSET = { id: Chain.Osmosis, symbol: "OSMO", decimals: 6 }; + +export const SQUID_OSMO_TO_ATOM_REQUEST: QuoteRequest = { + from_address: OSMOSIS_TEST_ADDRESS, + to_address: COSMOS_TEST_ADDRESS, + from_asset: OSMO_ASSET, + to_asset: ATOM_ASSET, + from_value: "10000000", + referral_bps: 0, + slippage_bps: 100, +}; + +export const SQUID_ATOM_TO_OSMO_REQUEST: QuoteRequest = { + from_address: COSMOS_TEST_ADDRESS, + to_address: OSMOSIS_TEST_ADDRESS, + from_asset: ATOM_ASSET, + to_asset: OSMO_ASSET, + from_value: "1000000", + referral_bps: 0, + slippage_bps: 100, +};