mppx 0.6.18 → 0.6.20

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 (155) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/Challenge.d.ts +2 -2
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +1 -1
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Method.d.ts +34 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +3 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/Receipt.d.ts +1 -0
  11. package/dist/Receipt.d.ts.map +1 -1
  12. package/dist/Receipt.js +2 -0
  13. package/dist/Receipt.js.map +1 -1
  14. package/dist/client/Methods.d.ts +1 -0
  15. package/dist/client/Methods.d.ts.map +1 -1
  16. package/dist/client/Methods.js +1 -0
  17. package/dist/client/Methods.js.map +1 -1
  18. package/dist/middlewares/elysia.d.ts.map +1 -1
  19. package/dist/middlewares/elysia.js +14 -0
  20. package/dist/middlewares/elysia.js.map +1 -1
  21. package/dist/middlewares/express.d.ts.map +1 -1
  22. package/dist/middlewares/express.js +1 -2
  23. package/dist/middlewares/express.js.map +1 -1
  24. package/dist/middlewares/hono.d.ts.map +1 -1
  25. package/dist/middlewares/hono.js +14 -0
  26. package/dist/middlewares/hono.js.map +1 -1
  27. package/dist/middlewares/nextjs.d.ts.map +1 -1
  28. package/dist/middlewares/nextjs.js +14 -0
  29. package/dist/middlewares/nextjs.js.map +1 -1
  30. package/dist/proxy/Proxy.d.ts.map +1 -1
  31. package/dist/proxy/Proxy.js +2 -2
  32. package/dist/proxy/Proxy.js.map +1 -1
  33. package/dist/proxy/Service.d.ts.map +1 -1
  34. package/dist/proxy/Service.js +1 -1
  35. package/dist/proxy/Service.js.map +1 -1
  36. package/dist/server/Mppx.d.ts +15 -3
  37. package/dist/server/Mppx.d.ts.map +1 -1
  38. package/dist/server/Mppx.js +190 -40
  39. package/dist/server/Mppx.js.map +1 -1
  40. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  41. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  42. package/dist/stripe/server/internal/html.gen.js +1 -1
  43. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  44. package/dist/tempo/Methods.d.ts +96 -0
  45. package/dist/tempo/Methods.d.ts.map +1 -1
  46. package/dist/tempo/Methods.js +97 -0
  47. package/dist/tempo/Methods.js.map +1 -1
  48. package/dist/tempo/client/Methods.d.ts +3 -0
  49. package/dist/tempo/client/Methods.d.ts.map +1 -1
  50. package/dist/tempo/client/Methods.js +3 -0
  51. package/dist/tempo/client/Methods.js.map +1 -1
  52. package/dist/tempo/client/Subscription.d.ts +114 -0
  53. package/dist/tempo/client/Subscription.d.ts.map +1 -0
  54. package/dist/tempo/client/Subscription.js +100 -0
  55. package/dist/tempo/client/Subscription.js.map +1 -0
  56. package/dist/tempo/client/index.d.ts +1 -0
  57. package/dist/tempo/client/index.d.ts.map +1 -1
  58. package/dist/tempo/client/index.js +1 -0
  59. package/dist/tempo/client/index.js.map +1 -1
  60. package/dist/tempo/index.d.ts +1 -0
  61. package/dist/tempo/index.d.ts.map +1 -1
  62. package/dist/tempo/index.js +1 -0
  63. package/dist/tempo/index.js.map +1 -1
  64. package/dist/tempo/server/Charge.js +2 -2
  65. package/dist/tempo/server/Charge.js.map +1 -1
  66. package/dist/tempo/server/Methods.d.ts +5 -0
  67. package/dist/tempo/server/Methods.d.ts.map +1 -1
  68. package/dist/tempo/server/Methods.js +5 -0
  69. package/dist/tempo/server/Methods.js.map +1 -1
  70. package/dist/tempo/server/Subscription.d.ts +221 -0
  71. package/dist/tempo/server/Subscription.d.ts.map +1 -0
  72. package/dist/tempo/server/Subscription.js +637 -0
  73. package/dist/tempo/server/Subscription.js.map +1 -0
  74. package/dist/tempo/server/index.d.ts +1 -0
  75. package/dist/tempo/server/index.d.ts.map +1 -1
  76. package/dist/tempo/server/index.js +1 -0
  77. package/dist/tempo/server/index.js.map +1 -1
  78. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  79. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  80. package/dist/tempo/server/internal/html.gen.js +1 -1
  81. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  82. package/dist/tempo/session/Chain.d.ts.map +1 -1
  83. package/dist/tempo/session/Chain.js +3 -4
  84. package/dist/tempo/session/Chain.js.map +1 -1
  85. package/dist/tempo/subscription/KeyAuthorization.d.ts +282 -0
  86. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -0
  87. package/dist/tempo/subscription/KeyAuthorization.js +297 -0
  88. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -0
  89. package/dist/tempo/subscription/Receipt.d.ts +10 -0
  90. package/dist/tempo/subscription/Receipt.d.ts.map +1 -0
  91. package/dist/tempo/subscription/Receipt.js +16 -0
  92. package/dist/tempo/subscription/Receipt.js.map +1 -0
  93. package/dist/tempo/subscription/Store.d.ts +99 -0
  94. package/dist/tempo/subscription/Store.d.ts.map +1 -0
  95. package/dist/tempo/subscription/Store.js +292 -0
  96. package/dist/tempo/subscription/Store.js.map +1 -0
  97. package/dist/tempo/subscription/Types.d.ts +65 -0
  98. package/dist/tempo/subscription/Types.d.ts.map +1 -0
  99. package/dist/tempo/subscription/Types.js +2 -0
  100. package/dist/tempo/subscription/Types.js.map +1 -0
  101. package/dist/tempo/subscription/index.d.ts +6 -0
  102. package/dist/tempo/subscription/index.d.ts.map +1 -0
  103. package/dist/tempo/subscription/index.js +4 -0
  104. package/dist/tempo/subscription/index.js.map +1 -0
  105. package/dist/zod.d.ts +7 -0
  106. package/dist/zod.d.ts.map +1 -1
  107. package/dist/zod.js +18 -0
  108. package/dist/zod.js.map +1 -1
  109. package/package.json +3 -3
  110. package/src/Challenge.test.ts +13 -0
  111. package/src/Challenge.ts +3 -3
  112. package/src/Method.ts +46 -1
  113. package/src/Receipt.ts +2 -0
  114. package/src/client/Methods.ts +1 -0
  115. package/src/middlewares/elysia.test.ts +31 -1
  116. package/src/middlewares/elysia.ts +13 -0
  117. package/src/middlewares/express.ts +1 -5
  118. package/src/middlewares/hono.test.ts +30 -1
  119. package/src/middlewares/hono.ts +13 -0
  120. package/src/middlewares/nextjs.test.ts +28 -1
  121. package/src/middlewares/nextjs.ts +13 -0
  122. package/src/proxy/Proxy.ts +2 -5
  123. package/src/proxy/Service.test.ts +34 -0
  124. package/src/proxy/Service.ts +7 -0
  125. package/src/server/Mppx.authorize.test.ts +210 -0
  126. package/src/server/Mppx.test-d.ts +23 -1
  127. package/src/server/Mppx.test.ts +73 -3
  128. package/src/server/Mppx.ts +291 -58
  129. package/src/stripe/server/internal/html/package.json +1 -1
  130. package/src/stripe/server/internal/html.gen.ts +1 -1
  131. package/src/tempo/Methods.test.ts +131 -0
  132. package/src/tempo/Methods.ts +136 -0
  133. package/src/tempo/Subscription.integration.test.ts +591 -0
  134. package/src/tempo/client/Methods.ts +3 -0
  135. package/src/tempo/client/Subscription.test.ts +131 -0
  136. package/src/tempo/client/Subscription.ts +155 -0
  137. package/src/tempo/client/index.ts +1 -0
  138. package/src/tempo/index.ts +1 -0
  139. package/src/tempo/server/Charge.ts +2 -2
  140. package/src/tempo/server/Methods.ts +5 -0
  141. package/src/tempo/server/Subscription.test.ts +1410 -0
  142. package/src/tempo/server/Subscription.ts +1014 -0
  143. package/src/tempo/server/index.ts +1 -0
  144. package/src/tempo/server/internal/html/package.json +1 -1
  145. package/src/tempo/server/internal/html.gen.ts +1 -1
  146. package/src/tempo/session/Chain.ts +3 -5
  147. package/src/tempo/subscription/KeyAuthorization.test.ts +204 -0
  148. package/src/tempo/subscription/KeyAuthorization.ts +394 -0
  149. package/src/tempo/subscription/Receipt.ts +28 -0
  150. package/src/tempo/subscription/Store.test.ts +554 -0
  151. package/src/tempo/subscription/Store.ts +431 -0
  152. package/src/tempo/subscription/Types.ts +68 -0
  153. package/src/tempo/subscription/index.ts +23 -0
  154. package/src/zod.test.ts +23 -1
  155. package/src/zod.ts +24 -0
@@ -0,0 +1,1014 @@
1
+ import { Base64 } from 'ox'
2
+ import { KeyAuthorization } from 'ox/tempo'
3
+ import { encodeFunctionData, isAddressEqual, type Address, type Client as ViemClient } from 'viem'
4
+ import {
5
+ call as viem_call,
6
+ sendRawTransaction,
7
+ sendRawTransactionSync,
8
+ signTransaction,
9
+ } from 'viem/actions'
10
+ import { tempo as tempo_chain } from 'viem/chains'
11
+ import { Abis, Account as TempoAccount, Transaction } from 'viem/tempo'
12
+
13
+ import { VerificationFailedError } from '../../Errors.js'
14
+ import type { LooseOmit, MaybePromise, NoExtraKeys } from '../../internal/types.js'
15
+ import * as Method from '../../Method.js'
16
+ import * as Store from '../../Store.js'
17
+ import type * as Client from '../../viem/Client.js'
18
+ import * as ClientResolver from '../../viem/Client.js'
19
+ import * as Attribution from '../Attribution.js'
20
+ import * as Account from '../internal/account.js'
21
+ import * as defaults from '../internal/defaults.js'
22
+ import * as Proof from '../internal/proof.js'
23
+ import type * as types from '../internal/types.js'
24
+ import * as Methods from '../Methods.js'
25
+ import {
26
+ assertSubscriptionTiming,
27
+ toSubscriptionPeriodSeconds,
28
+ verifySubscriptionKeyAuthorization,
29
+ } from '../subscription/KeyAuthorization.js'
30
+ import * as SubscriptionReceipt from '../subscription/Receipt.js'
31
+ import * as SubscriptionStore from '../subscription/Store.js'
32
+ import type {
33
+ SubscriptionAccessKey,
34
+ SubscriptionCredentialPayload,
35
+ SubscriptionLookup,
36
+ SubscriptionPeriodUnit,
37
+ SubscriptionRecord,
38
+ SubscriptionReceipt as SubscriptionReceiptValue,
39
+ } from '../subscription/Types.js'
40
+
41
+ type SubscriptionRequest = ReturnType<typeof Methods.subscription.schema.request.parse>
42
+
43
+ /**
44
+ * Creates a Tempo subscription method for recurring TIP-20 token payments.
45
+ *
46
+ * The method handles activation, request-path reuse, and optional lazy renewals.
47
+ */
48
+ export function subscription<const parameters extends subscription.Parameters>(
49
+ p: NoExtraKeys<parameters, subscription.Parameters>,
50
+ ) {
51
+ const parameters = p as parameters
52
+ const rawStore = (parameters.store ?? Store.memory()) as Store.AtomicStore<
53
+ Record<string, unknown>
54
+ >
55
+ if (typeof rawStore.update !== 'function') {
56
+ throw new Error('tempo.subscription() requires an atomic store with `update`.')
57
+ }
58
+ const defaultChainId = parameters.chainId ?? defaults.chainId.testnet
59
+ const {
60
+ amount,
61
+ currency = defaults.resolveCurrency({ chainId: defaultChainId }),
62
+ decimals = defaults.decimals,
63
+ description,
64
+ externalId,
65
+ periodCount,
66
+ periodUnit,
67
+ subscriptionExpires,
68
+ waitForConfirmation = true,
69
+ } = parameters
70
+
71
+ const store = SubscriptionStore.fromStore(rawStore, {
72
+ activationTimeoutMs: parameters.activationTimeoutMs,
73
+ renewalTimeoutMs: parameters.renewalTimeoutMs,
74
+ })
75
+ const { recipient } = Account.resolve(parameters)
76
+ const getClient = ClientResolver.getResolver({
77
+ chain: tempo_chain,
78
+ getClient: parameters.getClient,
79
+ rpcUrl: defaults.rpcUrl,
80
+ })
81
+
82
+ type Defaults = subscription.DeriveDefaults<parameters>
83
+ return Method.toServer<typeof Methods.subscription, Defaults>(Methods.subscription, {
84
+ defaults: {
85
+ amount,
86
+ currency,
87
+ decimals,
88
+ description,
89
+ externalId,
90
+ periodCount,
91
+ periodUnit,
92
+ recipient,
93
+ subscriptionExpires,
94
+ } as unknown as Defaults,
95
+
96
+ async authorize({ input, request }) {
97
+ const resolved = await parameters.resolve({ input, request })
98
+ if (!resolved) return undefined
99
+
100
+ const subscription = await store.getByKey(resolved.key)
101
+ if (!subscription || !isActive(subscription)) return undefined
102
+ if (!subscriptionMatchesRequest(subscription, request)) return undefined
103
+
104
+ const periodIndex = getPeriodIndex(subscription)
105
+ if (periodIndex > subscription.lastChargedPeriod) {
106
+ const renew = resolveRenewalHandler({
107
+ getClient,
108
+ parameters,
109
+ store,
110
+ subscription,
111
+ waitForConfirmation,
112
+ })
113
+ if (!renew) return undefined
114
+
115
+ const renewal = await settleRenewal({
116
+ expectedLookupKey: resolved.key,
117
+ periodIndex,
118
+ renew,
119
+ request,
120
+ store,
121
+ subscription,
122
+ })
123
+ if (!renewal) return undefined
124
+ if (renewal.status === 'charged') return { receipt: renewal.receipt }
125
+ if (renewal.status === 'inFlight') {
126
+ return {
127
+ receipt: renewal.receipt,
128
+ response: new Response(null, {
129
+ headers: { 'Retry-After': '1' },
130
+ status: 409,
131
+ }),
132
+ }
133
+ }
134
+
135
+ await parameters.hooks?.renewed?.({
136
+ periodIndex,
137
+ receipt: renewal.result.receipt,
138
+ subscription: renewal.result.subscription,
139
+ })
140
+ return {
141
+ receipt: renewal.result.receipt,
142
+ }
143
+ }
144
+
145
+ return {
146
+ receipt: SubscriptionReceipt.fromRecord(subscription),
147
+ }
148
+ },
149
+
150
+ async request({ capturedRequest, credential, request }) {
151
+ const credentialRequest = credential?.challenge.request as SubscriptionRequest | undefined
152
+ const chainId =
153
+ request.chainId ??
154
+ parameters.chainId ??
155
+ credentialRequest?.methodDetails?.chainId ??
156
+ defaults.chainId.testnet
157
+ const parsedRequest = Methods.subscription.schema.request.parse({
158
+ ...request,
159
+ chainId,
160
+ })
161
+ const input = requestFromCaptured(capturedRequest)
162
+ const resolved = await parameters.resolve({ input, request: parsedRequest })
163
+ const existing = resolved ? await store.getByKey(resolved.key) : null
164
+ const accessKey =
165
+ resolved && !credential
166
+ ? await resolveChallengeAccessKey({
167
+ existing,
168
+ input,
169
+ parameters,
170
+ request: parsedRequest,
171
+ resolved,
172
+ store,
173
+ })
174
+ : (credentialRequest?.methodDetails?.accessKey ?? parsedRequest.methodDetails?.accessKey)
175
+ if (!accessKey) {
176
+ throw new VerificationFailedError({ reason: 'subscription accessKey is missing' })
177
+ }
178
+
179
+ // Challenges carry the server-generated key in methodDetails so the shared request shape stays spec-compatible.
180
+ return {
181
+ ...request,
182
+ methodDetails: {
183
+ ...request.methodDetails,
184
+ accessKey,
185
+ },
186
+ chainId,
187
+ }
188
+ },
189
+
190
+ stableBinding: subscriptionBinding,
191
+
192
+ async verify({ credential, envelope, request }) {
193
+ const input = requestFromCaptured(envelope?.capturedRequest)
194
+ const parsed = Methods.subscription.schema.request.safeParse(request)
195
+ const parsedRequest = parsed.success ? parsed.data : (request as SubscriptionRequest)
196
+ assertSubscriptionTiming({
197
+ challengeExpires: credential.challenge.expires,
198
+ request: parsedRequest,
199
+ })
200
+ const resolved = await parameters.resolve({ input, request: parsedRequest })
201
+
202
+ if (!resolved) {
203
+ throw new VerificationFailedError({ reason: 'subscription could not be resolved' })
204
+ }
205
+ const challengeRequest = credential.challenge.request as SubscriptionRequest
206
+ const accessKey =
207
+ challengeRequest.methodDetails?.accessKey ??
208
+ parsedRequest.methodDetails?.accessKey ??
209
+ (await resolveAccessKey({ input, parameters, request: parsedRequest, resolved }))
210
+ if (!accessKey) {
211
+ throw new VerificationFailedError({ reason: 'subscription accessKey is missing' })
212
+ }
213
+ const verified = verifySubscriptionKeyAuthorization({
214
+ accessKey,
215
+ chainId: parsedRequest.methodDetails?.chainId ?? defaults.chainId.testnet,
216
+ payload: credential.payload as SubscriptionCredentialPayload,
217
+ request: parsedRequest,
218
+ })
219
+ const declaredSource = credential.source ? Proof.parsePkhSource(credential.source) : null
220
+ if (
221
+ declaredSource &&
222
+ (declaredSource.chainId !== verified.source.chainId ||
223
+ !isAddressEqual(declaredSource.address, verified.source.address))
224
+ ) {
225
+ throw new VerificationFailedError({ reason: 'credential source does not match signature' })
226
+ }
227
+
228
+ const activation = await store.activate({
229
+ challengeId: credential.challenge.id,
230
+ isReusable: (subscription) => isReusableSubscription(subscription, parsedRequest),
231
+ lookupKey: resolved.key,
232
+ async create() {
233
+ const activation = withSubscriptionAccessKey(
234
+ await activateSubscription({
235
+ accessKey,
236
+ auto: {
237
+ challengeId: credential.challenge.id,
238
+ getClient,
239
+ keyAuthorization: (credential.payload as SubscriptionCredentialPayload).signature,
240
+ realm: credential.challenge.realm,
241
+ store,
242
+ waitForConfirmation,
243
+ },
244
+ credential: credential as typeof credential & {
245
+ payload: SubscriptionCredentialPayload
246
+ },
247
+ input,
248
+ parameters,
249
+ request: parsedRequest,
250
+ resolved,
251
+ source: verified.source,
252
+ }),
253
+ accessKey,
254
+ )
255
+ validateSubscriptionSettlement(activation, {
256
+ expectedLookupKey: resolved.key,
257
+ expectedPeriodIndex: 0,
258
+ request: parsedRequest,
259
+ })
260
+ return activation
261
+ },
262
+ })
263
+ if (activation.status === 'replayed') {
264
+ throw new VerificationFailedError({
265
+ reason: 'subscription credential has already been used',
266
+ })
267
+ }
268
+ if (activation.status === 'inFlight') {
269
+ throw new VerificationFailedError({
270
+ reason: 'subscription activation is already in flight',
271
+ })
272
+ }
273
+ if (activation.status === 'claimMismatch') {
274
+ throw new VerificationFailedError({ reason: 'subscription activation claim mismatch' })
275
+ }
276
+ if (activation.status === 'existing') {
277
+ return SubscriptionReceipt.fromRecord(activation.subscription)
278
+ }
279
+
280
+ await parameters.hooks?.activated?.({
281
+ receipt: activation.result.receipt,
282
+ subscription: activation.result.subscription,
283
+ })
284
+ return activation.result.receipt
285
+ },
286
+ })
287
+ }
288
+
289
+ function requestFromCaptured(capturedRequest: Method.CapturedRequest | undefined): Request {
290
+ if (!capturedRequest) return new Request('https://subscription.invalid')
291
+ return new Request(capturedRequest.url, {
292
+ headers: capturedRequest.headers,
293
+ method: capturedRequest.method,
294
+ })
295
+ }
296
+
297
+ async function resolveAccessKey(parameters: {
298
+ input: Request
299
+ parameters: subscription.Parameters
300
+ request: SubscriptionRequest
301
+ resolved: subscription.ResolvedSubscription
302
+ }) {
303
+ const { input, parameters: subscriptionParameters, request, resolved } = parameters
304
+ return (
305
+ resolved.accessKey ??
306
+ (subscriptionParameters.accessKey
307
+ ? await subscriptionParameters.accessKey({ input, request, resolved })
308
+ : undefined)
309
+ )
310
+ }
311
+
312
+ async function resolveChallengeAccessKey(parameters: {
313
+ existing: SubscriptionRecord | null
314
+ input: Request
315
+ parameters: subscription.Parameters
316
+ request: SubscriptionRequest
317
+ resolved: subscription.ResolvedSubscription
318
+ store: SubscriptionStore.SubscriptionStore
319
+ }) {
320
+ const {
321
+ existing,
322
+ input,
323
+ parameters: subscriptionParameters,
324
+ request,
325
+ resolved,
326
+ store,
327
+ } = parameters
328
+ if (!subscriptionParameters.activate) {
329
+ // In automatic mode, the SDK owns the server access key so apps can issue
330
+ // challenges from only their resolved subscription lookup key.
331
+ const accessKey = await store.getOrCreateAccessKey(resolved.key)
332
+ return {
333
+ accessKeyAddress: accessKey.accessKeyAddress,
334
+ keyType: accessKey.keyType,
335
+ } satisfies SubscriptionAccessKey
336
+ }
337
+ // Manual activation keeps the lower-level API: callers can provide the
338
+ // access key for new challenges, while active subscriptions reuse the stored key.
339
+ return (
340
+ (await resolveAccessKey({ input, parameters: subscriptionParameters, request, resolved })) ??
341
+ (existing && isActive(existing) ? existing.accessKey : undefined)
342
+ )
343
+ }
344
+
345
+ async function activateSubscription(parameters: {
346
+ accessKey: SubscriptionAccessKey
347
+ auto: {
348
+ challengeId: string
349
+ getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
350
+ keyAuthorization: `0x${string}`
351
+ realm: string
352
+ store: SubscriptionStore.SubscriptionStore
353
+ waitForConfirmation: boolean
354
+ }
355
+ credential: {
356
+ payload: SubscriptionCredentialPayload
357
+ source?: string | undefined
358
+ }
359
+ input: Request
360
+ parameters: subscription.Parameters
361
+ request: SubscriptionRequest
362
+ resolved: subscription.ResolvedSubscription
363
+ source: { address: Address; chainId: number } | null
364
+ }) {
365
+ const {
366
+ accessKey,
367
+ auto,
368
+ credential,
369
+ input,
370
+ parameters: subscriptionParameters,
371
+ request,
372
+ resolved,
373
+ source,
374
+ } = parameters
375
+ if (subscriptionParameters.activate) {
376
+ // A custom activate hook owns settlement and record creation.
377
+ return subscriptionParameters.activate({
378
+ accessKey,
379
+ credential,
380
+ input,
381
+ request,
382
+ resolved,
383
+ source,
384
+ })
385
+ }
386
+ if (!source) {
387
+ throw new VerificationFailedError({ reason: 'subscription payer is missing' })
388
+ }
389
+
390
+ // Automatic activation bills the first period and persists the recurring
391
+ // billing authority needed for request-path and background renewals.
392
+ const reference = await submitSubscriptionPayment({
393
+ accessKey,
394
+ getClient: auto.getClient,
395
+ keyAuthorization: auto.keyAuthorization,
396
+ lookupKey: resolved.key,
397
+ request,
398
+ settlementReference: auto.challengeId,
399
+ source,
400
+ store: auto.store,
401
+ waitForConfirmation: auto.waitForConfirmation,
402
+ })
403
+ const timestamp = new Date().toISOString()
404
+ const subscription = {
405
+ accessKey,
406
+ amount: request.amount,
407
+ billingAnchor: timestamp,
408
+ chainId: request.methodDetails?.chainId,
409
+ currency: request.currency,
410
+ externalId: request.externalId,
411
+ keyAuthorization: auto.keyAuthorization,
412
+ lastChargedPeriod: 0,
413
+ lookupKey: resolved.key,
414
+ payer: source,
415
+ periodCount: request.periodCount,
416
+ periodUnit: request.periodUnit,
417
+ recipient: request.recipient,
418
+ reference,
419
+ subscriptionExpires: request.subscriptionExpires,
420
+ subscriptionId: createSubscriptionId(),
421
+ timestamp,
422
+ } satisfies SubscriptionRecord
423
+
424
+ return {
425
+ receipt: SubscriptionReceipt.createSubscriptionReceipt(subscription),
426
+ subscription,
427
+ }
428
+ }
429
+
430
+ async function settleRenewal(parameters: {
431
+ expectedLookupKey: string
432
+ periodIndex: number
433
+ renew: (parameters: {
434
+ inFlightReference: string
435
+ periodIndex: number
436
+ subscription: SubscriptionRecord
437
+ }) => Promise<subscription.RenewalResult>
438
+ request?: SubscriptionRequest | undefined
439
+ store: SubscriptionStore.SubscriptionStore
440
+ subscription: SubscriptionRecord
441
+ }): Promise<
442
+ | { status: 'charged'; receipt: SubscriptionReceiptValue }
443
+ | { status: 'inFlight'; receipt: SubscriptionReceiptValue }
444
+ | { status: 'renewed'; result: subscription.RenewalResult }
445
+ | null
446
+ > {
447
+ const { expectedLookupKey, periodIndex, renew, request, store, subscription } = parameters
448
+ const inFlightReference = renewalReference(subscription.subscriptionId, periodIndex)
449
+ const renewal = await store.renew({
450
+ inFlightReference,
451
+ periodIndex,
452
+ async renew({ inFlightReference, periodIndex, subscription: started }) {
453
+ const renewed = withSubscriptionAccessKey(
454
+ await renew({
455
+ inFlightReference,
456
+ periodIndex,
457
+ subscription: started,
458
+ }),
459
+ started.accessKey,
460
+ )
461
+ validateSubscriptionSettlement(renewed, {
462
+ expectedLookupKey,
463
+ expectedPeriodIndex: periodIndex,
464
+ expectedSubscriptionId: subscription.subscriptionId,
465
+ previous: started,
466
+ request,
467
+ })
468
+ return renewed
469
+ },
470
+ subscriptionId: subscription.subscriptionId,
471
+ })
472
+
473
+ if (renewal.status === 'charged') {
474
+ return { receipt: SubscriptionReceipt.fromRecord(renewal.subscription), status: 'charged' }
475
+ }
476
+ if (renewal.status === 'inFlight') {
477
+ return { receipt: SubscriptionReceipt.fromRecord(renewal.subscription), status: 'inFlight' }
478
+ }
479
+ if (renewal.status === 'renewed') return { result: renewal.result, status: 'renewed' }
480
+ if (renewal.status === 'claimMismatch') {
481
+ throw new VerificationFailedError({ reason: 'subscription renewal claim mismatch' })
482
+ }
483
+ return null
484
+ }
485
+
486
+ function renewalReference(subscriptionId: string, periodIndex: number): string {
487
+ // This stable identifier is persisted before the billing hook runs so apps can
488
+ // use it as an idempotency/reconciliation key if a renewal crashes mid-flight.
489
+ return `renewal:${subscriptionId}:${periodIndex}`
490
+ }
491
+
492
+ function withSubscriptionAccessKey<
493
+ result extends subscription.ActivationResult | subscription.RenewalResult,
494
+ >(result: result, accessKey: SubscriptionAccessKey | undefined): result {
495
+ if (!accessKey || result.subscription.accessKey) return result
496
+ return {
497
+ ...result,
498
+ subscription: {
499
+ ...result.subscription,
500
+ accessKey,
501
+ },
502
+ }
503
+ }
504
+
505
+ function getPeriodIndex(subscription: SubscriptionRecord): number {
506
+ const anchor = new Date(subscription.billingAnchor).getTime()
507
+ const expires = new Date(subscription.subscriptionExpires).getTime()
508
+ const now = Date.now()
509
+ if (!Number.isFinite(anchor) || !Number.isFinite(expires) || now >= expires) {
510
+ return Number.POSITIVE_INFINITY
511
+ }
512
+
513
+ let periodSeconds: number
514
+ try {
515
+ periodSeconds = toSubscriptionPeriodSeconds(subscription)
516
+ } catch {
517
+ return Number.POSITIVE_INFINITY
518
+ }
519
+
520
+ return Math.max(0, Math.floor((now - anchor) / (periodSeconds * 1_000)))
521
+ }
522
+
523
+ function isActive(subscription: SubscriptionRecord): boolean {
524
+ if (subscription.canceledAt || subscription.revokedAt) return false
525
+ return new Date(subscription.subscriptionExpires).getTime() > Date.now()
526
+ }
527
+
528
+ function isReusableSubscription(
529
+ subscription: SubscriptionRecord,
530
+ request: SubscriptionRequest,
531
+ ): boolean {
532
+ return (
533
+ isActive(subscription) &&
534
+ getPeriodIndex(subscription) <= subscription.lastChargedPeriod &&
535
+ subscriptionMatchesRequest(subscription, request)
536
+ )
537
+ }
538
+
539
+ function subscriptionMatchesRequest(
540
+ subscription: SubscriptionRecord,
541
+ request: SubscriptionRequest | SubscriptionRecord,
542
+ ): boolean {
543
+ const actual = comparableSubscriptionBinding(subscription)
544
+ const expected = comparableSubscriptionBinding(request)
545
+ return (Object.keys(expected) as (keyof typeof expected)[]).every(
546
+ (key) => actual[key] === expected[key],
547
+ )
548
+ }
549
+
550
+ function comparableSubscriptionBinding(value: SubscriptionRecord | SubscriptionRequest) {
551
+ const chainId =
552
+ 'chainId' in value ? value.chainId : (value as SubscriptionRequest).methodDetails?.chainId
553
+ return {
554
+ amount: value.amount,
555
+ chainId,
556
+ currency: value.currency.toLowerCase(),
557
+ externalId: value.externalId,
558
+ periodCount: value.periodCount,
559
+ periodUnit: value.periodUnit,
560
+ recipient: value.recipient.toLowerCase(),
561
+ subscriptionExpires: value.subscriptionExpires,
562
+ }
563
+ }
564
+
565
+ function validateSubscriptionSettlement(
566
+ result: subscription.ActivationResult | subscription.RenewalResult,
567
+ options: {
568
+ expectedLookupKey: string
569
+ expectedPeriodIndex: number
570
+ expectedSubscriptionId?: string | undefined
571
+ previous?: SubscriptionRecord | undefined
572
+ request?: SubscriptionRequest | undefined
573
+ },
574
+ ) {
575
+ const { receipt, subscription } = result
576
+ assertSubscriptionReceipt(receipt, subscription)
577
+ assertSubscriptionRecord(subscription, options)
578
+
579
+ if (options.request) {
580
+ assertSubscriptionRequestMatch(subscription, options.request)
581
+ } else if (options.previous) {
582
+ assertSubscriptionRequestMatch(subscription, options.previous)
583
+ }
584
+ }
585
+
586
+ function assertSubscriptionReceipt(
587
+ receipt: SubscriptionReceiptValue,
588
+ subscription: SubscriptionRecord,
589
+ ) {
590
+ if (receipt.method !== 'tempo' || receipt.status !== 'success') {
591
+ throw new VerificationFailedError({ reason: 'subscription receipt is invalid' })
592
+ }
593
+ if (receipt.subscriptionId !== subscription.subscriptionId) {
594
+ throw new VerificationFailedError({ reason: 'subscription receipt id mismatch' })
595
+ }
596
+ if (receipt.reference !== subscription.reference) {
597
+ throw new VerificationFailedError({ reason: 'subscription receipt reference mismatch' })
598
+ }
599
+ if (receipt.timestamp !== subscription.timestamp) {
600
+ throw new VerificationFailedError({ reason: 'subscription receipt timestamp mismatch' })
601
+ }
602
+ assertTransactionHash(receipt.reference, 'subscription reference must be a transaction hash')
603
+ assertValidDate(receipt.timestamp, 'subscription receipt timestamp is invalid')
604
+ }
605
+
606
+ function assertSubscriptionRecord(
607
+ subscription: SubscriptionRecord,
608
+ options: {
609
+ expectedLookupKey: string
610
+ expectedPeriodIndex: number
611
+ expectedSubscriptionId?: string | undefined
612
+ },
613
+ ) {
614
+ assertBase64Url(subscription.subscriptionId, 'subscriptionId must be base64url')
615
+ assertTransactionHash(subscription.reference, 'subscription reference must be a transaction hash')
616
+ const billingAnchor = assertValidDate(
617
+ subscription.billingAnchor,
618
+ 'subscription billingAnchor is invalid',
619
+ )
620
+ const subscriptionExpires = assertValidDate(
621
+ subscription.subscriptionExpires,
622
+ 'subscriptionExpires is invalid',
623
+ )
624
+
625
+ assertEqual(subscription.lookupKey, options.expectedLookupKey, {
626
+ reason: 'subscription lookupKey does not match the resolved key',
627
+ })
628
+ assertEqual(subscription.lastChargedPeriod, options.expectedPeriodIndex, {
629
+ reason: 'subscription lastChargedPeriod does not match the settled period',
630
+ })
631
+ if (options.expectedSubscriptionId) {
632
+ assertEqual(subscription.subscriptionId, options.expectedSubscriptionId, {
633
+ reason: 'subscriptionId does not match the active subscription',
634
+ })
635
+ }
636
+ if (billingAnchor >= subscriptionExpires) {
637
+ throw new VerificationFailedError({
638
+ reason: 'subscription billingAnchor must be before subscriptionExpires',
639
+ })
640
+ }
641
+ }
642
+
643
+ function assertSubscriptionRequestMatch(
644
+ subscription: SubscriptionRecord,
645
+ request: SubscriptionRequest | SubscriptionRecord,
646
+ ) {
647
+ if (!subscriptionMatchesRequest(subscription, request)) {
648
+ throw new VerificationFailedError({ reason: 'subscription record does not match request' })
649
+ }
650
+ }
651
+
652
+ function assertBase64Url(value: string, reason: string) {
653
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
654
+ throw new VerificationFailedError({ reason })
655
+ }
656
+ }
657
+
658
+ function assertTransactionHash(value: string, reason: string) {
659
+ if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
660
+ throw new VerificationFailedError({ reason })
661
+ }
662
+ }
663
+
664
+ function assertValidDate(value: string, reason: string) {
665
+ const milliseconds = new Date(value).getTime()
666
+ if (!Number.isFinite(milliseconds)) {
667
+ throw new VerificationFailedError({ reason })
668
+ }
669
+ return milliseconds
670
+ }
671
+
672
+ function assertEqual<value>(actual: value, expected: value, options: { reason: string }) {
673
+ if (actual !== expected) {
674
+ throw new VerificationFailedError(options)
675
+ }
676
+ }
677
+
678
+ function subscriptionBinding(request: SubscriptionRequest) {
679
+ return {
680
+ amount: request.amount,
681
+ chainId: request.methodDetails?.chainId,
682
+ currency: request.currency,
683
+ externalId: request.externalId,
684
+ periodCount: request.periodCount,
685
+ periodUnit: request.periodUnit,
686
+ recipient: request.recipient,
687
+ subscriptionExpires: request.subscriptionExpires,
688
+ }
689
+ }
690
+
691
+ function resolveRenewalHandler(parameters: {
692
+ getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
693
+ parameters: {
694
+ renew?:
695
+ | ((parameters: {
696
+ inFlightReference: string
697
+ periodIndex: number
698
+ subscription: SubscriptionRecord
699
+ }) => Promise<subscription.RenewalResult>)
700
+ | undefined
701
+ }
702
+ store: SubscriptionStore.SubscriptionStore
703
+ subscription: SubscriptionRecord
704
+ waitForConfirmation: boolean
705
+ }):
706
+ | ((parameters: {
707
+ inFlightReference: string
708
+ periodIndex: number
709
+ subscription: SubscriptionRecord
710
+ }) => Promise<subscription.RenewalResult>)
711
+ | undefined {
712
+ const {
713
+ getClient,
714
+ parameters: subscriptionParameters,
715
+ store,
716
+ subscription,
717
+ waitForConfirmation,
718
+ } = parameters
719
+ if (subscriptionParameters.renew) return subscriptionParameters.renew
720
+ if (!subscription.accessKey || !subscription.payer) return undefined
721
+ return async ({ inFlightReference, periodIndex, subscription }) => {
722
+ const reference = await submitSubscriptionPayment({
723
+ accessKey: subscription.accessKey!,
724
+ getClient,
725
+ lookupKey: subscription.lookupKey,
726
+ request: subscription,
727
+ settlementReference: inFlightReference,
728
+ source: subscription.payer!,
729
+ store,
730
+ waitForConfirmation,
731
+ })
732
+ const record = {
733
+ ...subscription,
734
+ lastChargedPeriod: periodIndex,
735
+ reference,
736
+ timestamp: new Date().toISOString(),
737
+ } satisfies SubscriptionRecord
738
+ return {
739
+ receipt: SubscriptionReceipt.createSubscriptionReceipt(record),
740
+ subscription: record,
741
+ }
742
+ }
743
+ }
744
+
745
+ async function submitSubscriptionPayment(parameters: {
746
+ accessKey: SubscriptionAccessKey
747
+ getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
748
+ keyAuthorization?: `0x${string}` | undefined
749
+ lookupKey: string
750
+ request: Pick<SubscriptionRequest, 'amount'> & {
751
+ methodDetails?: { chainId?: number | undefined } | undefined
752
+ } & { currency: Address | string; recipient: Address | string }
753
+ settlementReference: string
754
+ source: { address: Address; chainId: number }
755
+ store: SubscriptionStore.SubscriptionStore
756
+ waitForConfirmation: boolean
757
+ }) {
758
+ const {
759
+ accessKey,
760
+ getClient,
761
+ keyAuthorization,
762
+ lookupKey,
763
+ request,
764
+ settlementReference,
765
+ source,
766
+ store,
767
+ waitForConfirmation,
768
+ } = parameters
769
+ const stored = await store.getAccessKey(lookupKey)
770
+ if (!stored) {
771
+ throw new VerificationFailedError({ reason: 'subscription access key is missing' })
772
+ }
773
+ const rawAccessAccount = TempoAccount.fromSecp256k1(stored.privateKey)
774
+ if (!isAddressEqual(rawAccessAccount.address, accessKey.accessKeyAddress)) {
775
+ throw new VerificationFailedError({
776
+ reason: 'subscription access key does not match stored key',
777
+ })
778
+ }
779
+
780
+ const chainId = request.methodDetails?.chainId ?? source.chainId
781
+ const client = await getClient({ chainId })
782
+ const account = TempoAccount.fromSecp256k1(stored.privateKey, {
783
+ access: source.address,
784
+ })
785
+ const memo = Attribution.encode({
786
+ challengeId: settlementReference,
787
+ serverId: lookupKey,
788
+ })
789
+ const serializedTransaction = await signTransaction(client, {
790
+ account,
791
+ calls: [
792
+ {
793
+ data: encodeFunctionData({
794
+ abi: Abis.tip20,
795
+ functionName: 'transferWithMemo',
796
+ args: [request.recipient as Address, BigInt(request.amount), memo],
797
+ }),
798
+ to: request.currency as Address,
799
+ },
800
+ ],
801
+ chainId,
802
+ ...(keyAuthorization
803
+ ? { keyAuthorization: KeyAuthorization.deserialize(keyAuthorization) }
804
+ : {}),
805
+ } as never)
806
+ const transaction = Transaction.deserialize(
807
+ serializedTransaction as Transaction.TransactionSerializedTempo,
808
+ )
809
+ await viem_call(client, {
810
+ ...transaction,
811
+ account: transaction.from,
812
+ calls: transaction.calls,
813
+ } as never)
814
+
815
+ if (!waitForConfirmation) {
816
+ return sendRawTransaction(client, {
817
+ serializedTransaction: serializedTransaction as Transaction.TransactionSerializedTempo,
818
+ })
819
+ }
820
+
821
+ const receipt = await sendRawTransactionSync(client, {
822
+ serializedTransaction: serializedTransaction as Transaction.TransactionSerializedTempo,
823
+ })
824
+ if (receipt.status !== 'success') {
825
+ throw new VerificationFailedError({
826
+ reason: `subscription transaction reverted: ${receipt.transactionHash}`,
827
+ })
828
+ }
829
+ return receipt.transactionHash
830
+ }
831
+
832
+ function createSubscriptionId() {
833
+ const bytes = new Uint8Array(18)
834
+ globalThis.crypto.getRandomValues(bytes)
835
+ return Base64.fromBytes(bytes, { url: true }).replace(/=+$/, '')
836
+ }
837
+
838
+ /**
839
+ * Renews an overdue subscription outside of the HTTP request path.
840
+ * Intended for cron jobs or background workers that bill subscriptions on a schedule.
841
+ *
842
+ * Returns the renewal result if the subscription was overdue, or `null` if already current.
843
+ */
844
+ export async function renew(parameters: renew.Parameters): Promise<renew.Result | null> {
845
+ const { store: rawStore, waitForConfirmation = true } = parameters
846
+ const store = SubscriptionStore.fromStore(rawStore, {
847
+ renewalTimeoutMs: parameters.renewalTimeoutMs,
848
+ })
849
+ const getClient = ClientResolver.getResolver({
850
+ chain: tempo_chain,
851
+ getClient: parameters.getClient,
852
+ rpcUrl: defaults.rpcUrl,
853
+ })
854
+
855
+ const record = await store.get(parameters.subscriptionId)
856
+ if (!record) return null
857
+ if (!isActive(record)) return null
858
+ const active = await store.getByKey(record.lookupKey)
859
+ if (active?.subscriptionId !== record.subscriptionId) return null
860
+
861
+ const periodIndex = getPeriodIndex(record)
862
+ if (periodIndex <= record.lastChargedPeriod) return null
863
+
864
+ const renew = resolveRenewalHandler({
865
+ getClient,
866
+ parameters,
867
+ store,
868
+ subscription: record,
869
+ waitForConfirmation,
870
+ })
871
+ if (!renew) return null
872
+
873
+ const renewal = await settleRenewal({
874
+ expectedLookupKey: record.lookupKey,
875
+ periodIndex,
876
+ renew,
877
+ store,
878
+ subscription: record,
879
+ })
880
+ return renewal?.status === 'renewed' ? renewal.result : null
881
+ }
882
+
883
+ export declare namespace renew {
884
+ /** Parameters for renewing an overdue subscription outside the request path. */
885
+ type Parameters = {
886
+ /** The subscription to renew. */
887
+ subscriptionId: string
888
+ /** Billing callback — same signature as the `renew` hook on {@link subscription}. */
889
+ renew?:
890
+ | ((parameters: {
891
+ /** Stable idempotency/reconciliation reference persisted before the renewal hook runs. */
892
+ inFlightReference: string
893
+ periodIndex: number
894
+ subscription: SubscriptionRecord
895
+ }) => Promise<subscription.RenewalResult>)
896
+ | undefined
897
+ /** Store containing subscription records. */
898
+ store: Store.AtomicStore<Record<string, unknown>>
899
+ /**
900
+ * Milliseconds before an in-flight renewal lock can be replaced.
901
+ * Keeps concurrent renewal safe while allowing recovery from abandoned attempts.
902
+ */
903
+ renewalTimeoutMs?: number | undefined
904
+ waitForConfirmation?: boolean | undefined
905
+ } & Client.getResolver.Parameters
906
+
907
+ /** Renewal result returned by {@link renew}. */
908
+ type Result = subscription.RenewalResult
909
+ }
910
+
911
+ export declare namespace subscription {
912
+ /** Request-scoped lookup key used to find the active subscription. */
913
+ type ResolvedSubscription = SubscriptionLookup
914
+
915
+ /** Activation result returned after the initial credential is verified. */
916
+ type ActivationResult = {
917
+ receipt: SubscriptionReceiptValue
918
+ subscription: SubscriptionRecord
919
+ }
920
+
921
+ /** Renewal result returned when an overdue subscription is charged. */
922
+ type RenewalResult = {
923
+ receipt: SubscriptionReceiptValue
924
+ subscription: SubscriptionRecord
925
+ }
926
+
927
+ /** Request defaults supported by the subscription method. */
928
+ type Defaults = LooseOmit<
929
+ Method.RequestDefaults<typeof Methods.subscription>,
930
+ 'accessKey' | 'recipient'
931
+ >
932
+
933
+ /** Parameters for configuring a Tempo subscription method. */
934
+ type Parameters = Account.resolve.Parameters &
935
+ Client.getResolver.Parameters & {
936
+ accessKey?:
937
+ | ((parameters: {
938
+ input: Request
939
+ request: SubscriptionRequest
940
+ resolved: ResolvedSubscription
941
+ }) => MaybePromise<SubscriptionAccessKey>)
942
+ | undefined
943
+ /**
944
+ * Milliseconds before an in-flight activation lock can be replaced.
945
+ * Keeps concurrent activation safe while allowing recovery from abandoned attempts.
946
+ */
947
+ activationTimeoutMs?: number | undefined
948
+ /**
949
+ * Milliseconds before an in-flight renewal lock can be replaced.
950
+ * Keeps concurrent renewal safe while allowing recovery from abandoned attempts.
951
+ */
952
+ renewalTimeoutMs?: number | undefined
953
+ activate?:
954
+ | ((parameters: {
955
+ /** Custom activation must verify this access key matches the resolved subscription. */
956
+ accessKey: SubscriptionAccessKey
957
+ credential: {
958
+ payload: SubscriptionCredentialPayload
959
+ source?: string | undefined
960
+ }
961
+ input: Request
962
+ request: SubscriptionRequest
963
+ resolved: ResolvedSubscription
964
+ source: { address: Address; chainId: number } | null
965
+ }) => Promise<ActivationResult>)
966
+ | undefined
967
+ hooks?:
968
+ | {
969
+ activated?:
970
+ | ((parameters: {
971
+ receipt: SubscriptionReceiptValue
972
+ subscription: SubscriptionRecord
973
+ }) => MaybePromise<void>)
974
+ | undefined
975
+ renewed?:
976
+ | ((parameters: {
977
+ periodIndex: number
978
+ receipt: SubscriptionReceiptValue
979
+ subscription: SubscriptionRecord
980
+ }) => MaybePromise<void>)
981
+ | undefined
982
+ }
983
+ | undefined
984
+ periodCount?: Methods.SubscriptionPeriodCountInput | undefined
985
+ periodUnit?: SubscriptionPeriodUnit | undefined
986
+ /**
987
+ * Resolves the request identity. This callback must authenticate and
988
+ * authorize the caller before returning a key; automatic mode may create
989
+ * a server-owned access key for that key while issuing a challenge.
990
+ */
991
+ resolve: (parameters: {
992
+ input: Request
993
+ request: SubscriptionRequest
994
+ }) => MaybePromise<ResolvedSubscription | null>
995
+ renew?: (parameters: {
996
+ /** Stable idempotency/reconciliation reference persisted before the renewal hook runs. */
997
+ inFlightReference: string
998
+ periodIndex: number
999
+ /** Custom renewal hooks must preserve amount, currency, recipient, period, expiry, and lookup key. */
1000
+ subscription: SubscriptionRecord
1001
+ }) => Promise<RenewalResult>
1002
+ store?: Store.AtomicStore<Record<string, unknown>> | undefined
1003
+ testnet?: boolean | undefined
1004
+ waitForConfirmation?: boolean | undefined
1005
+ } & Defaults
1006
+
1007
+ /** Derived defaults after account and chain configuration are applied. */
1008
+ type DeriveDefaults<parameters extends Parameters> = types.DeriveDefaults<
1009
+ parameters,
1010
+ Defaults
1011
+ > & {
1012
+ decimals: number
1013
+ }
1014
+ }