@riftresearch/sdk 0.12.2 → 0.14.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
@@ -67,3 +67,50 @@ 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
+ ```
104
+
105
+ For supported limit orders, you can later cancel the order. The SDK will fetch the
106
+ correct typed-data payload from the router, sign it with your wallet, and submit the
107
+ cancellation:
108
+
109
+ ```ts
110
+ const cancel = await sdk.cancelOrder({
111
+ swapId: result.swapId,
112
+ walletClient,
113
+ })
114
+
115
+ console.log(`Cancel accepted: ${cancel.accepted}`)
116
+ ```
package/dist/index.d.ts CHANGED
@@ -241,6 +241,14 @@ interface SwapResponse {
241
241
  /** Ordered list of steps the client must execute */
242
242
  executionSteps: ExecutionStep[];
243
243
  }
244
+ interface CancelOrderResponse {
245
+ /** The public swap ID from the router API. */
246
+ swapId: string;
247
+ /** Whether the downstream cancellation request was accepted. */
248
+ accepted: boolean;
249
+ /** Optional downstream CoW order UID for the cancelled order. */
250
+ cowOrderUid?: string;
251
+ }
244
252
  /** Create an ERC-20 currency on an EVM chain */
245
253
  declare function createCurrency(params: {
246
254
  chainId: number;
@@ -383,6 +391,7 @@ interface SwapResult {
383
391
  status: SwapStatus;
384
392
  rift: RiftSwap;
385
393
  }
394
+ type CancelOrderResult = CancelOrderResponse;
386
395
  /**
387
396
  * Function type for sending Bitcoin.
388
397
  * Implementers provide this function to handle BTC transactions in their app.
@@ -417,7 +426,8 @@ type CreateLimitOrderOptions<chain extends Chain2 | undefined = Chain2 | undefin
417
426
  destinationAddress: string;
418
427
  refundAddress?: string;
419
428
  approvalMode?: "full" | "partial";
420
- validForSeconds?: number;
429
+ /** Absolute Unix timestamp in UTC seconds when the limit order expires. Defaults to now + 1 year if not provided. */
430
+ validUntil?: number;
421
431
  };
422
432
  type ExecuteSwapOnExecuteStepCallback = (type: ExecuteSwapStepType) => void | Promise<void>;
423
433
  type ExecuteSwapContext<chain extends Chain2 | undefined = Chain2 | undefined> = {
@@ -439,6 +449,17 @@ type ExecuteSwapOptions<chain extends Chain2 | undefined = Chain2 | undefined> =
439
449
  /** Address to receive refunds if swap fails. Defaults to source wallet address */
440
450
  refundAddress?: string;
441
451
  };
452
+ type CancelOrderOptions<chain extends Chain2 | undefined = Chain2 | undefined> = {
453
+ swapId: string;
454
+ /** Viem WalletClient for signing the router-prepared cancellation typed data */
455
+ walletClient: WalletClient<Transport, chain, Account | undefined>;
456
+ /**
457
+ * Optional absolute Unix timestamp in UTC seconds for tee-swapper
458
+ * cancellation signatures. Defaults to now + 5 minutes and is ignored for
459
+ * direct CoW-backed cancellations.
460
+ */
461
+ deadline?: number;
462
+ };
442
463
  interface RiftSdkOptions {
443
464
  /** Rift API URL. Defaults to production API */
444
465
  apiUrl?: string;
@@ -483,6 +504,10 @@ declare class RiftSdk {
483
504
  getQuote(params: QuoteParameters): Promise<GetQuoteResult>;
484
505
  createLimitOrder<chain extends Chain3 | undefined = Chain3 | undefined>(options: CreateLimitOrderOptions<chain>): Promise<SwapResult>;
485
506
  private assertPositiveIntegerString;
507
+ private assertValidLimitValidUntil;
508
+ private assertValidCancelDeadline;
509
+ private assertCancelWalletChain;
510
+ private signPreparedCancellation;
486
511
  private executeOrderFlow;
487
512
  /**
488
513
  * Execute a single step from the server's execution steps.
@@ -517,6 +542,14 @@ declare class RiftSdk {
517
542
  * Get the current status of a swap by its ID.
518
543
  */
519
544
  getSwapStatus(swapId: string): Promise<SwapStatusResponse>;
545
+ /**
546
+ * Cancel a supported limit order through the swap router.
547
+ *
548
+ * The SDK first requests the backend-specific typed-data payload from the
549
+ * router, signs it with the provided wallet, and then submits the signed
550
+ * cancellation request back to the router.
551
+ */
552
+ cancelOrder<chain extends Chain3 | undefined = Chain3 | undefined>(options: CancelOrderOptions<chain>): Promise<CancelOrderResult>;
520
553
  }
521
554
  declare function createRiftSdk(options: RiftSdkOptions): RiftSdk;
522
- 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 };
555
+ 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, CancelOrderResult, CancelOrderOptions, BtcTransferStep, BtcTransferKind, BitcoinChain };
package/dist/index.js CHANGED
@@ -226,6 +226,12 @@ function createClient(baseUrl) {
226
226
  const swapId = encodeURIComponent(params.swapId);
227
227
  return {
228
228
  get: () => get(normalizedBaseUrl, `/order/${swapId}`),
229
+ cancel: {
230
+ prepare: {
231
+ post: (body) => postJson(normalizedBaseUrl, `/order/${swapId}/cancel/prepare`, body)
232
+ },
233
+ post: (body) => postJson(normalizedBaseUrl, `/order/${swapId}/cancel`, body)
234
+ },
229
235
  tx: {
230
236
  post: (body) => postJson(normalizedBaseUrl, `/order/${swapId}/tx`, body)
231
237
  },
@@ -255,6 +261,11 @@ function createClient(baseUrl) {
255
261
  var GAS_LIMIT_MULTIPLIER_NUMERATOR = 3n;
256
262
  var GAS_LIMIT_MULTIPLIER_DENOMINATOR = 2n;
257
263
  var GPV2_SETTLEMENT = "0x9008d19f58aabd9ed0d60971565aa8510560ab41";
264
+ var DEFAULT_LIMIT_VALIDITY_WINDOW_SECONDS = 365 * 24 * 60 * 60;
265
+ var MIN_LIMIT_VALIDITY_LEAD_TIME_SECONDS = 60;
266
+ var MAX_LIMIT_VALIDITY_WINDOW_SECONDS = DEFAULT_LIMIT_VALIDITY_WINDOW_SECONDS;
267
+ var MAX_COW_VALID_TO = 4294967295;
268
+ var CANCEL_AUTH_WINDOW_SECONDS = 300;
258
269
 
259
270
  class RiftSdk {
260
271
  riftClient;
@@ -369,8 +380,8 @@ class RiftSdk {
369
380
  }
370
381
  this.assertPositiveIntegerString(options.pricing.buyAmount, "pricing.buyAmount");
371
382
  this.assertPositiveIntegerString(options.pricing.sellAmount, "pricing.sellAmount");
372
- if (options.validForSeconds !== undefined && (!Number.isInteger(options.validForSeconds) || options.validForSeconds <= 0)) {
373
- throw new Error("validForSeconds must be a positive integer when provided");
383
+ if (options.validUntil !== undefined) {
384
+ this.assertValidLimitValidUntil(options.validUntil, "validUntil");
374
385
  }
375
386
  let refundAddress;
376
387
  let senderAddress;
@@ -431,7 +442,7 @@ class RiftSdk {
431
442
  destinationAddress: options.destinationAddress,
432
443
  refundAddress,
433
444
  ...options.approvalMode ? { approvalMode: options.approvalMode } : {},
434
- ...typeof options.validForSeconds === "number" ? { validForSeconds: options.validForSeconds } : {},
445
+ ...typeof options.validUntil === "number" ? { validUntil: options.validUntil } : {},
435
446
  integratorName: this.integratorName
436
447
  };
437
448
  this.logDebug("creating limit order", {
@@ -450,6 +461,64 @@ class RiftSdk {
450
461
  throw new Error(`${field} must be greater than zero`);
451
462
  }
452
463
  }
464
+ assertValidLimitValidUntil(validUntil, field) {
465
+ if (!Number.isInteger(validUntil)) {
466
+ throw new Error(`${field} must be an integer when provided`);
467
+ }
468
+ if (validUntil > MAX_COW_VALID_TO) {
469
+ throw new Error(`${field} exceeds the maximum supported Unix timestamp`);
470
+ }
471
+ const nowSeconds = Math.floor(Date.now() / 1000);
472
+ const secondsUntilExpiry = validUntil - nowSeconds;
473
+ if (secondsUntilExpiry < MIN_LIMIT_VALIDITY_LEAD_TIME_SECONDS || secondsUntilExpiry > MAX_LIMIT_VALIDITY_WINDOW_SECONDS) {
474
+ throw new Error(`${field} must be between 60 seconds and 31536000 seconds in the future`);
475
+ }
476
+ }
477
+ assertValidCancelDeadline(deadline) {
478
+ if (!Number.isInteger(deadline)) {
479
+ throw new Error("deadline must be an integer when provided");
480
+ }
481
+ const nowSeconds = Math.floor(Date.now() / 1000);
482
+ if (deadline < nowSeconds) {
483
+ throw new Error("deadline must be in the future");
484
+ }
485
+ if (deadline > nowSeconds + CANCEL_AUTH_WINDOW_SECONDS) {
486
+ throw new Error(`deadline must be within ${CANCEL_AUTH_WINDOW_SECONDS} seconds`);
487
+ }
488
+ }
489
+ assertCancelWalletChain(walletClient, chainId) {
490
+ const walletChainId = walletClient.chain?.id;
491
+ if (!walletChainId) {
492
+ throw new Error("Wallet client is missing an EVM chain configuration");
493
+ }
494
+ if (walletChainId !== chainId) {
495
+ throw new Error(`Wallet client chain mismatch. Expected ${chainId}, got ${walletChainId}`);
496
+ }
497
+ }
498
+ async signPreparedCancellation(walletClient, account, preparation) {
499
+ if (preparation.kind === "tee_swapper") {
500
+ return walletClient.signTypedData({
501
+ account,
502
+ domain: preparation.typedData.domain,
503
+ types: preparation.typedData.types,
504
+ primaryType: preparation.typedData.primaryType,
505
+ message: {
506
+ swapId: preparation.typedData.message.swapId,
507
+ deadline: BigInt(preparation.typedData.message.deadline)
508
+ }
509
+ });
510
+ }
511
+ return walletClient.signTypedData({
512
+ account,
513
+ domain: {
514
+ ...preparation.typedData.domain,
515
+ verifyingContract: preparation.typedData.domain.verifyingContract
516
+ },
517
+ types: preparation.typedData.types,
518
+ primaryType: preparation.typedData.primaryType,
519
+ message: preparation.typedData.message
520
+ });
521
+ }
453
522
  async executeOrderFlow(params) {
454
523
  const swapResponse = await params.createOrder();
455
524
  this.logDebug("order created", {
@@ -830,6 +899,27 @@ class RiftSdk {
830
899
  async getSwapStatus(swapId) {
831
900
  return this.unwrapEdenResult(await this.riftClient.swap({ swapId }).get());
832
901
  }
902
+ async cancelOrder(options) {
903
+ const walletClient = options.walletClient;
904
+ const account = walletClient.account;
905
+ if (!account) {
906
+ throw new Error("No account configured on wallet client");
907
+ }
908
+ const deadline = options.deadline ?? Math.floor(Date.now() / 1000) + CANCEL_AUTH_WINDOW_SECONDS;
909
+ this.assertValidCancelDeadline(deadline);
910
+ const preparation = this.unwrapEdenResult(await this.riftClient.swap({ swapId: options.swapId }).cancel.prepare.post({
911
+ deadline
912
+ }));
913
+ this.assertCancelWalletChain(walletClient, preparation.cancellation.chainId);
914
+ const signature = await this.signPreparedCancellation(walletClient, account, preparation.cancellation);
915
+ const request2 = {
916
+ authorization: {
917
+ signature,
918
+ ...preparation.cancellation.kind === "tee_swapper" ? { deadline: preparation.cancellation.deadline } : {}
919
+ }
920
+ };
921
+ return this.unwrapEdenResult(await this.riftClient.swap({ swapId: options.swapId }).cancel.post(request2));
922
+ }
833
923
  }
834
924
  function createRiftSdk(options) {
835
925
  return new RiftSdk(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riftresearch/sdk",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "description": "SDK for swapping between bitcoin and evm chains",
5
5
  "license": "MIT",
6
6
  "files": [