@riftresearch/sdk 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,15 +16,15 @@ Then use it:
16
16
  import { RiftSdk, Currencies, createCurrency } from '@riftresearch/sdk'
17
17
  import { createPublicClient, createWalletClient, http } from 'viem'
18
18
  import { privateKeyToAccount } from 'viem/accounts'
19
- import { ethereum } from 'viem/chains'
19
+ import { mainnet } from 'viem/chains'
20
20
 
21
21
 
22
22
  // Setup viem clients
23
23
  const account = privateKeyToAccount('0x...')
24
- const publicClient = createPublicClient({ chain: ethereum, transport: http(process.env.ETH_RPC) })
24
+ const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.ETH_RPC) })
25
25
  const walletClient = createWalletClient({
26
26
  account,
27
- chain: ethereum,
27
+ chain: mainnet,
28
28
  transport: http(process.env.ETH_RPC),
29
29
  })
30
30
 
@@ -67,3 +67,37 @@ console.log(`Swap ID: ${swap.swapId}`)
67
67
  const status = await sdk.getSwapStatus(swap.swapId)
68
68
  console.log(`Status: ${status.status}`)
69
69
  ```
70
+
71
+ ## Limit Orders
72
+
73
+ You can place limit orders with a fixed price instead of using a market quote:
74
+
75
+ ```ts
76
+ import { RiftSdk, Currencies } from '@riftresearch/sdk'
77
+ import { createPublicClient, createWalletClient, http } from 'viem'
78
+ import { base } from 'viem/chains'
79
+ import { privateKeyToAccount } from 'viem/accounts'
80
+
81
+ const account = privateKeyToAccount('0x...')
82
+ const publicClient = createPublicClient({ chain: base, transport: http() })
83
+ const walletClient = createWalletClient({ account, chain: base, transport: http() })
84
+
85
+ const sdk = new RiftSdk({ integratorName: 'my-app' })
86
+
87
+ const result = await sdk.createLimitOrder({
88
+ from: Currencies.Base.USDC,
89
+ to: Currencies.Bitcoin.BTC,
90
+ pricing: {
91
+ sellAmount: '1000000', // 1 USDC (6 decimals)
92
+ buyAmount: '2500', // 2500 sats
93
+ },
94
+ destinationAddress: 'bc1q...',
95
+ refundAddress: '0x...',
96
+ walletClient,
97
+ publicClient,
98
+ validUntil: Math.floor(Date.now() / 1000) + 3600, // optional: order expires in 1 hour
99
+ })
100
+
101
+ console.log(`Order ID: ${result.swapId}`)
102
+ console.log(`Status: ${result.status}`)
103
+ ```
package/dist/index.d.ts CHANGED
@@ -128,6 +128,10 @@ interface ExactOutputQuoteResponse extends QuoteResponseBase {
128
128
  * Use `mode` to determine which fields are specified vs calculated.
129
129
  */
130
130
  type QuoteResponse = ExactInputQuoteResponse | ExactOutputQuoteResponse;
131
+ interface LimitPricing {
132
+ buyAmount: U256;
133
+ sellAmount: U256;
134
+ }
131
135
  declare const SWAP_STATUSES: readonly ["waiting_for_deposit", "deposit_confirming", "initiating_payout", "confirming_payout", "swap_complete", "refunding_user", "failed"];
132
136
  type SwapStatus = (typeof SWAP_STATUSES)[number];
133
137
  interface SwapStatusResponse {
@@ -307,6 +311,7 @@ interface SupportedModes {
307
311
  * - EVM → EVM (same chain): both modes supported
308
312
  */
309
313
  declare function getSupportedModes(from: Currency, to: Currency): SupportedModes;
314
+ import { Chain as Chain3 } from "viem";
310
315
  import { Account, PublicClient, Transport, WalletClient } from "viem";
311
316
  import { Chain as Chain2 } from "viem/chains";
312
317
  type RiftSwap = SwapStatusResponse;
@@ -405,6 +410,16 @@ type SendBitcoinFn = (params: {
405
410
  amountSats: string;
406
411
  }) => Promise<void>;
407
412
  type ExecuteSwapStepType = "approval" | "transaction" | "signature";
413
+ type CreateLimitOrderOptions<chain extends Chain2 | undefined = Chain2 | undefined> = ExecuteSwapContext<chain> & {
414
+ from: Currency;
415
+ to: Currency;
416
+ pricing: LimitPricing;
417
+ destinationAddress: string;
418
+ refundAddress?: string;
419
+ approvalMode?: "full" | "partial";
420
+ /** Absolute Unix timestamp in UTC seconds when the limit order expires. Defaults to now + 1 year if not provided. */
421
+ validUntil?: number;
422
+ };
408
423
  type ExecuteSwapOnExecuteStepCallback = (type: ExecuteSwapStepType) => void | Promise<void>;
409
424
  type ExecuteSwapContext<chain extends Chain2 | undefined = Chain2 | undefined> = {
410
425
  /** Viem PublicClient for reading chain data */
@@ -467,6 +482,10 @@ declare class RiftSdk {
467
482
  * })
468
483
  */
469
484
  getQuote(params: QuoteParameters): Promise<GetQuoteResult>;
485
+ createLimitOrder<chain extends Chain3 | undefined = Chain3 | undefined>(options: CreateLimitOrderOptions<chain>): Promise<SwapResult>;
486
+ private assertPositiveIntegerString;
487
+ private assertValidLimitValidUntil;
488
+ private executeOrderFlow;
470
489
  /**
471
490
  * Execute a single step from the server's execution steps.
472
491
  * Dispatch based on `action` (the execution mechanism).
@@ -502,4 +521,4 @@ declare class RiftSdk {
502
521
  getSwapStatus(swapId: string): Promise<SwapStatusResponse>;
503
522
  }
504
523
  declare function createRiftSdk(options: RiftSdkOptions): RiftSdk;
505
- export { getSupportedModes, detectRoute, createRiftSdk, createCurrency, TokenIdentifier, SwapStatus, SwapRouterApiError, SwapRoute, SwapResult, SwapResponse, SupportedModes, SendBitcoinFn, RiftSwap, RiftSdkOptions, RiftSdk, QuoteResult, QuoteParameters, NativeToken, GetQuoteResult, ExecutionStep, ExecutionAction, ExecuteSwapStepType, ExecuteSwapOptions, ExecuteSwapOnExecuteStepCallback, EvmChain, EvmCallStep, EvmCallKind, Erc20Token, Currency, Currencies, Chain, BtcTransferStep, BtcTransferKind, BitcoinChain };
524
+ export { getSupportedModes, detectRoute, createRiftSdk, createCurrency, TokenIdentifier, SwapStatus, SwapRouterApiError, SwapRoute, SwapResult, SwapResponse, SupportedModes, SendBitcoinFn, RiftSwap, RiftSdkOptions, RiftSdk, QuoteResult, QuoteParameters, NativeToken, LimitPricing, GetQuoteResult, ExecutionStep, ExecutionAction, ExecuteSwapStepType, ExecuteSwapOptions, ExecuteSwapOnExecuteStepCallback, EvmChain, EvmCallStep, EvmCallKind, Erc20Token, Currency, Currencies, CreateLimitOrderOptions, Chain, BtcTransferStep, BtcTransferKind, BitcoinChain };
package/dist/index.js CHANGED
@@ -153,7 +153,7 @@ function getSupportedModes(from, to) {
153
153
  return { exactInput: true, exactOutput: false };
154
154
  }
155
155
  // src/sdk.ts
156
- import { erc20Abi } from "viem";
156
+ import { erc20Abi, isAddress } from "viem";
157
157
 
158
158
  // src/client.ts
159
159
  async function request(baseUrl, path, init) {
@@ -225,20 +225,28 @@ function createClient(baseUrl) {
225
225
  const swap = (params) => {
226
226
  const swapId = encodeURIComponent(params.swapId);
227
227
  return {
228
- get: () => get(normalizedBaseUrl, `/swap/${swapId}`),
228
+ get: () => get(normalizedBaseUrl, `/order/${swapId}`),
229
229
  tx: {
230
- post: (body) => postJson(normalizedBaseUrl, `/swap/${swapId}/tx`, body)
230
+ post: (body) => postJson(normalizedBaseUrl, `/order/${swapId}/tx`, body)
231
231
  },
232
232
  "refresh-step": {
233
- post: (body) => postJson(normalizedBaseUrl, `/swap/${swapId}/refresh-step`, body)
233
+ post: (body) => postJson(normalizedBaseUrl, `/order/${swapId}/refresh-step`, body)
234
234
  }
235
235
  };
236
236
  };
237
237
  swap.post = (body) => postJson(normalizedBaseUrl, "/swap", body);
238
+ const market = {
239
+ post: (body) => postJson(normalizedBaseUrl, "/order/market", body)
240
+ };
241
+ const limit = {
242
+ post: (body) => postJson(normalizedBaseUrl, "/order/limit", body)
243
+ };
238
244
  return {
239
245
  quote: {
240
246
  post: (body) => postJson(normalizedBaseUrl, "/quote", body)
241
247
  },
248
+ market,
249
+ limit,
242
250
  swap
243
251
  };
244
252
  }
@@ -247,6 +255,10 @@ function createClient(baseUrl) {
247
255
  var GAS_LIMIT_MULTIPLIER_NUMERATOR = 3n;
248
256
  var GAS_LIMIT_MULTIPLIER_DENOMINATOR = 2n;
249
257
  var GPV2_SETTLEMENT = "0x9008d19f58aabd9ed0d60971565aa8510560ab41";
258
+ var DEFAULT_LIMIT_VALIDITY_WINDOW_SECONDS = 365 * 24 * 60 * 60;
259
+ var MIN_LIMIT_VALIDITY_LEAD_TIME_SECONDS = 60;
260
+ var MAX_LIMIT_VALIDITY_WINDOW_SECONDS = DEFAULT_LIMIT_VALIDITY_WINDOW_SECONDS;
261
+ var MAX_COW_VALID_TO = 4294967295;
250
262
 
251
263
  class RiftSdk {
252
264
  riftClient;
@@ -295,7 +307,6 @@ class RiftSdk {
295
307
  throw new Error("slippageBps must be between 0 and 10000");
296
308
  }
297
309
  const route = detectRoute(params.from, params.to);
298
- const isMonochain = route.type === "dex_monochain";
299
310
  const quoteRequest = {
300
311
  type: params.mode === "exact_input" ? "EXACT_INPUT" : "EXACT_OUTPUT",
301
312
  from: params.from,
@@ -328,60 +339,177 @@ class RiftSdk {
328
339
  this.logDebug("running preflight balance check");
329
340
  await this.assertSufficientBalance(params.from, quote.from.expected, context);
330
341
  }
331
- this.logDebug("creating swap", { quoteId: riftQuote.id });
332
- const senderAddress = params.from.chain.kind === "EVM" ? this.getAddress(context) : undefined;
333
- const swapResponse = this.unwrapEdenResult(await this.riftClient.swap.post({
334
- id: riftQuote.id,
335
- ...senderAddress ? { senderAddress } : {},
336
- destinationAddress: context.destinationAddress,
337
- refundAddress,
338
- integratorName: this.integratorName,
339
- approvalMode: params.approvalMode
340
- }));
341
- this.logDebug("swap created", {
342
- swapId: swapResponse.swapId,
343
- steps: swapResponse.executionSteps.length
344
- });
345
- this.assertEvmChainMatchForSteps(swapResponse.executionSteps, context);
346
- for (const step of swapResponse.executionSteps) {
347
- this.logDebug("executing step", {
348
- stepId: step.id,
349
- action: step.action,
350
- kind: "kind" in step ? step.kind : undefined,
351
- chainId: "chainId" in step ? step.chainId : undefined
352
- });
353
- const result = await this.executeStep(step, context, swapResponse.swapId, route);
354
- this.logDebug("step completed", {
355
- stepId: step.id,
356
- txHash: result.txHash,
357
- cowswapOrderId: result.cowswapOrderId
358
- });
359
- if (this.shouldReportStepResult(step, result)) {
360
- this.logDebug("reporting step result", {
361
- stepId: step.id,
362
- kind: "kind" in step ? step.kind : undefined,
363
- monochain: isMonochain
364
- });
365
- this.unwrapEdenResult(await this.riftClient.swap({ swapId: swapResponse.swapId }).tx.post({
366
- stepId: step.id,
367
- ...result
342
+ return this.executeOrderFlow({
343
+ context,
344
+ route,
345
+ chained: isChained,
346
+ createOrder: async () => {
347
+ this.logDebug("creating market order", { quoteId: riftQuote.id });
348
+ const senderAddress = params.from.chain.kind === "EVM" ? this.getAddress(context) : undefined;
349
+ return this.unwrapEdenResult(await this.riftClient.market.post({
350
+ id: riftQuote.id,
351
+ ...senderAddress ? { senderAddress } : {},
352
+ destinationAddress: context.destinationAddress,
353
+ refundAddress,
354
+ integratorName: this.integratorName,
355
+ approvalMode: params.approvalMode
368
356
  }));
369
357
  }
370
- }
371
- this.logDebug("fetching swap status", {
372
- swapId: swapResponse.swapId
373
358
  });
374
- const swap = this.unwrapEdenResult(await this.riftClient.swap({ swapId: swapResponse.swapId }).get());
375
- this.logDebug("swap fetched", {
376
- swapId: swapResponse.swapId,
377
- status: swap.status
359
+ }
360
+ };
361
+ }
362
+ async createLimitOrder(options) {
363
+ if (!options?.destinationAddress) {
364
+ throw new Error("destinationAddress is required to create a limit order");
365
+ }
366
+ if (options.from.token.kind === "TOKEN") {
367
+ if (!isAddress(options.from.token.address)) {
368
+ throw new Error("from.token.address must be a valid EVM token address");
369
+ }
370
+ }
371
+ if (options.to.token.kind === "TOKEN" && !isAddress(options.to.token.address)) {
372
+ throw new Error("to.token.address must be a valid EVM token address");
373
+ }
374
+ this.assertPositiveIntegerString(options.pricing.buyAmount, "pricing.buyAmount");
375
+ this.assertPositiveIntegerString(options.pricing.sellAmount, "pricing.sellAmount");
376
+ if (options.validUntil !== undefined) {
377
+ this.assertValidLimitValidUntil(options.validUntil, "validUntil");
378
+ }
379
+ let refundAddress;
380
+ let senderAddress;
381
+ if (options.from.chain.kind === "BITCOIN") {
382
+ if (options.from.token.kind !== "NATIVE") {
383
+ throw new Error("BTC-start limit orders must use native BTC as the input");
384
+ }
385
+ if (!options.refundAddress) {
386
+ throw new Error("refundAddress is required for BTC-start limit orders (Bitcoin refund address).");
387
+ }
388
+ if (isCbBtc(options.to)) {
389
+ throw new Error("BTC -> cbBTC limit orders are not supported");
390
+ }
391
+ refundAddress = options.refundAddress;
392
+ } else {
393
+ senderAddress = this.getAddress(options);
394
+ refundAddress = options.refundAddress ?? senderAddress;
395
+ if (!isAddress(refundAddress)) {
396
+ throw new Error("refundAddress must be a valid EVM address for EVM-start limit orders");
397
+ }
398
+ }
399
+ if (options.to.chain.kind === "BITCOIN") {
400
+ if (options.from.chain.kind !== "EVM") {
401
+ throw new Error("Bitcoin-payout limit orders require an EVM source currency.");
402
+ }
403
+ if (isCbBtc(options.from)) {
404
+ throw new Error("cbBTC -> BTC limit orders are not supported");
405
+ }
406
+ } else {
407
+ if (!isAddress(options.destinationAddress)) {
408
+ throw new Error("destinationAddress must be a valid EVM address for EVM-destination limit orders");
409
+ }
410
+ if (options.to.chain.chainId !== 1 && options.to.chain.chainId !== 8453) {
411
+ throw new Error(`Unsupported limit order chainId: ${options.to.chain.chainId}. Expected 1 or 8453.`);
412
+ }
413
+ if (options.from.chain.kind === "EVM") {
414
+ if (options.from.chain.chainId !== options.to.chain.chainId) {
415
+ throw new Error("EVM-start limit orders currently require from/to on the same EVM chain.");
416
+ }
417
+ }
418
+ }
419
+ this.logDebug("resolved limit order refund address", { refundAddress });
420
+ const route = detectRoute(options.from, options.to);
421
+ if (this.preflightCheckBalances && options.from.chain.kind === "EVM") {
422
+ this.logDebug("running limit-order preflight balance check");
423
+ await this.assertSufficientBalance(options.from, options.pricing.sellAmount, options);
424
+ }
425
+ return this.executeOrderFlow({
426
+ context: options,
427
+ route,
428
+ chained: false,
429
+ createOrder: async () => {
430
+ const request2 = {
431
+ from: options.from,
432
+ to: options.to,
433
+ ...senderAddress ? { senderAddress } : {},
434
+ pricing: options.pricing,
435
+ destinationAddress: options.destinationAddress,
436
+ refundAddress,
437
+ ...options.approvalMode ? { approvalMode: options.approvalMode } : {},
438
+ ...typeof options.validUntil === "number" ? { validUntil: options.validUntil } : {},
439
+ integratorName: this.integratorName
440
+ };
441
+ this.logDebug("creating limit order", {
442
+ to: request2.to,
443
+ pricing: request2.pricing
378
444
  });
379
- return this.buildSwapResult(swap, {
380
- chained: isChained,
381
- riftSwapId: swapResponse.swapId
445
+ return this.unwrapEdenResult(await this.riftClient.limit.post(request2));
446
+ }
447
+ });
448
+ }
449
+ assertPositiveIntegerString(value, field) {
450
+ if (!/^\d+$/.test(value)) {
451
+ throw new Error(`${field} must be a positive integer string`);
452
+ }
453
+ if (BigInt(value) <= 0n) {
454
+ throw new Error(`${field} must be greater than zero`);
455
+ }
456
+ }
457
+ assertValidLimitValidUntil(validUntil, field) {
458
+ if (!Number.isInteger(validUntil)) {
459
+ throw new Error(`${field} must be an integer when provided`);
460
+ }
461
+ if (validUntil > MAX_COW_VALID_TO) {
462
+ throw new Error(`${field} exceeds the maximum supported Unix timestamp`);
463
+ }
464
+ const nowSeconds = Math.floor(Date.now() / 1000);
465
+ const secondsUntilExpiry = validUntil - nowSeconds;
466
+ if (secondsUntilExpiry < MIN_LIMIT_VALIDITY_LEAD_TIME_SECONDS || secondsUntilExpiry > MAX_LIMIT_VALIDITY_WINDOW_SECONDS) {
467
+ throw new Error(`${field} must be between 60 seconds and 31536000 seconds in the future`);
468
+ }
469
+ }
470
+ async executeOrderFlow(params) {
471
+ const swapResponse = await params.createOrder();
472
+ this.logDebug("order created", {
473
+ swapId: swapResponse.swapId,
474
+ steps: swapResponse.executionSteps.length
475
+ });
476
+ this.assertEvmChainMatchForSteps(swapResponse.executionSteps, params.context);
477
+ for (const step of swapResponse.executionSteps) {
478
+ this.logDebug("executing step", {
479
+ stepId: step.id,
480
+ action: step.action,
481
+ kind: "kind" in step ? step.kind : undefined,
482
+ chainId: "chainId" in step ? step.chainId : undefined
483
+ });
484
+ const result = await this.executeStep(step, params.context, swapResponse.swapId, params.route);
485
+ this.logDebug("step completed", {
486
+ stepId: step.id,
487
+ txHash: result.txHash,
488
+ cowswapOrderId: result.cowswapOrderId
489
+ });
490
+ if (this.shouldReportStepResult(step, result)) {
491
+ this.logDebug("reporting step result", {
492
+ stepId: step.id,
493
+ kind: "kind" in step ? step.kind : undefined
382
494
  });
495
+ this.unwrapEdenResult(await this.riftClient.swap({ swapId: swapResponse.swapId }).tx.post({
496
+ stepId: step.id,
497
+ ...result
498
+ }));
383
499
  }
384
- };
500
+ }
501
+ this.logDebug("fetching swap status", {
502
+ swapId: swapResponse.swapId
503
+ });
504
+ const swap = this.unwrapEdenResult(await this.riftClient.swap({ swapId: swapResponse.swapId }).get());
505
+ this.logDebug("swap fetched", {
506
+ swapId: swapResponse.swapId,
507
+ status: swap.status
508
+ });
509
+ return this.buildSwapResult(swap, {
510
+ chained: params.chained,
511
+ riftSwapId: swapResponse.swapId
512
+ });
385
513
  }
386
514
  async executeStep(step, context, swapId, route) {
387
515
  switch (step.action) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riftresearch/sdk",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "SDK for swapping between bitcoin and evm chains",
5
5
  "license": "MIT",
6
6
  "files": [