@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,634 @@
1
+ import { Errors } from 'mppx'
2
+
3
+ import { SettlementFailedError } from '../Errors.js'
4
+ import * as Types from '../Types.js'
5
+
6
+ /**
7
+ * 1Click Swap API settlement core.
8
+ *
9
+ * Thin, dependency-free client for the three endpoints this method uses
10
+ * (`POST /v0/quote`, `POST /v0/deposit/submit`, `GET /v0/status`) plus the
11
+ * token-list-driven CAIP-19 ↔ 1Click asset mapping and the terminal-state →
12
+ * MPP problem-type mapping. Everything is injectable (`fetch`, `baseUrl`) so
13
+ * tests run against the in-process mock — never live 1Click.
14
+ *
15
+ * Designed to be shared with the x402 1CS Gateway workstream: one settlement
16
+ * core, two protocol adapters.
17
+ */
18
+
19
+ export const defaultBaseUrl = 'https://1click.chaindefuser.com'
20
+
21
+ export type Config = {
22
+ /** API base URL. @default "https://1click.chaindefuser.com" */
23
+ baseUrl?: string | undefined
24
+ /**
25
+ * Partner JWT (env `ONE_CLICK_JWT`). Server-side only — MUST never appear
26
+ * in any challenge field. Unauthenticated requests incur a 0.2% fee.
27
+ */
28
+ jwt?: string | undefined
29
+ /** Fetch implementation. @default globalThis.fetch */
30
+ fetch?: typeof globalThis.fetch | undefined
31
+ /** Per-request timeout in milliseconds. @default 30000 */
32
+ requestTimeoutMs?: number | undefined
33
+ /** Additional CAIP-2 → 1Click blockchain-code entries, merged over {@link defaultNetworks}. */
34
+ networks?: Record<string, string> | undefined
35
+ /** Additional blockchain-code → SLIP-44 coin-type entries, merged over {@link defaultNativeCoinTypes}. */
36
+ nativeCoinTypes?: Record<string, number> | undefined
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Errors
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * The 1Click API request failed (application error or unusable response).
45
+ * Note {@link OneClickUnavailableError} (network failure / 5xx) is a subclass —
46
+ * narrow on it first when the distinction matters.
47
+ */
48
+ export class OneClickError extends Error {
49
+ override readonly name: string = 'OneClickError'
50
+ readonly status: number
51
+ readonly body: unknown
52
+
53
+ constructor(message: string, options: { status?: number; body?: unknown; cause?: unknown } = {}) {
54
+ super(message, options.cause !== undefined ? { cause: options.cause } : undefined)
55
+ this.status = options.status ?? 0
56
+ this.body = options.body
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The 1Click API was unreachable or returned a server error (network failure
62
+ * or HTTP 5xx). Per the spec, callers MUST surface this as an HTTP 5xx —
63
+ * never as `verification-failed` — and MUST NOT settle the credential.
64
+ */
65
+ export class OneClickUnavailableError extends OneClickError {
66
+ override readonly name = 'OneClickUnavailableError'
67
+ }
68
+
69
+ /** `pollToTerminal` exceeded its time budget without reaching a terminal status. */
70
+ export class PollTimeoutError extends Error {
71
+ override readonly name = 'PollTimeoutError'
72
+ /** Last successfully observed status, if any poll succeeded. */
73
+ readonly lastStatus: SwapStatus | undefined
74
+ /** Last transport error, if polling was failing when the budget ran out. */
75
+ readonly lastError: unknown
76
+
77
+ constructor(options: { timeoutMs: number; lastStatus?: SwapStatus; lastError?: unknown }) {
78
+ super(
79
+ `1Click status polling timed out after ${options.timeoutMs}ms` +
80
+ (options.lastStatus ? ` (last status: ${options.lastStatus})` : ''),
81
+ )
82
+ this.lastStatus = options.lastStatus
83
+ this.lastError = options.lastError
84
+ }
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // API types (subset of the 1Click OpenAPI actually used by this method)
89
+ // ---------------------------------------------------------------------------
90
+
91
+ export type TokenInfo = {
92
+ /** 1Click-native asset id (e.g. `nep141:arb-0xaf88….omft.near`). */
93
+ assetId: string
94
+ decimals: number
95
+ /** 1Click blockchain code (e.g. "arb", "btc", "near"). */
96
+ blockchain: string
97
+ symbol: string
98
+ /** Absent for chain-native assets (e.g. BTC). */
99
+ contractAddress?: string | undefined
100
+ }
101
+
102
+ export type Quote = {
103
+ /** Unique single-use deposit address on the origin chain. */
104
+ depositAddress: string
105
+ /** Required alongside the deposit on memo-based chains (e.g. Stellar). */
106
+ depositMemo?: string | undefined
107
+ /** Input amount with slippage buffer baked in — the challenge `amount`. */
108
+ amountIn: string
109
+ /** Minimum input that still guarantees `amountOut` — the verification threshold. */
110
+ minAmountIn: string
111
+ /** Exact output amount the merchant receives (EXACT_OUTPUT). */
112
+ amountOut: string
113
+ minAmountOut?: string | undefined
114
+ /** Deposit deadline (ISO 8601) — the challenge `expires` upper bound. */
115
+ deadline: string
116
+ timeWhenInactive?: string | undefined
117
+ /** Estimated swap completion time in seconds after deposit confirmation. */
118
+ timeEstimate: number
119
+ }
120
+
121
+ export type QuoteResult = {
122
+ quote: Quote
123
+ /** Echo of the quote request. */
124
+ quoteRequest: Record<string, unknown>
125
+ /** 1Click service signature over the quote — persist for dispute resolution. */
126
+ signature?: string | undefined
127
+ timestamp?: string | undefined
128
+ }
129
+
130
+ export type SwapStatus =
131
+ | 'KNOWN_DEPOSIT_TX'
132
+ | 'PENDING_DEPOSIT'
133
+ | 'INCOMPLETE_DEPOSIT'
134
+ | 'PROCESSING'
135
+ | 'SUCCESS'
136
+ | 'REFUNDED'
137
+ | 'FAILED'
138
+
139
+ export type TransactionDetails = {
140
+ hash: string
141
+ explorerUrl?: string | undefined
142
+ }
143
+
144
+ export type SwapDetails = {
145
+ originChainTxHashes?: TransactionDetails[] | undefined
146
+ destinationChainTxHashes?: TransactionDetails[] | undefined
147
+ intentHashes?: string[] | undefined
148
+ nearTxHashes?: string[] | undefined
149
+ amountIn?: string | undefined
150
+ amountOut?: string | undefined
151
+ depositedAmount?: string | undefined
152
+ refundedAmount?: string | undefined
153
+ refundReason?: string | undefined
154
+ }
155
+
156
+ export type StatusResult = {
157
+ status: SwapStatus
158
+ updatedAt?: string | undefined
159
+ swapDetails?: SwapDetails | undefined
160
+ quoteResponse?: Record<string, unknown> | undefined
161
+ }
162
+
163
+ /**
164
+ * Terminal settlement statuses per the spec (§Settlement Procedure): the swap
165
+ * either delivered (`SUCCESS`) or the deposit is refunded to `refundTo`.
166
+ * `INCOMPLETE_DEPOSIT` is treated as terminal for settlement even though the
167
+ * backend may still accept top-ups before the deadline.
168
+ */
169
+ export const terminalStatuses = ['SUCCESS', 'FAILED', 'REFUNDED', 'INCOMPLETE_DEPOSIT'] as const
170
+
171
+ export function isTerminal(status: SwapStatus): boolean {
172
+ return (terminalStatuses as readonly string[]).includes(status)
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // HTTP core
177
+ // ---------------------------------------------------------------------------
178
+
179
+ async function request<result>(
180
+ config: Config,
181
+ path: string,
182
+ init: { method: 'GET' | 'POST'; body?: unknown; searchParams?: Record<string, string> },
183
+ ): Promise<result> {
184
+ const baseUrl = config.baseUrl ?? defaultBaseUrl
185
+ const fetchFn = config.fetch ?? globalThis.fetch
186
+ const url = new URL(path, baseUrl)
187
+ for (const [key, value] of Object.entries(init.searchParams ?? {})) {
188
+ url.searchParams.set(key, value)
189
+ }
190
+
191
+ let response: Response
192
+ try {
193
+ response = await fetchFn(url, {
194
+ method: init.method,
195
+ headers: {
196
+ accept: 'application/json',
197
+ ...(init.body !== undefined && { 'content-type': 'application/json' }),
198
+ ...(config.jwt && { authorization: `Bearer ${config.jwt}` }),
199
+ },
200
+ ...(init.body !== undefined && { body: JSON.stringify(init.body) }),
201
+ signal: AbortSignal.timeout(config.requestTimeoutMs ?? 30_000),
202
+ })
203
+ } catch (error) {
204
+ throw new OneClickUnavailableError(`1Click request failed: ${init.method} ${path}`, {
205
+ cause: error,
206
+ })
207
+ }
208
+
209
+ const body = await response.json().catch(() => undefined)
210
+
211
+ if (response.status >= 500)
212
+ throw new OneClickUnavailableError(
213
+ `1Click returned ${response.status} for ${init.method} ${path}`,
214
+ { status: response.status, body },
215
+ )
216
+ if (!response.ok)
217
+ throw new OneClickError(
218
+ `1Click returned ${response.status} for ${init.method} ${path}: ${detailOf(body)}`,
219
+ { status: response.status, body },
220
+ )
221
+
222
+ return body as result
223
+ }
224
+
225
+ function detailOf(body: unknown): string {
226
+ if (body && typeof body === 'object' && 'message' in body) return String(body.message)
227
+ return body === undefined ? 'no response body' : JSON.stringify(body)
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Endpoints
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /** Fetches the supported token list (`GET /v0/tokens`). No auth required. */
235
+ export async function getTokens(config: Config): Promise<TokenInfo[]> {
236
+ const tokens = await request<TokenInfo[]>(config, '/v0/tokens', { method: 'GET' })
237
+ if (!Array.isArray(tokens))
238
+ throw new OneClickError('1Click token list response is not an array.', { body: tokens })
239
+ return tokens
240
+ }
241
+
242
+ export type QuoteParameters = {
243
+ /** Origin asset as a 1Click asset id (map from CAIP-19 via {@link AssetMap}). */
244
+ originAsset: string
245
+ /** Destination asset as a 1Click asset id. */
246
+ destinationAsset: string
247
+ /** Exact output amount the merchant must receive, in base units (EXACT_OUTPUT). */
248
+ amountOut: string
249
+ /** Merchant address on the destination chain. */
250
+ recipient: string
251
+ /** Merchant-configured refund address on the origin chain. */
252
+ refundTo: string
253
+ /** Slippage tolerance in basis points (applied to the input side). */
254
+ slippageTolerance: number
255
+ /** Quote/deposit deadline (ISO 8601). Size to the origin chain (BTC ≈ 45–60 min). */
256
+ deadline: string
257
+ /** `SIMPLE` (default) or `MEMO` for memo-based chains. */
258
+ depositMode?: 'SIMPLE' | 'MEMO' | undefined
259
+ referral?: string | undefined
260
+ sessionId?: string | undefined
261
+ quoteWaitingTimeMs?: number | undefined
262
+ /** Escape hatch merged last into the raw request body. */
263
+ overrides?: Record<string, unknown> | undefined
264
+ }
265
+
266
+ /**
267
+ * Requests a wet (`dry: false`) EXACT_OUTPUT quote (`POST /v0/quote`),
268
+ * yielding the unique single-use deposit address for one challenge.
269
+ */
270
+ export async function quote(config: Config, parameters: QuoteParameters): Promise<QuoteResult> {
271
+ const {
272
+ amountOut,
273
+ deadline,
274
+ depositMode,
275
+ destinationAsset,
276
+ originAsset,
277
+ overrides,
278
+ quoteWaitingTimeMs,
279
+ recipient,
280
+ referral,
281
+ refundTo,
282
+ sessionId,
283
+ slippageTolerance,
284
+ } = parameters
285
+
286
+ const body = {
287
+ dry: false,
288
+ swapType: 'EXACT_OUTPUT',
289
+ depositType: 'ORIGIN_CHAIN',
290
+ recipientType: 'DESTINATION_CHAIN',
291
+ refundType: 'ORIGIN_CHAIN',
292
+ amount: amountOut,
293
+ originAsset,
294
+ destinationAsset,
295
+ recipient,
296
+ refundTo,
297
+ slippageTolerance,
298
+ deadline,
299
+ ...(depositMode !== undefined && { depositMode }),
300
+ ...(referral !== undefined && { referral }),
301
+ ...(sessionId !== undefined && { sessionId }),
302
+ ...(quoteWaitingTimeMs !== undefined && { quoteWaitingTimeMs }),
303
+ ...overrides,
304
+ }
305
+
306
+ const result = await request<{ quote?: Quote; quoteRequest?: Record<string, unknown> }>(
307
+ config,
308
+ '/v0/quote',
309
+ { method: 'POST', body },
310
+ )
311
+
312
+ const quote_ = result?.quote
313
+ for (const field of ['depositAddress', 'amountIn', 'minAmountIn', 'deadline'] as const) {
314
+ if (!quote_?.[field])
315
+ throw new OneClickError(`1Click quote response is missing "quote.${field}".`, {
316
+ body: result,
317
+ })
318
+ }
319
+
320
+ return {
321
+ quote: quote_ as Quote,
322
+ quoteRequest: result.quoteRequest ?? body,
323
+ ...(hasString(result, 'signature') && { signature: result.signature }),
324
+ ...(hasString(result, 'timestamp') && { timestamp: result.timestamp }),
325
+ }
326
+ }
327
+
328
+ function hasString<key extends string>(value: unknown, key: key): value is Record<key, string> {
329
+ return (
330
+ typeof value === 'object' &&
331
+ value !== null &&
332
+ typeof (value as Record<string, unknown>)[key] === 'string'
333
+ )
334
+ }
335
+
336
+ /**
337
+ * Notifies 1Click of a sent deposit (`POST /v0/deposit/submit`). Optional but
338
+ * recommended after verification — it accelerates deposit detection.
339
+ */
340
+ export async function submitDeposit(
341
+ config: Config,
342
+ parameters: { txHash: string; depositAddress: string; memo?: string | undefined },
343
+ ): Promise<StatusResult> {
344
+ const { depositAddress, memo, txHash } = parameters
345
+ return request<StatusResult>(config, '/v0/deposit/submit', {
346
+ method: 'POST',
347
+ body: { txHash, depositAddress, ...(memo !== undefined && { memo }) },
348
+ })
349
+ }
350
+
351
+ /** Fetches the swap status for a deposit address (`GET /v0/status`). */
352
+ export async function getStatus(
353
+ config: Config,
354
+ parameters: { depositAddress: string; depositMemo?: string | undefined },
355
+ ): Promise<StatusResult> {
356
+ const { depositAddress, depositMemo } = parameters
357
+ return request<StatusResult>(config, '/v0/status', {
358
+ method: 'GET',
359
+ searchParams: {
360
+ depositAddress,
361
+ ...(depositMemo !== undefined && { depositMemo }),
362
+ },
363
+ })
364
+ }
365
+
366
+ export type PollParameters = {
367
+ depositAddress: string
368
+ depositMemo?: string | undefined
369
+ /** Total time budget. Derive from the challenge window + `timeEstimate` + margin. */
370
+ timeoutMs: number
371
+ /** Delay between polls. @default 2000 */
372
+ intervalMs?: number | undefined
373
+ /** Invoked once per observed status change (not per poll tick). */
374
+ onStatus?: ((status: SwapStatus) => void) | undefined
375
+ signal?: AbortSignal | undefined
376
+ }
377
+
378
+ /**
379
+ * Polls the status endpoint until a terminal status ({@link terminalStatuses})
380
+ * or the time budget is exceeded.
381
+ *
382
+ * Transient transport failures ({@link OneClickUnavailableError}) are retried
383
+ * until the budget runs out; if the budget ends on a failing endpoint the last
384
+ * error is rethrown (so callers surface 5xx, not a false timeout). A budget
385
+ * that ends while the swap is still in flight throws {@link PollTimeoutError}.
386
+ */
387
+ export async function pollToTerminal(
388
+ config: Config,
389
+ parameters: PollParameters,
390
+ ): Promise<StatusResult> {
391
+ const { depositAddress, depositMemo, onStatus, signal, timeoutMs } = parameters
392
+ const intervalMs = parameters.intervalMs ?? 2000
393
+ const deadline = Date.now() + timeoutMs
394
+
395
+ let lastStatus: SwapStatus | undefined
396
+ let lastError: unknown
397
+
398
+ while (true) {
399
+ signal?.throwIfAborted()
400
+ try {
401
+ const result = await getStatus(config, { depositAddress, depositMemo })
402
+ if (result.status !== lastStatus) onStatus?.(result.status)
403
+ lastStatus = result.status
404
+ lastError = undefined
405
+ if (isTerminal(result.status)) return result
406
+ } catch (error) {
407
+ if (!(error instanceof OneClickUnavailableError)) throw error
408
+ lastError = error
409
+ }
410
+
411
+ if (Date.now() + intervalMs > deadline) {
412
+ if (lastError !== undefined) throw lastError
413
+ throw new PollTimeoutError({
414
+ timeoutMs,
415
+ ...(lastStatus !== undefined && { lastStatus }),
416
+ })
417
+ }
418
+ await sleep(intervalMs, signal)
419
+ }
420
+ }
421
+
422
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
423
+ return new Promise((resolve, reject) => {
424
+ if (signal?.aborted) {
425
+ reject(signal.reason ?? new Error('Aborted.'))
426
+ return
427
+ }
428
+ const timer = setTimeout(() => {
429
+ signal?.removeEventListener('abort', onAbort)
430
+ resolve()
431
+ }, ms)
432
+ const onAbort = () => {
433
+ clearTimeout(timer)
434
+ reject(signal?.reason ?? new Error('Aborted.'))
435
+ }
436
+ signal?.addEventListener('abort', onAbort, { once: true })
437
+ })
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Terminal-state → MPP problem mapping (spec §Error Codes)
442
+ // ---------------------------------------------------------------------------
443
+
444
+ /**
445
+ * Maps a non-success terminal status to its MPP problem type:
446
+ * `INCOMPLETE_DEPOSIT` → `payment-insufficient`; `FAILED`/`REFUNDED` →
447
+ * `settlement-failed`. Returns `undefined` for `SUCCESS`. Throws if the
448
+ * status is not terminal.
449
+ */
450
+ export function terminalError(result: StatusResult): Errors.PaymentError | undefined {
451
+ const { status, swapDetails } = result
452
+ switch (status) {
453
+ case 'SUCCESS':
454
+ return undefined
455
+ case 'INCOMPLETE_DEPOSIT':
456
+ return new Errors.PaymentInsufficientError({
457
+ reason: swapDetails?.depositedAmount
458
+ ? `deposit of ${swapDetails.depositedAmount} is below the required minimum`
459
+ : 'deposit is below the required minimum',
460
+ })
461
+ case 'FAILED':
462
+ case 'REFUNDED':
463
+ return new SettlementFailedError({
464
+ reason: swapDetails?.refundReason ?? `1Click terminal status ${status}`,
465
+ })
466
+ default:
467
+ throw new Error(`Status "${status}" is not terminal.`)
468
+ }
469
+ }
470
+
471
+ /** Extracts the destination-chain delivery tx hash (the receipt `reference`). */
472
+ export function destinationTxHash(result: StatusResult): string | undefined {
473
+ return result.swapDetails?.destinationChainTxHashes?.[0]?.hash
474
+ }
475
+
476
+ /**
477
+ * Checks whether the credential's `payload.hash` is among the origin-chain
478
+ * transactions the backend observed for this deposit address — the spec's
479
+ * status-observation deposit confirmation. Hex hashes compare
480
+ * case-insensitively; other formats (base58, …) compare exactly.
481
+ */
482
+ export function matchesOriginTx(result: StatusResult, hash: string): boolean {
483
+ const hashes = result.swapDetails?.originChainTxHashes ?? []
484
+ return hashes.some((tx) => txHashEqual(tx.hash, hash))
485
+ }
486
+
487
+ function txHashEqual(a: string, b: string): boolean {
488
+ const hexLike = /^(0x)?[0-9a-fA-F]{16,}$/
489
+ if (hexLike.test(a) && hexLike.test(b)) return normalizeHex(a) === normalizeHex(b)
490
+ return a === b
491
+ }
492
+
493
+ function normalizeHex(hash: string): string {
494
+ return hash.toLowerCase().replace(/^0x/, '')
495
+ }
496
+
497
+ // ---------------------------------------------------------------------------
498
+ // CAIP-19 ↔ 1Click asset mapping (token-list driven)
499
+ // ---------------------------------------------------------------------------
500
+
501
+ /**
502
+ * CAIP-2 chain identifier → 1Click blockchain code, for chains whose CAIP-2
503
+ * form is well established. Extensible per-instance via `Config.networks` —
504
+ * unsupported chains need a config entry, not a package release.
505
+ */
506
+ export const defaultNetworks: Record<string, string> = {
507
+ 'near:mainnet': 'near',
508
+ 'eip155:1': 'eth',
509
+ 'eip155:10': 'op',
510
+ 'eip155:56': 'bsc',
511
+ 'eip155:100': 'gnosis',
512
+ 'eip155:137': 'pol',
513
+ 'eip155:143': 'monad',
514
+ 'eip155:196': 'xlayer',
515
+ 'eip155:8453': 'base',
516
+ 'eip155:42161': 'arb',
517
+ 'eip155:43114': 'avax',
518
+ 'eip155:80094': 'bera',
519
+ 'eip155:534352': 'scroll',
520
+ 'bip122:000000000019d6689c085ae165831e93': 'btc',
521
+ 'bip122:12a765e31ffd4059bada1e25190f6e98': 'ltc',
522
+ 'bip122:1a91e3dace36e2be3bf030a65679fe82': 'doge',
523
+ 'bip122:000000000000000000651ef99cb9fcbe': 'bch',
524
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': 'sol',
525
+ 'ton:-239': 'ton',
526
+ 'sui:mainnet': 'sui',
527
+ 'stellar:pubnet': 'stellar',
528
+ 'xrpl:0': 'xrp',
529
+ 'starknet:SN_MAIN': 'starknet',
530
+ }
531
+
532
+ /**
533
+ * 1Click blockchain code → SLIP-44 coin type used to render chain-native
534
+ * assets (`<caip2>/slip44:<coinType>`). Extensible via `Config.nativeCoinTypes`.
535
+ */
536
+ export const defaultNativeCoinTypes: Record<string, number> = {
537
+ btc: 0,
538
+ ltc: 2,
539
+ doge: 3,
540
+ bch: 145,
541
+ eth: 60,
542
+ base: 60,
543
+ arb: 60,
544
+ op: 60,
545
+ scroll: 60,
546
+ bsc: 714,
547
+ pol: 966,
548
+ gnosis: 700,
549
+ avax: 9000,
550
+ xrp: 144,
551
+ sol: 501,
552
+ ton: 607,
553
+ stellar: 148,
554
+ sui: 784,
555
+ }
556
+
557
+ /** CAIP-19 asset-namespace used when rendering a contract asset for a chain namespace. */
558
+ const contractAssetNamespaces: Record<string, string> = {
559
+ eip155: 'erc20',
560
+ near: 'nep141',
561
+ solana: 'token',
562
+ }
563
+
564
+ export type AssetMap = {
565
+ /** Resolves a CAIP-19 asset id to the matching 1Click token, or `undefined`. */
566
+ tokenOf(caip19Id: string): TokenInfo | undefined
567
+ /** Resolves a CAIP-19 asset id to a 1Click asset id, or `undefined`. */
568
+ toAssetId(caip19Id: string): string | undefined
569
+ /** Renders a 1Click asset id as a CAIP-19 asset id, or `undefined` when unmapped. */
570
+ toCaip19(assetId: string): string | undefined
571
+ }
572
+
573
+ /**
574
+ * Builds a bidirectional CAIP-19 ↔ 1Click asset map from the `/v0/tokens`
575
+ * list. Contract assets match on `contractAddress` (case-insensitively for
576
+ * `eip155` chains); `slip44` assets match the chain's token that has no
577
+ * `contractAddress` (chain-native), and only for the chain's canonical coin
578
+ * type — a wrong coin type resolves to nothing rather than silently mapping
579
+ * to the native asset.
580
+ */
581
+ export function createAssetMap(tokens: TokenInfo[], config: Config = {}): AssetMap {
582
+ const networks = { ...defaultNetworks, ...config.networks }
583
+ const nativeCoinTypes = { ...defaultNativeCoinTypes, ...config.nativeCoinTypes }
584
+ const blockchainToCaip2 = new Map<string, string>()
585
+ for (const [caip2Id, code] of Object.entries(networks)) {
586
+ if (!blockchainToCaip2.has(code)) blockchainToCaip2.set(code, caip2Id)
587
+ }
588
+
589
+ function blockchainOf(caip19Id: string): string | undefined {
590
+ const chain = Types.chainOf(caip19Id)
591
+ return Object.entries(networks).find(([caip2Id]) => Types.networkEqual(caip2Id, chain))?.[1]
592
+ }
593
+
594
+ function tokenOf(caip19Id: string): TokenInfo | undefined {
595
+ const parsed = Types.parseCaip19(caip19Id)
596
+ const blockchain = blockchainOf(caip19Id)
597
+ if (!blockchain) return undefined
598
+
599
+ if (parsed.assetNamespace === 'slip44') {
600
+ const coinType = nativeCoinTypes[blockchain]
601
+ if (coinType === undefined || parsed.assetReference !== String(coinType)) return undefined
602
+ return tokens.find((token) => token.blockchain === blockchain && !token.contractAddress)
603
+ }
604
+
605
+ const caseInsensitive = parsed.chain.namespace === 'eip155'
606
+ const reference = caseInsensitive ? parsed.assetReference.toLowerCase() : parsed.assetReference
607
+ return tokens.find((token) => {
608
+ if (token.blockchain !== blockchain || !token.contractAddress) return false
609
+ const address = caseInsensitive ? token.contractAddress.toLowerCase() : token.contractAddress
610
+ return address === reference
611
+ })
612
+ }
613
+
614
+ return {
615
+ tokenOf,
616
+ toAssetId(caip19Id) {
617
+ return tokenOf(caip19Id)?.assetId
618
+ },
619
+ toCaip19(assetId) {
620
+ const token = tokens.find((entry) => entry.assetId === assetId)
621
+ if (!token) return undefined
622
+ const caip2Id = blockchainToCaip2.get(token.blockchain)
623
+ if (!caip2Id) return undefined
624
+ if (token.contractAddress) {
625
+ const namespace = Types.parseCaip2(caip2Id).namespace
626
+ const assetNamespace = contractAssetNamespaces[namespace] ?? 'token'
627
+ return `${caip2Id}/${assetNamespace}:${token.contractAddress}`
628
+ }
629
+ const coinType = nativeCoinTypes[token.blockchain]
630
+ if (coinType === undefined) return undefined
631
+ return `${caip2Id}/slip44:${coinType}`
632
+ },
633
+ }
634
+ }