@defuse-protocol/nearintents-mpp-sdk 0.0.0 → 0.1.1

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/Errors.d.ts +57 -0
  4. package/dist/Errors.d.ts.map +1 -0
  5. package/dist/Errors.js +54 -0
  6. package/dist/Errors.js.map +1 -0
  7. package/dist/Methods.d.ts +44 -0
  8. package/dist/Methods.d.ts.map +1 -0
  9. package/dist/Methods.js +23 -0
  10. package/dist/Methods.js.map +1 -0
  11. package/dist/Types.d.ts +125 -0
  12. package/dist/Types.d.ts.map +1 -0
  13. package/dist/Types.js +195 -0
  14. package/dist/Types.js.map +1 -0
  15. package/dist/client/Charge.d.ts +141 -0
  16. package/dist/client/Charge.d.ts.map +1 -0
  17. package/dist/client/Charge.js +167 -0
  18. package/dist/client/Charge.js.map +1 -0
  19. package/dist/client/index.d.ts +5 -0
  20. package/dist/client/index.d.ts.map +1 -0
  21. package/dist/client/index.js +5 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +5 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/internal/OneClick.d.ts +243 -0
  28. package/dist/internal/OneClick.d.ts.map +1 -0
  29. package/dist/internal/OneClick.js +422 -0
  30. package/dist/internal/OneClick.js.map +1 -0
  31. package/dist/server/Charge.d.ts +241 -0
  32. package/dist/server/Charge.d.ts.map +1 -0
  33. package/dist/server/Charge.js +404 -0
  34. package/dist/server/Charge.js.map +1 -0
  35. package/dist/server/index.d.ts +5 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +5 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/package.json +87 -9
  40. package/src/Errors.ts +86 -0
  41. package/src/Methods.ts +24 -0
  42. package/src/Types.ts +256 -0
  43. package/src/client/Charge.ts +268 -0
  44. package/src/client/index.ts +4 -0
  45. package/src/index.ts +4 -0
  46. package/src/internal/OneClick.ts +634 -0
  47. package/src/server/Charge.ts +621 -0
  48. package/src/server/index.ts +4 -0
@@ -0,0 +1,621 @@
1
+ import { Errors, Method, Store } from 'mppx'
2
+
3
+ import {
4
+ SettlementFailedError,
5
+ SettlementTimeoutError,
6
+ SettlementUnavailableError,
7
+ } from '../Errors.js'
8
+ import * as OneClick from '../internal/OneClick.js'
9
+ import * as Methods from '../Methods.js'
10
+ import * as Types from '../Types.js'
11
+
12
+ /**
13
+ * Creates a `nearintents` charge method for usage on the server.
14
+ *
15
+ * Each 402 challenge carries a unique single-use 1Click deposit address as
16
+ * `recipient` (minted via a wet `EXACT_OUTPUT` quote and cached with early
17
+ * refresh so the challenge `expires` always precedes the quote deadline).
18
+ * Verification confirms the client's deposit by 1Click status observation,
19
+ * drives the swap to a terminal state, and issues the extended receipt on
20
+ * `SUCCESS`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { Mppx } from 'mppx/server'
25
+ * import { nearintents } from '@defuse-protocol/nearintents-mpp-sdk/server'
26
+ *
27
+ * const mppx = Mppx.create({
28
+ * secretKey: process.env.MPP_SECRET_KEY!,
29
+ * methods: [
30
+ * nearintents.charge({
31
+ * originAsset: 'eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
32
+ * destinationAsset: 'near:mainnet/nep141:1720…33a1',
33
+ * destinationRecipient: 'merchant.near',
34
+ * refundTo: '0x2527D02599Ba641c19FEa793cD0F9a6e8457C317',
35
+ * amountOut: '1000000',
36
+ * oneClick: { jwt: process.env.ONE_CLICK_JWT },
37
+ * }),
38
+ * ],
39
+ * })
40
+ * ```
41
+ */
42
+ export function charge(parameters: charge.Parameters) {
43
+ const {
44
+ amountOut: defaultAmountOut,
45
+ description,
46
+ destinationAsset,
47
+ destinationRecipient,
48
+ externalId,
49
+ oneClick = {},
50
+ originAsset,
51
+ // Distribution-channel attribution: every 1Click swap minted by this
52
+ // payment method carries the "mpp" referral (mirrors the x402 gateway).
53
+ referral = 'mpp',
54
+ refundTo,
55
+ slippageTolerance = 100,
56
+ } = parameters
57
+
58
+ // Merchant observability: settlement progress is reported as structured
59
+ // events (the library never logs). A throwing handler must not affect
60
+ // payment processing.
61
+ const emit = (event: charge.Event): void => {
62
+ try {
63
+ parameters.onEvent?.(event)
64
+ } catch {
65
+ // Observer errors are the observer's problem.
66
+ }
67
+ }
68
+
69
+ // Fail fast on malformed merchant config (throws at construction time).
70
+ const originNetwork = Types.chainOf(originAsset)
71
+ const destinationNetwork = Types.chainOf(destinationAsset)
72
+
73
+ const expiresWindowMs = (parameters.expiresWindow ?? 300) * 1000
74
+ const quoteDeadlineBufferMs = (parameters.quoteDeadlineBuffer ?? 900) * 1000
75
+ const pollIntervalMs = parameters.pollInterval ?? 2000
76
+
77
+ const store = Store.from(
78
+ (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>,
79
+ { keyPrefix: parameters.storeKeyPrefix ?? '' },
80
+ )
81
+
82
+ // Token list is fetched lazily and re-fetched once on an unresolved asset
83
+ // (the list evolves); a persistent miss is a merchant-config error.
84
+ let assetMapPromise: Promise<OneClick.AssetMap> | undefined
85
+ function getAssetMap(refresh = false): Promise<OneClick.AssetMap> {
86
+ if (!assetMapPromise || refresh) {
87
+ assetMapPromise = OneClick.getTokens(oneClick)
88
+ .then((tokens) => OneClick.createAssetMap(tokens, oneClick))
89
+ .catch((error) => {
90
+ assetMapPromise = undefined
91
+ throw error
92
+ })
93
+ }
94
+ return assetMapPromise
95
+ }
96
+
97
+ async function resolveAssetId(caip19Id: string): Promise<string> {
98
+ let assetId = (await getAssetMap()).toAssetId(caip19Id)
99
+ if (!assetId) assetId = (await getAssetMap(true)).toAssetId(caip19Id)
100
+ if (!assetId)
101
+ throw new Error(
102
+ `nearintents-mpp-sdk: asset "${caip19Id}" is not on the 1Click token list — check the merchant configuration (or extend oneClick.networks).`,
103
+ )
104
+ return assetId
105
+ }
106
+
107
+ const depositKey = (address: string) => `nearintents-mpp-sdk:deposit:${address}` as const
108
+ const quoteKey = (identity: string) => `nearintents-mpp-sdk:quote:${identity}` as const
109
+ const hashKey = (hash: string) => `nearintents-mpp-sdk:hash:${hash.toLowerCase()}` as const
110
+
111
+ function identityOf(amountOut: string): string {
112
+ return [
113
+ originAsset,
114
+ destinationAsset,
115
+ amountOut,
116
+ destinationRecipient,
117
+ refundTo,
118
+ slippageTolerance,
119
+ referral,
120
+ ].join('|')
121
+ }
122
+
123
+ /**
124
+ * Returns the route's active canonical request, minting a fresh 1Click
125
+ * quote when there is none — or when the cached one is stale (early
126
+ * refresh: a quote is stale once `now + expiresWindow > quote deadline`,
127
+ * so the challenge `expires` always precedes the active quote's deadline).
128
+ */
129
+ async function resolveActiveRequest(amountOut: string): Promise<Types.ChargeRequest> {
130
+ const identity = identityOf(amountOut)
131
+
132
+ const pointer = await store.get(quoteKey(identity))
133
+ if (pointer && pointer.identity === identity) {
134
+ const deposit = await store.get(depositKey(pointer.depositAddress))
135
+ if (
136
+ deposit &&
137
+ deposit.state === 'active' &&
138
+ deposit.identity === identity &&
139
+ Date.parse(deposit.deadline) > Date.now() + expiresWindowMs
140
+ ) {
141
+ emit({ type: 'quote.reused', depositAddress: deposit.request.recipient })
142
+ return deposit.request
143
+ }
144
+ }
145
+
146
+ const [originAssetId, destinationAssetId] = await Promise.all([
147
+ resolveAssetId(originAsset),
148
+ resolveAssetId(destinationAsset),
149
+ ])
150
+
151
+ const requestedDeadline = new Date(
152
+ Date.now() + expiresWindowMs + quoteDeadlineBufferMs,
153
+ ).toISOString()
154
+ const { quote } = await OneClick.quote(oneClick, {
155
+ originAsset: originAssetId,
156
+ destinationAsset: destinationAssetId,
157
+ amountOut,
158
+ recipient: destinationRecipient,
159
+ refundTo,
160
+ slippageTolerance,
161
+ deadline: requestedDeadline,
162
+ referral,
163
+ })
164
+
165
+ // Spec §Expiry: `expires` MUST be at or before the quote deadline. The
166
+ // backend's deadline is authoritative; if it cannot cover the route's
167
+ // static expires window, the route is misconfigured — fail loud.
168
+ if (Date.parse(quote.deadline) <= Date.now() + expiresWindowMs)
169
+ throw new Error(
170
+ `nearintents-mpp-sdk: 1Click quote deadline (${quote.deadline}) does not cover the route's expires window (${expiresWindowMs / 1000}s) — lower expiresWindow or raise quoteDeadlineBuffer.`,
171
+ )
172
+
173
+ const request: Types.ChargeRequest = {
174
+ amount: quote.amountIn,
175
+ currency: originAsset,
176
+ recipient: quote.depositAddress,
177
+ ...(description !== undefined && { description }),
178
+ ...(externalId !== undefined && { externalId }),
179
+ methodDetails: {
180
+ originNetwork,
181
+ destinationNetwork,
182
+ destinationAsset,
183
+ destinationRecipient,
184
+ amountOut,
185
+ minAmountIn: quote.minAmountIn,
186
+ depositMemo: quote.depositMemo ?? null,
187
+ slippageTolerance,
188
+ timeEstimate: quote.timeEstimate,
189
+ refundTo,
190
+ settlementBackend: Types.settlementBackend,
191
+ credentialTypes: [...Types.credentialTypes],
192
+ },
193
+ }
194
+
195
+ await store.put(depositKey(quote.depositAddress), {
196
+ identity,
197
+ request,
198
+ deadline: quote.deadline,
199
+ timeEstimate: quote.timeEstimate,
200
+ depositMemo: quote.depositMemo ?? null,
201
+ state: 'active',
202
+ })
203
+ await store.put(quoteKey(identity), { identity, depositAddress: quote.depositAddress })
204
+
205
+ emit({
206
+ type: 'quote.minted',
207
+ depositAddress: quote.depositAddress,
208
+ amountIn: quote.amountIn,
209
+ minAmountIn: quote.minAmountIn,
210
+ amountOut,
211
+ deadline: quote.deadline,
212
+ timeEstimate: quote.timeEstimate,
213
+ })
214
+
215
+ return request
216
+ }
217
+
218
+ /** Marks a deposit spent and drops the cache pointer so the next 402 mints fresh. */
219
+ async function retireDeposit(depositAddress: string, identity: string): Promise<void> {
220
+ await store.update(depositKey(depositAddress), (current) => {
221
+ if (!current) return { op: 'noop', result: undefined }
222
+ return { op: 'set', value: { ...current, state: 'settled' }, result: undefined }
223
+ })
224
+ await store.update(quoteKey(identity), (current) => {
225
+ if (current?.depositAddress !== depositAddress) return { op: 'noop', result: undefined }
226
+ return { op: 'delete', result: undefined }
227
+ })
228
+ }
229
+
230
+ /** Atomically claims `hash` as in-flight. Expired leases are reclaimable. */
231
+ async function claimHash(
232
+ hash: string,
233
+ leaseUntil: number,
234
+ ): Promise<'claimed' | 'inflight' | 'consumed'> {
235
+ return store.update(hashKey(hash), (current) => {
236
+ if (current?.state === 'consumed') return { op: 'noop', result: 'consumed' }
237
+ if (current?.state === 'inflight' && current.leaseUntil > Date.now())
238
+ return { op: 'noop', result: 'inflight' }
239
+ return { op: 'set', value: { state: 'inflight', leaseUntil }, result: 'claimed' }
240
+ })
241
+ }
242
+
243
+ return Method.toServer<typeof Methods.charge, charge.Defaults>(Methods.charge, {
244
+ // Valid canonical placeholder — replaced by the request hook on every
245
+ // real path; only the fallback challenge (request hook threw a
246
+ // PaymentError) ever serializes it.
247
+ defaults: {
248
+ amount: '0',
249
+ currency: originAsset,
250
+ recipient: 'unavailable',
251
+ ...(description !== undefined && { description }),
252
+ ...(externalId !== undefined && { externalId }),
253
+ methodDetails: {
254
+ originNetwork,
255
+ destinationNetwork,
256
+ destinationAsset,
257
+ destinationRecipient,
258
+ amountOut: defaultAmountOut ?? '0',
259
+ minAmountIn: '0',
260
+ depositMemo: null,
261
+ slippageTolerance,
262
+ refundTo,
263
+ settlementBackend: Types.settlementBackend,
264
+ credentialTypes: [...Types.credentialTypes],
265
+ },
266
+ } as charge.Defaults,
267
+
268
+ // Spec §Challenge Binding: the deposit address, amount, and source asset
269
+ // are challenge-specific. Binding them means a rotated quote yields a
270
+ // binding mismatch → 402 with a fresh challenge (the client-recovery flow).
271
+ stableBinding(request) {
272
+ const { amount, currency, methodDetails, recipient } = request
273
+ return { amount, currency, originNetwork: methodDetails.originNetwork, recipient }
274
+ },
275
+
276
+ async request({ credential, request }) {
277
+ const amountOut =
278
+ (request.methodDetails as Partial<Types.MethodDetails> | undefined)?.amountOut ??
279
+ defaultAmountOut
280
+ if (!amountOut || amountOut === '0')
281
+ throw new Error(
282
+ 'nearintents-mpp-sdk: amountOut is required — set it in charge({ amountOut }) or per route via methodDetails.amountOut.',
283
+ )
284
+
285
+ // Credential-bearing request: resolve the quote the challenge was
286
+ // minted from (store lookup keyed by deposit address) — never mint a
287
+ // quote for a credential. Note the echoed challenge is NOT yet
288
+ // HMAC-verified here; the lookup returns OUR stored request, so a
289
+ // forged recipient can at most cause a cache-limited quote refresh.
290
+ if (credential) {
291
+ const recipient = (credential.challenge.request as Partial<Types.ChargeRequest>)?.recipient
292
+ if (typeof recipient === 'string' && recipient) {
293
+ const deposit = await store.get(depositKey(recipient))
294
+ if (
295
+ deposit &&
296
+ deposit.state === 'active' &&
297
+ deposit.identity === identityOf(amountOut) &&
298
+ Date.parse(deposit.deadline) > Date.now()
299
+ )
300
+ return deposit.request
301
+ }
302
+ // Unknown, spent, or expired deposit address: fall through to the
303
+ // current active quote. The stableBinding mismatch then yields a 402
304
+ // carrying this fresh challenge — the spec's client-recovery flow.
305
+ }
306
+
307
+ return resolveActiveRequest(amountOut)
308
+ },
309
+
310
+ async verify({ credential, request }) {
311
+ const { challenge } = credential
312
+ const resolved = (() => {
313
+ const parsed = Methods.charge.schema.request.safeParse(request)
314
+ if (parsed.success) return parsed.data
315
+ return request as unknown as Types.ChargeRequest
316
+ })()
317
+
318
+ const payload = credential.payload
319
+ if (payload.type !== 'hash')
320
+ throw new Errors.InvalidPayloadError({ reason: 'only "hash" credentials are accepted' })
321
+ const hash = payload.hash
322
+
323
+ // Spec §Verification step 4: the deposit address must correspond to an
324
+ // active, non-expired, non-settled quote in the server's state.
325
+ const recipient = resolved.recipient
326
+ const deposit = await store.get(depositKey(recipient))
327
+ if (!deposit)
328
+ throw new Errors.InvalidChallengeError({
329
+ id: challenge.id,
330
+ reason: 'no active quote for this deposit address',
331
+ })
332
+ if (deposit.state === 'settled')
333
+ throw new Errors.InvalidChallengeError({
334
+ id: challenge.id,
335
+ reason: 'the quote for this deposit address is already settled',
336
+ })
337
+ if (Date.parse(deposit.deadline) <= Date.now())
338
+ throw new Errors.PaymentExpiredError({ expires: deposit.deadline })
339
+
340
+ const settlementTimeoutMs =
341
+ (parameters.settlementTimeout ?? deposit.timeEstimate + 120) * 1000
342
+
343
+ // Spec §Verification step 5 / §Replay: claim the hash in-flight
344
+ // atomically; consume permanently only on a terminal settlement state.
345
+ // The lease covers the settlement budget so a crashed settlement never
346
+ // strands a legitimate retry.
347
+ const claim = await claimHash(hash, Date.now() + settlementTimeoutMs + 60_000)
348
+ if (claim === 'consumed')
349
+ throw new Errors.VerificationFailedError({
350
+ reason: 'transaction hash has already been used',
351
+ })
352
+ if (claim === 'inflight')
353
+ throw new Errors.VerificationFailedError({
354
+ reason: 'settlement for this transaction hash is already in progress',
355
+ })
356
+
357
+ let hashOutcome: 'release' | 'consume' = 'release'
358
+ try {
359
+ // Spec §Settlement step 1: notify the backend (optional accelerator;
360
+ // status observation below is authoritative, so failures are benign).
361
+ const accepted = await OneClick.submitDeposit(oneClick, {
362
+ txHash: hash,
363
+ depositAddress: recipient,
364
+ ...(deposit.depositMemo !== null && { memo: deposit.depositMemo }),
365
+ }).then(
366
+ () => true,
367
+ () => false,
368
+ )
369
+ emit({ type: 'deposit.submitted', depositAddress: recipient, originTxHash: hash, accepted })
370
+
371
+ // Spec §Settlement step 2 + §Verification step 3: poll to a terminal
372
+ // status; the backend detecting a qualifying deposit at `recipient`
373
+ // IS the deposit confirmation (status-observation mode).
374
+ const status = await OneClick.pollToTerminal(oneClick, {
375
+ depositAddress: recipient,
376
+ ...(deposit.depositMemo !== null && { depositMemo: deposit.depositMemo }),
377
+ timeoutMs: settlementTimeoutMs,
378
+ intervalMs: pollIntervalMs,
379
+ onStatus: (observed) =>
380
+ emit({ type: 'settlement.status', depositAddress: recipient, status: observed }),
381
+ })
382
+
383
+ // Any terminal state spends the quote and its deposit address —
384
+ // including SUCCESS whose observed deposits don't match the presented
385
+ // hash (below). Retiring here, before that match check, is deliberate:
386
+ // once the backend reaches SUCCESS the deposit is delivered, so the
387
+ // quote MUST leave rotation regardless of which credential presented
388
+ // it. Deferring the retire until after a match check would strand a
389
+ // backend-settled deposit as "active" in the store, and a later
390
+ // cache hit could hand a fresh payer an already-spent deposit address.
391
+ await retireDeposit(recipient, deposit.identity)
392
+ emit({
393
+ type: 'settlement.terminal',
394
+ depositAddress: recipient,
395
+ originTxHash: hash,
396
+ status: status.status,
397
+ ...(OneClick.destinationTxHash(status) !== undefined && {
398
+ destinationTxHash: OneClick.destinationTxHash(status),
399
+ }),
400
+ })
401
+
402
+ if (status.status === 'SUCCESS') {
403
+ // Deposit confirmation: the presented hash must be among the
404
+ // origin-chain transactions the backend observed for this address.
405
+ if (!OneClick.matchesOriginTx(status, hash))
406
+ throw new Errors.VerificationFailedError({
407
+ reason:
408
+ 'the presented transaction hash is not among the deposits observed for this address',
409
+ })
410
+ hashOutcome = 'consume'
411
+ const reference =
412
+ OneClick.destinationTxHash(status) ?? status.swapDetails?.nearTxHashes?.[0]
413
+ // The merchant has been paid at this point; a missing reference is
414
+ // a backend anomaly worth failing loudly on (500), not a 402.
415
+ if (!reference)
416
+ throw new Error(
417
+ `nearintents-mpp-sdk: swap for ${recipient} reached SUCCESS but reported no settlement transaction hash.`,
418
+ )
419
+ const receipt = Types.toReceipt({
420
+ challengeId: challenge.id,
421
+ reference,
422
+ originTxHash: hash,
423
+ destinationNetwork: resolved.methodDetails.destinationNetwork,
424
+ ...(resolved.externalId !== undefined && { externalId: resolved.externalId }),
425
+ })
426
+ emit({
427
+ type: 'receipt.issued',
428
+ challengeId: challenge.id,
429
+ depositAddress: recipient,
430
+ originTxHash: hash,
431
+ reference,
432
+ })
433
+ return receipt
434
+ }
435
+
436
+ // Non-success terminal (FAILED/REFUNDED/INCOMPLETE_DEPOSIT): the
437
+ // deposit is refunded to refundTo and the hash can never deliver —
438
+ // consume it. The client recovers with a fresh challenge.
439
+ hashOutcome = 'consume'
440
+ throw (
441
+ OneClick.terminalError(status) ??
442
+ new SettlementFailedError({ reason: `unexpected terminal status ${status.status}` })
443
+ )
444
+ } catch (error) {
445
+ // Backend unavailability / settlement timeout: 5xx, never
446
+ // verification-failed; do not settle, release the claim so the same
447
+ // credential can be re-presented.
448
+ if (error instanceof OneClick.OneClickUnavailableError) {
449
+ emit({
450
+ type: 'settlement.suspended',
451
+ depositAddress: recipient,
452
+ originTxHash: hash,
453
+ reason: 'unavailable',
454
+ })
455
+ throw new SettlementUnavailableError({ reason: error.message })
456
+ }
457
+ if (error instanceof OneClick.PollTimeoutError) {
458
+ emit({
459
+ type: 'settlement.suspended',
460
+ depositAddress: recipient,
461
+ originTxHash: hash,
462
+ reason: 'timeout',
463
+ })
464
+ throw new SettlementTimeoutError({ timeoutMs: settlementTimeoutMs })
465
+ }
466
+ throw error
467
+ } finally {
468
+ if (hashOutcome === 'consume') await store.put(hashKey(hash), { state: 'consumed' })
469
+ else await store.delete(hashKey(hash))
470
+ }
471
+ },
472
+ })
473
+ }
474
+
475
+ export declare namespace charge {
476
+ /**
477
+ * Structured settlement-progress events for merchant observability (wire
478
+ * them to your logger via `charge({ onEvent })`). All referenced values —
479
+ * deposit addresses and tx hashes — are public on-chain data. Outcome-level
480
+ * events (`payment.success` / `payment.failed` / `challenge.created`) come
481
+ * from mppx itself via `mppx.on(...)`.
482
+ */
483
+ type Event =
484
+ | {
485
+ type: 'quote.minted'
486
+ depositAddress: string
487
+ amountIn: string
488
+ minAmountIn: string
489
+ amountOut: string
490
+ deadline: string
491
+ timeEstimate: number
492
+ }
493
+ | { type: 'quote.reused'; depositAddress: string }
494
+ | { type: 'deposit.submitted'; depositAddress: string; originTxHash: string; accepted: boolean }
495
+ | { type: 'settlement.status'; depositAddress: string; status: OneClick.SwapStatus }
496
+ | {
497
+ type: 'settlement.terminal'
498
+ depositAddress: string
499
+ originTxHash: string
500
+ status: OneClick.SwapStatus
501
+ destinationTxHash?: string | undefined
502
+ }
503
+ | {
504
+ type: 'settlement.suspended'
505
+ depositAddress: string
506
+ originTxHash: string
507
+ /** The credential was NOT consumed; the client re-presents it later. */
508
+ reason: 'unavailable' | 'timeout'
509
+ }
510
+ | {
511
+ type: 'receipt.issued'
512
+ challengeId: string
513
+ depositAddress: string
514
+ originTxHash: string
515
+ reference: string
516
+ }
517
+
518
+ type DepositState = {
519
+ identity: string
520
+ request: Types.ChargeRequest
521
+ /** Quote deadline (ISO 8601) — the deposit address validity window. */
522
+ deadline: string
523
+ timeEstimate: number
524
+ depositMemo: string | null
525
+ state: 'active' | 'settled'
526
+ }
527
+
528
+ type HashState = { state: 'inflight'; leaseUntil: number } | { state: 'consumed' }
529
+
530
+ type QuotePointer = { identity: string; depositAddress: string }
531
+
532
+ type StoreItemMap = {
533
+ [key: `nearintents-mpp-sdk:deposit:${string}`]: DepositState
534
+ [key: `nearintents-mpp-sdk:quote:${string}`]: QuotePointer
535
+ [key: `nearintents-mpp-sdk:hash:${string}`]: HashState
536
+ }
537
+
538
+ type Defaults = Types.ChargeRequest
539
+
540
+ type Parameters = {
541
+ /** Source asset the client pays with, as CAIP-19. Its chain is the origin network. */
542
+ originAsset: string
543
+ /** Destination asset the merchant receives, as CAIP-19. Its chain is the destination network. */
544
+ destinationAsset: string
545
+ /** Merchant address on the destination chain. */
546
+ destinationRecipient: string
547
+ /**
548
+ * Merchant-configured refund address on the origin chain. The server
549
+ * cannot know the payer pre-payment; clients recover refunds off-band
550
+ * (document this in the merchant's terms).
551
+ */
552
+ refundTo: string
553
+ /**
554
+ * Default price: exact amount the merchant receives, in base units of
555
+ * `destinationAsset` (EXACT_OUTPUT). Overridable per route via
556
+ * `methodDetails.amountOut`.
557
+ */
558
+ amountOut?: string | undefined
559
+ /** Slippage tolerance in basis points, applied to the input side. @default 100 */
560
+ slippageTolerance?: number | undefined
561
+ /**
562
+ * 1Click referral identifier (distribution-channel attribution / fee
563
+ * tracking) attached to every quote this method mints. @default "mpp"
564
+ */
565
+ referral?: string | undefined
566
+ /**
567
+ * Settlement-progress observer ({@link charge.Event}) — the library never
568
+ * logs on its own; wire this to your logger. Handler errors are swallowed
569
+ * and never affect payment processing.
570
+ */
571
+ onEvent?: ((event: Event) => void) | undefined
572
+ /** Human-readable payment description for challenges. */
573
+ description?: string | undefined
574
+ /** Merchant reference echoed into challenges and receipts. */
575
+ externalId?: string | undefined
576
+ /** 1Click API configuration (base URL, JWT, fetch, network tables). */
577
+ oneClick?: OneClick.Config | undefined
578
+ /**
579
+ * Atomic store for the quote cache and replay protection (in-flight /
580
+ * consumed hashes). Defaults to in-memory; use a shared store (e.g.
581
+ * redis) in production and multi-instance deployments — atomicity is a
582
+ * hard requirement for the replay guarantees.
583
+ */
584
+ store?: Store.AtomicStore | undefined
585
+ /** Prefix prepended to every store key. */
586
+ storeKeyPrefix?: string | undefined
587
+ /**
588
+ * The route's challenge-expiry window in seconds.
589
+ *
590
+ * **MUST equal the `expires` you pass to the mppx route** (`mppx.charge({
591
+ * expires })`). mppx computes the challenge `expires` per route, before
592
+ * this method's `request` hook runs and unreadable by it, so the window
593
+ * cannot be auto-derived — it is a manual coupling the caller owns.
594
+ *
595
+ * It sizes the quote-cache early refresh so the invariant "advertised
596
+ * `expires` ≤ quote deadline" holds. If the route `expires` is set
597
+ * **larger** than `expiresWindow`, that invariant can break near the
598
+ * cache-staleness boundary: a challenge may advertise an `expires` after
599
+ * the quote deadline, and a client depositing just before `expires` lands
600
+ * after the deadline and is refunded rather than swapped (no fund loss —
601
+ * the client recovers with a fresh challenge — but wasted gas and a silent
602
+ * UX failure). Keep the two values identical. Size to the origin chain
603
+ * (minutes for fast chains, 45–60 min for BTC).
604
+ * @default 300
605
+ */
606
+ expiresWindow?: number | undefined
607
+ /**
608
+ * Extra seconds of quote (deposit-address) validity requested beyond the
609
+ * expires window, so deposits made just before `expires` still settle
610
+ * within the quote's validity. @default 900
611
+ */
612
+ quoteDeadlineBuffer?: number | undefined
613
+ /**
614
+ * Settlement budget in seconds verify() will hold the request while
615
+ * polling for a terminal status. @default quote timeEstimate + 120
616
+ */
617
+ settlementTimeout?: number | undefined
618
+ /** Status poll interval in milliseconds. @default 2000 */
619
+ pollInterval?: number | undefined
620
+ }
621
+ }
@@ -0,0 +1,4 @@
1
+ export * as Errors from '../Errors.js'
2
+ export * as Methods from '../Methods.js'
3
+ export * as Types from '../Types.js'
4
+ export * as nearintents from './Charge.js'