tapimo 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/ApiKey.d.ts +3 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -0
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +7 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +9 -2
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +20434 -19484
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +62 -4
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +84 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +14 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -2
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +2 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +168 -168
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +67 -67
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +2 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +3 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/data/App.d.ts +10305 -12785
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +4 -8
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +24 -24
- package/dist/apps/data/routes/addresses.d.ts +288 -288
- package/dist/apps/data/routes/balances.d.ts +6 -6
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +436 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +631 -4
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +10 -10
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/indexer.d.ts +6 -6
- package/dist/apps/data/routes/indexer.js +4 -1
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +235 -235
- package/dist/apps/data/routes/rpc.d.ts +1 -1
- package/dist/apps/data/routes/rpc.js +4 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/tokens.d.ts +6 -6
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +9 -9
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/funding/Access.d.ts +16 -0
- package/dist/apps/funding/Access.d.ts.map +1 -0
- package/dist/apps/funding/Access.js +22 -0
- package/dist/apps/funding/Access.js.map +1 -0
- package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +46 -0
- package/dist/apps/funding/App.js.map +1 -0
- package/dist/apps/funding/Inventory.d.ts +5 -0
- package/dist/apps/funding/Inventory.d.ts.map +1 -0
- package/dist/apps/funding/Inventory.js +25 -0
- package/dist/apps/funding/Inventory.js.map +1 -0
- package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
- package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
- package/dist/apps/funding/ProviderAttempts.js +24 -0
- package/dist/apps/funding/ProviderAttempts.js.map +1 -0
- package/dist/apps/funding/index.d.ts +2 -0
- package/dist/apps/funding/index.d.ts.map +1 -0
- package/dist/apps/funding/index.js +2 -0
- package/dist/apps/funding/index.js.map +1 -0
- package/dist/apps/funding/routes/chains.d.ts +169 -0
- package/dist/apps/funding/routes/chains.d.ts.map +1 -0
- package/dist/apps/funding/routes/chains.js +80 -0
- package/dist/apps/funding/routes/chains.js.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +860 -0
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
- package/dist/apps/funding/routes/providers.d.ts +91 -0
- package/dist/apps/funding/routes/providers.d.ts.map +1 -0
- package/dist/apps/funding/routes/providers.js +56 -0
- package/dist/apps/funding/routes/providers.js.map +1 -0
- package/dist/apps/funding/routes/quotes.d.ts +426 -0
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
- package/dist/apps/funding/routes/quotes.js +188 -0
- package/dist/apps/funding/routes/quotes.js.map +1 -0
- package/dist/apps/funding/routes/transfers.d.ts +1999 -0
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
- package/dist/apps/funding/routes/transfers.js +614 -0
- package/dist/apps/funding/routes/transfers.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +430 -165
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +298 -21
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +75 -1
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +6 -6
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/invitations.d.ts +48 -48
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +37 -37
- package/dist/apps/management/routes/projects.d.ts +25 -25
- package/dist/apps/management/routes/scopes.d.ts +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
- package/dist/apps/management/routes/webhooks.d.ts +128 -128
- package/dist/apps/mcp.d.ts +2 -3
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +16 -14
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/metadata.d.ts +4 -0
- package/dist/apps/metadata.d.ts.map +1 -1
- package/dist/apps/metadata.js +10 -0
- package/dist/apps/metadata.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +2 -2
- package/dist/cloudflare.d.ts +50 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +178 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +37 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +20 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +12 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
- package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
- package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
- package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
- package/dist/db/tables/fundingDeposits.d.ts +57 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +82 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +11 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +41 -15
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.js +1 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +2 -2
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +10 -4
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/IpAllowlist.d.ts +15 -0
- package/dist/internal/IpAllowlist.d.ts.map +1 -0
- package/dist/internal/IpAllowlist.js +27 -0
- package/dist/internal/IpAllowlist.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +1 -1
- package/dist/internal/OpenApi.js +1 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Tidx.d.ts +6 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +8 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -5
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +42 -0
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +37 -5
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts +1 -5
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +4 -3
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +102 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +5 -0
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts +16 -2
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +257 -48
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts +108 -0
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
- package/dist/internal/funding/SourceObservation.js +143 -0
- package/dist/internal/funding/SourceObservation.js.map +1 -0
- package/dist/internal/funding/Subsidy.d.ts +2 -2
- package/dist/internal/funding/Subsidy.d.ts.map +1 -1
- package/dist/internal/funding/Subsidy.js +1 -1
- package/dist/internal/funding/Subsidy.js.map +1 -1
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +43 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +434 -27
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKey.test.ts +35 -0
- package/src/ApiKey.ts +27 -0
- package/src/ApiKeys.test.ts +29 -0
- package/src/ApiKeys.ts +11 -3
- package/src/App.test.ts +23 -5
- package/src/App.ts +76 -5
- package/src/Client.test-d.ts +171 -19
- package/src/admin/apps/api-keys.test.ts +25 -1
- package/src/admin/apps/api-keys.ts +6 -2
- package/src/admin/ui.generated.ts +67 -67
- package/src/analytics/Analytics.ts +4 -0
- package/src/apps/composition.test.ts +100 -2
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/earn.test.ts +283 -0
- package/src/apps/data/routes/earn.ts +800 -3
- package/src/apps/data/routes/indexer.ts +2 -1
- package/src/apps/data/routes/rpc.ts +2 -0
- package/src/apps/data/routes/tokens.test.ts +6 -8
- package/src/apps/funding/Access.ts +26 -0
- package/src/apps/funding/App.test.ts +78 -0
- package/src/apps/funding/App.ts +63 -0
- package/src/apps/funding/Inventory.ts +30 -0
- package/src/apps/funding/ProviderAttempts.ts +26 -0
- package/src/apps/funding/index.ts +1 -0
- package/src/apps/funding/routes/chains.test.ts +78 -0
- package/src/apps/funding/routes/chains.ts +101 -0
- package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
- package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
- package/src/apps/funding/routes/providers.test.ts +165 -0
- package/src/apps/funding/routes/providers.ts +73 -0
- package/src/apps/funding/routes/quotes.test.ts +511 -0
- package/src/apps/funding/routes/quotes.ts +205 -0
- package/src/apps/funding/routes/transfers.test.ts +1255 -0
- package/src/apps/funding/routes/transfers.ts +704 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +98 -0
- package/src/apps/management/routes/api-keys.ts +96 -1
- package/src/apps/mcp.test.ts +17 -2
- package/src/apps/mcp.ts +18 -13
- package/src/apps/metadata.ts +12 -0
- package/src/apps/mpp/internal/Policy.test.ts +7 -1
- package/src/cloudflare.ts +279 -0
- package/src/db/Db.ts +62 -0
- package/src/db/Schema.ts +24 -0
- package/src/db/tables/fundingDepositAddresses.ts +28 -0
- package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
- package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
- package/src/db/tables/fundingDeposits.test.ts +138 -0
- package/src/db/tables/fundingDeposits.ts +145 -1
- package/src/internal/Auth.test.ts +177 -5
- package/src/internal/Auth.ts +48 -18
- package/src/internal/Cache.test.ts +37 -3
- package/src/internal/Cache.ts +1 -1
- package/src/internal/EdgeCache.test.ts +7 -7
- package/src/internal/EdgeCache.ts +13 -4
- package/src/internal/IpAllowlist.test.ts +52 -0
- package/src/internal/IpAllowlist.ts +39 -0
- package/src/internal/Log.test.ts +7 -1
- package/src/internal/OpenApi.ts +1 -1
- package/src/internal/Tidx.test.ts +28 -0
- package/src/internal/Tidx.ts +19 -3
- package/src/internal/Viem.test.ts +28 -0
- package/src/internal/Viem.ts +21 -5
- package/src/internal/funding/Deposit.ts +74 -5
- package/src/internal/funding/Evidence.test.ts +2 -2
- package/src/internal/funding/Evidence.ts +4 -9
- package/src/internal/funding/Provider.ts +115 -0
- package/src/internal/funding/Reconciliation.local.test.ts +7 -0
- package/src/internal/funding/Reconciliation.test.ts +1052 -35
- package/src/internal/funding/Reconciliation.ts +328 -56
- package/src/internal/funding/SourceObservation.test.ts +301 -0
- package/src/internal/funding/SourceObservation.ts +267 -0
- package/src/internal/funding/Subsidy.ts +2 -2
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +94 -4
- package/src/internal/funding/providers/relay.ts +528 -27
- package/dist/apps/data/routes/funding.d.ts.map +0 -1
- package/dist/apps/data/routes/funding.js +0 -1557
- package/dist/apps/data/routes/funding.js.map +0 -1
- package/src/apps/data/routes/funding.test.ts +0 -2994
- package/src/apps/data/routes/funding.ts +0 -1821
|
@@ -359,9 +359,13 @@ export class ConfigError extends Error {
|
|
|
359
359
|
|
|
360
360
|
/** Failed ClickHouse insert (non-2xx HTTP response). */
|
|
361
361
|
export class InsertError extends Error {
|
|
362
|
+
/** HTTP response status returned by ClickHouse. */
|
|
363
|
+
status: number
|
|
364
|
+
|
|
362
365
|
constructor(status: number, body: string) {
|
|
363
366
|
super(`ClickHouse insert failed with status ${status}: ${body.slice(0, 500)}`)
|
|
364
367
|
this.name = 'Analytics.InsertError'
|
|
368
|
+
this.status = status
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { App, Store } from 'tapimo'
|
|
2
|
-
import { data, management, mpp, relay } from 'tapimo/apps'
|
|
2
|
+
import { data, funding, management, mpp, relay } from 'tapimo/apps'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { describeRoute } from 'hono-openapi'
|
|
5
5
|
import * as TestApp from '../../test/App.js'
|
|
@@ -14,7 +14,10 @@ async function document(app: { request: (path: string) => Promise<Response> | Re
|
|
|
14
14
|
const res = await app.request('/openapi.json')
|
|
15
15
|
return (await res.json()) as {
|
|
16
16
|
components: { securitySchemes: Record<string, unknown> }
|
|
17
|
-
paths: Record<
|
|
17
|
+
paths: Record<
|
|
18
|
+
string,
|
|
19
|
+
Partial<Record<'get' | 'post', { operationId: string; tags?: readonly string[] | undefined }>>
|
|
20
|
+
>
|
|
18
21
|
tags: readonly { description: string; name: string }[]
|
|
19
22
|
webhooks?: Record<string, unknown> | undefined
|
|
20
23
|
'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
|
|
@@ -146,10 +149,12 @@ describe('data()', () => {
|
|
|
146
149
|
expect(keys.some((path) => path.startsWith('/v1/blocks'))).toBe(true)
|
|
147
150
|
expect(keys.some((path) => path.startsWith('/v1/orgs'))).toBe(false)
|
|
148
151
|
expect(keys.some((path) => path.startsWith('/v1/me'))).toBe(false)
|
|
152
|
+
expect(keys.some((path) => path.startsWith('/v1/funding'))).toBe(false)
|
|
149
153
|
|
|
150
154
|
// The omitted management route resolves to a 404 (never mounted), not a 401.
|
|
151
155
|
const orgs = await app.request('/v1/orgs')
|
|
152
156
|
expect(orgs.status).toBe(404)
|
|
157
|
+
expect((await app.request('/v1/funding/chains')).status).toBe(404)
|
|
153
158
|
})
|
|
154
159
|
|
|
155
160
|
test('inherits shared chain context', async () => {
|
|
@@ -272,6 +277,99 @@ describe('data()', () => {
|
|
|
272
277
|
})
|
|
273
278
|
})
|
|
274
279
|
|
|
280
|
+
describe('funding()', () => {
|
|
281
|
+
test('mounts the Funding & Bridge API independently', async () => {
|
|
282
|
+
const group = funding()
|
|
283
|
+
expectTypeOf(App.funding).toEqualTypeOf<typeof funding>()
|
|
284
|
+
expectTypeOf(group).toEqualTypeOf<App.FundingApp>()
|
|
285
|
+
const app = App.create({ auth: false, db: TestApp.database() }).route('/', group)
|
|
286
|
+
const spec = await document(app)
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
Object.keys(spec.paths)
|
|
290
|
+
.filter((path) => path.startsWith('/v1/funding'))
|
|
291
|
+
.sort(),
|
|
292
|
+
).toEqual(
|
|
293
|
+
[
|
|
294
|
+
'/v1/funding/chains',
|
|
295
|
+
'/v1/funding/deposit-addresses',
|
|
296
|
+
'/v1/funding/deposit-addresses/{id}',
|
|
297
|
+
'/v1/funding/deposit-addresses/{id}/deposits',
|
|
298
|
+
'/v1/funding/deposit-addresses/{id}/reconcile',
|
|
299
|
+
'/v1/funding/deposits',
|
|
300
|
+
'/v1/funding/deposits/{id}',
|
|
301
|
+
'/v1/funding/providers',
|
|
302
|
+
'/v1/funding/quotes',
|
|
303
|
+
'/v1/funding/transfers',
|
|
304
|
+
'/v1/funding/transfers/vault',
|
|
305
|
+
'/v1/funding/transfers/zone',
|
|
306
|
+
'/v1/funding/transfers/{id}',
|
|
307
|
+
].sort(),
|
|
308
|
+
)
|
|
309
|
+
expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/tokens'))).toBe(false)
|
|
310
|
+
expect(spec.paths['/webhooks/funding/{providerId}']).toBeUndefined()
|
|
311
|
+
expect(spec['x-tagGroups']).toEqual([
|
|
312
|
+
{
|
|
313
|
+
name: 'Funding & Bridge API',
|
|
314
|
+
tags: ['Chains', 'Deposit Addresses', 'Providers', 'Quotes', 'Transfers'],
|
|
315
|
+
},
|
|
316
|
+
])
|
|
317
|
+
expect(
|
|
318
|
+
Object.values(spec.paths).flatMap((path) =>
|
|
319
|
+
[path.get, path.post].flatMap((operation) =>
|
|
320
|
+
operation?.tags?.includes('Deposit Addresses') ? [operation.operationId] : [],
|
|
321
|
+
),
|
|
322
|
+
),
|
|
323
|
+
).toEqual([
|
|
324
|
+
'createFundingDepositAddress',
|
|
325
|
+
'reconcileFundingDepositAddress',
|
|
326
|
+
'getFundingDepositAddress',
|
|
327
|
+
'getFundingDeposit',
|
|
328
|
+
'listFundingDepositAddressDeposits',
|
|
329
|
+
'listFundingDeposits',
|
|
330
|
+
])
|
|
331
|
+
expect((await app.request('/v1/tokens')).status).toBe(404)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('maps operations to five tags and reuses Transfers across groups', async () => {
|
|
335
|
+
const app = App.create({ auth: false, db: TestApp.database() })
|
|
336
|
+
.route('/', data())
|
|
337
|
+
.route('/', funding())
|
|
338
|
+
const spec = await document(app)
|
|
339
|
+
const operationTag = (path: string, method: 'get' | 'post') =>
|
|
340
|
+
(spec.paths[path] as Record<string, { tags?: readonly string[] | undefined } | undefined>)[
|
|
341
|
+
method
|
|
342
|
+
]?.tags
|
|
343
|
+
|
|
344
|
+
expect({
|
|
345
|
+
chains: operationTag('/v1/funding/chains', 'get'),
|
|
346
|
+
depositAddress: operationTag('/v1/funding/deposit-addresses', 'post'),
|
|
347
|
+
deposits: operationTag('/v1/funding/deposit-addresses/{id}/deposits', 'get'),
|
|
348
|
+
providers: operationTag('/v1/funding/providers', 'get'),
|
|
349
|
+
quotes: operationTag('/v1/funding/quotes', 'get'),
|
|
350
|
+
transfers: operationTag('/v1/funding/transfers', 'get'),
|
|
351
|
+
vault: operationTag('/v1/funding/transfers/vault', 'post'),
|
|
352
|
+
zone: operationTag('/v1/funding/transfers/zone', 'post'),
|
|
353
|
+
}).toEqual({
|
|
354
|
+
chains: ['Chains'],
|
|
355
|
+
depositAddress: ['Deposit Addresses'],
|
|
356
|
+
deposits: ['Deposit Addresses'],
|
|
357
|
+
providers: ['Providers'],
|
|
358
|
+
quotes: ['Quotes'],
|
|
359
|
+
transfers: ['Transfers'],
|
|
360
|
+
vault: ['Transfers'],
|
|
361
|
+
zone: ['Transfers'],
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const groups = Object.fromEntries(
|
|
365
|
+
(spec['x-tagGroups'] ?? []).map((group) => [group.name, group.tags]),
|
|
366
|
+
)
|
|
367
|
+
expect(groups['Data API']).toContain('Transfers')
|
|
368
|
+
expect(groups['Funding & Bridge API']).toContain('Transfers')
|
|
369
|
+
expect(spec.tags.filter((tag) => tag.name === 'Transfers')).toHaveLength(1)
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
275
373
|
describe('relay()', () => {
|
|
276
374
|
test('mounts the relay after data() without documenting RPC routes', async () => {
|
|
277
375
|
const composed = App.create({ db: TestApp.database() }).route('/', data()).route('/', relay())
|
|
@@ -386,18 +386,6 @@ describe('zone chain gate', () => {
|
|
|
386
386
|
expect((await privateApp.request(path)).status).toBe(403)
|
|
387
387
|
})
|
|
388
388
|
|
|
389
|
-
test('public chains remain edge-cacheable on a Zone deployment', async () => {
|
|
390
|
-
const store = Store.memory()
|
|
391
|
-
const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
|
|
392
|
-
const path = '/v1/funding/chains'
|
|
393
|
-
|
|
394
|
-
const first = await app.request(path)
|
|
395
|
-
const second = await app.request(path)
|
|
396
|
-
expect(first.status).toBe(200)
|
|
397
|
-
expect(second.status).toBe(200)
|
|
398
|
-
expect(second.headers.get('cache-control')).toContain('public')
|
|
399
|
-
})
|
|
400
|
-
|
|
401
389
|
test('serves anonymous token lists from the edge cache on public chains', async () => {
|
|
402
390
|
const store = Store.memory()
|
|
403
391
|
const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
|
package/src/apps/data/App.ts
CHANGED
|
@@ -25,7 +25,6 @@ import { coingecko } from './routes/coingecko.js'
|
|
|
25
25
|
import { earn } from './routes/earn.js'
|
|
26
26
|
import { exchanges } from './routes/exchanges.js'
|
|
27
27
|
import { feeAmm } from './routes/fee-amm.js'
|
|
28
|
-
import { funding } from './routes/funding.js'
|
|
29
28
|
import { indexer } from './routes/indexer.js'
|
|
30
29
|
import { receipts } from './routes/receipts.js'
|
|
31
30
|
import { rpc } from './routes/rpc.js'
|
|
@@ -217,7 +216,6 @@ export function data(options: data.Options = {}) {
|
|
|
217
216
|
.route('/', earn_routes)
|
|
218
217
|
.route('/', exchanges({ fx: options.fx }))
|
|
219
218
|
.route('/', feeAmm())
|
|
220
|
-
.route('/', funding())
|
|
221
219
|
.route('/', indexer_routes)
|
|
222
220
|
.route('/', receipts({ fx: options.fx }))
|
|
223
221
|
.route('/', tokenlist())
|
|
@@ -375,13 +373,12 @@ const openapi = {
|
|
|
375
373
|
{ name: 'Earn', description: 'Vaults that earn yield from assets deposited on Tempo.' },
|
|
376
374
|
{ name: 'Exchange', description: "Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices." }, // prettier-ignore
|
|
377
375
|
{ name: 'Fee AMM', description: 'Pools that convert stablecoins to pay fees.' },
|
|
378
|
-
{
|
|
379
|
-
name: 'Funding',
|
|
380
|
-
description: 'Quote, transfer, and deposit management for funding Tempo from other chains.',
|
|
381
|
-
},
|
|
382
376
|
{ name: 'Tokens', description: 'TIP-20 token details, supply, and holders.' },
|
|
383
377
|
{ name: 'Transactions', description: 'Transactions submitted to Tempo, and their receipts.' },
|
|
384
|
-
{
|
|
378
|
+
{
|
|
379
|
+
name: 'Transfers',
|
|
380
|
+
description: 'Token movements from executing transactions.',
|
|
381
|
+
},
|
|
385
382
|
{ name: 'Verified Tokens', description: 'A curated, trusted list of TIP-20 tokens.' },
|
|
386
383
|
{ name: 'Webhooks', description: 'Get signed callbacks when onchain events happen.' },
|
|
387
384
|
{ name: 'Zones', description: 'Private chain operations anchored to Tempo.' },
|
|
@@ -399,7 +396,6 @@ const openapi = {
|
|
|
399
396
|
'Earn',
|
|
400
397
|
'Exchange',
|
|
401
398
|
'Fee AMM',
|
|
402
|
-
'Funding',
|
|
403
399
|
'Tokens',
|
|
404
400
|
'Transactions',
|
|
405
401
|
'Transfers',
|
|
@@ -87,6 +87,166 @@ describe('schema.getEarnVaults.Query', () => {
|
|
|
87
87
|
})
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
+
describe('schema.getEarnVaultEarnings.Response', () => {
|
|
91
|
+
test('enforces status-specific earnings fields', () => {
|
|
92
|
+
const response = {
|
|
93
|
+
account: TestEarn.earningsAddress,
|
|
94
|
+
assetToken: TestEarn.assetTokenAddress,
|
|
95
|
+
currentValue: '1000000',
|
|
96
|
+
id: TestEarn.earningsAddress,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expect({
|
|
100
|
+
active: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
101
|
+
...response,
|
|
102
|
+
activeEarnings: '0',
|
|
103
|
+
period: 'active',
|
|
104
|
+
status: 'complete',
|
|
105
|
+
}).success,
|
|
106
|
+
activeWithLifetimeEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
107
|
+
...response,
|
|
108
|
+
lifetimeEarnings: '0',
|
|
109
|
+
period: 'active',
|
|
110
|
+
status: 'complete',
|
|
111
|
+
}).success,
|
|
112
|
+
lifetime: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
113
|
+
...response,
|
|
114
|
+
lifetimeEarnings: '0',
|
|
115
|
+
period: 'lifetime',
|
|
116
|
+
status: 'complete',
|
|
117
|
+
}).success,
|
|
118
|
+
completeWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
119
|
+
...response,
|
|
120
|
+
period: 'lifetime',
|
|
121
|
+
status: 'complete',
|
|
122
|
+
}).success,
|
|
123
|
+
incomplete: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
124
|
+
...response,
|
|
125
|
+
period: 'active',
|
|
126
|
+
status: 'incomplete_cost_basis',
|
|
127
|
+
}).success,
|
|
128
|
+
incompleteWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
129
|
+
...response,
|
|
130
|
+
lifetimeEarnings: '0',
|
|
131
|
+
period: 'lifetime',
|
|
132
|
+
status: 'incomplete_cost_basis',
|
|
133
|
+
}).success,
|
|
134
|
+
pending: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
135
|
+
...response,
|
|
136
|
+
period: 'lifetime',
|
|
137
|
+
status: 'pending_redemption',
|
|
138
|
+
}).success,
|
|
139
|
+
pendingActive: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
140
|
+
...response,
|
|
141
|
+
period: 'active',
|
|
142
|
+
status: 'pending_redemption',
|
|
143
|
+
}).success,
|
|
144
|
+
pendingWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
|
|
145
|
+
...response,
|
|
146
|
+
lifetimeEarnings: '0',
|
|
147
|
+
period: 'lifetime',
|
|
148
|
+
status: 'pending_redemption',
|
|
149
|
+
}).success,
|
|
150
|
+
}).toMatchInlineSnapshot(`
|
|
151
|
+
{
|
|
152
|
+
"active": true,
|
|
153
|
+
"activeWithLifetimeEarnings": false,
|
|
154
|
+
"completeWithoutEarnings": false,
|
|
155
|
+
"incomplete": true,
|
|
156
|
+
"incompleteWithEarnings": false,
|
|
157
|
+
"lifetime": true,
|
|
158
|
+
"pending": true,
|
|
159
|
+
"pendingActive": false,
|
|
160
|
+
"pendingWithEarnings": false,
|
|
161
|
+
}
|
|
162
|
+
`)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('schema.getEarnVaultEarnings.Query', () => {
|
|
167
|
+
test('defaults to lifetime and accepts active earnings', () => {
|
|
168
|
+
expect([
|
|
169
|
+
Earn.schema.getEarnVaultEarnings.Query.parse({}).period,
|
|
170
|
+
Earn.schema.getEarnVaultEarnings.Query.parse({ period: 'active' }).period,
|
|
171
|
+
]).toStrictEqual(['lifetime', 'active'])
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('calculateActiveCostBasis', () => {
|
|
176
|
+
test('allocates weighted-average basis across partial exits', () => {
|
|
177
|
+
expect(
|
|
178
|
+
Earn.calculateActiveCostBasis({
|
|
179
|
+
actions: [
|
|
180
|
+
{ assets: 100n, kind: 'deposit', shares: 100n },
|
|
181
|
+
{ assets: 300n, kind: 'deposit', shares: 100n },
|
|
182
|
+
{ kind: 'redeem', shares: 100n },
|
|
183
|
+
],
|
|
184
|
+
shareBalance: 100n,
|
|
185
|
+
}),
|
|
186
|
+
).toBe(200n)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('calculates remaining basis while an async redemption is pending', () => {
|
|
190
|
+
expect(
|
|
191
|
+
Earn.calculateActiveCostBasis({
|
|
192
|
+
actions: [
|
|
193
|
+
{ assets: 100n, kind: 'deposit', shares: 100n },
|
|
194
|
+
{ kind: 'request', requestId: 'request-1', shares: 40n },
|
|
195
|
+
],
|
|
196
|
+
shareBalance: 60n,
|
|
197
|
+
}),
|
|
198
|
+
).toBe(60n)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('restores removed basis when an async redemption is cancelled', () => {
|
|
202
|
+
expect(
|
|
203
|
+
Earn.calculateActiveCostBasis({
|
|
204
|
+
actions: [
|
|
205
|
+
{ assets: 100n, kind: 'deposit', shares: 100n },
|
|
206
|
+
{ kind: 'request', requestId: 'request-1', shares: 40n },
|
|
207
|
+
{ kind: 'cancel', requestId: 'request-1', shares: 40n },
|
|
208
|
+
],
|
|
209
|
+
shareBalance: 100n,
|
|
210
|
+
}),
|
|
211
|
+
).toBe(100n)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('recovers after unknown-basis shares fully exit', () => {
|
|
215
|
+
expect(
|
|
216
|
+
Earn.calculateActiveCostBasis({
|
|
217
|
+
actions: [
|
|
218
|
+
{ kind: 'unknown', shares: 100n },
|
|
219
|
+
{ kind: 'redeem', shares: 100n },
|
|
220
|
+
{ assets: 200n, kind: 'deposit', shares: 100n },
|
|
221
|
+
],
|
|
222
|
+
shareBalance: 100n,
|
|
223
|
+
}),
|
|
224
|
+
).toBe(200n)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('rejects remaining shares with unknown cost basis', () => {
|
|
228
|
+
expect(
|
|
229
|
+
Earn.calculateActiveCostBasis({
|
|
230
|
+
actions: [
|
|
231
|
+
{ assets: 100n, kind: 'deposit', shares: 100n },
|
|
232
|
+
{ kind: 'unknown', shares: 100n },
|
|
233
|
+
{ kind: 'redeem', shares: 100n },
|
|
234
|
+
],
|
|
235
|
+
shareBalance: 100n,
|
|
236
|
+
}),
|
|
237
|
+
).toBeUndefined()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('rejects history that does not reconcile to the indexed share balance', () => {
|
|
241
|
+
expect(
|
|
242
|
+
Earn.calculateActiveCostBasis({
|
|
243
|
+
actions: [{ assets: 100n, kind: 'deposit', shares: 100n }],
|
|
244
|
+
shareBalance: 99n,
|
|
245
|
+
}),
|
|
246
|
+
).toBeUndefined()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
90
250
|
describe('discoveryBatchLimit', () => {
|
|
91
251
|
test('fetches one extra candidate without live filters', () => {
|
|
92
252
|
expect(
|
|
@@ -864,6 +1024,112 @@ describe('GET /v1/earn/vaults/:vaultId/positions/:address', () => {
|
|
|
864
1024
|
)
|
|
865
1025
|
})
|
|
866
1026
|
|
|
1027
|
+
describe('GET /v1/earn/vaults/:vaultId/earnings/:address', () => {
|
|
1028
|
+
test('returns chain selection errors', async () => {
|
|
1029
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1030
|
+
const invalid = await app.request(
|
|
1031
|
+
`/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=abc`,
|
|
1032
|
+
)
|
|
1033
|
+
const unsupported = await app.request(
|
|
1034
|
+
`/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=999999`,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
expect([
|
|
1038
|
+
(await TestApp.json(invalid, Schema.ErrorResponse)).error.code,
|
|
1039
|
+
(await TestApp.json(unsupported, Schema.ErrorResponse)).error.code,
|
|
1040
|
+
]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported'])
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
test('rejects malformed vault earnings parameters', async () => {
|
|
1044
|
+
const response = await TestApp.create({ auth: false, cache }).request(
|
|
1045
|
+
'/v1/earn/vaults/not-an-address/earnings/not-an-address',
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
expect(response.status).toBe(400)
|
|
1049
|
+
expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
|
|
1050
|
+
error: { code: 'earnings_invalid' },
|
|
1051
|
+
})
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
1055
|
+
'returns lifetime return through indexed vault events',
|
|
1056
|
+
async () => {
|
|
1057
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1058
|
+
const response = await app.request(
|
|
1059
|
+
`/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
expect(response.status).toBe(200)
|
|
1063
|
+
const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
|
|
1064
|
+
if (earnings.status !== 'complete') throw new Error('Expected complete earnings history.')
|
|
1065
|
+
expect(earnings).toMatchObject({
|
|
1066
|
+
account: TestEarn.earningsAddress,
|
|
1067
|
+
assetToken: TestEarn.assetTokenAddress,
|
|
1068
|
+
currentValue: expect.stringMatching(/^\d+$/),
|
|
1069
|
+
id: TestEarn.earningsAddress,
|
|
1070
|
+
lifetimeEarnings: expect.stringMatching(/^-?\d+$/),
|
|
1071
|
+
period: 'lifetime',
|
|
1072
|
+
status: 'complete',
|
|
1073
|
+
})
|
|
1074
|
+
},
|
|
1075
|
+
60_000,
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
1079
|
+
'returns earnings on shares still held',
|
|
1080
|
+
async () => {
|
|
1081
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1082
|
+
const response = await app.request(
|
|
1083
|
+
`/v1/earn/vaults/${TestEarn.activeEarningsVaultAddress}/earnings/${TestEarn.activeEarningsAddress}?chainId=${TestEarn.chain.id}&period=active`,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
expect(response.status).toBe(200)
|
|
1087
|
+
const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
|
|
1088
|
+
if (earnings.status !== 'complete' || earnings.period !== 'active')
|
|
1089
|
+
throw new Error('Expected complete active earnings history.')
|
|
1090
|
+
expect(earnings).toMatchObject({
|
|
1091
|
+
account: TestEarn.activeEarningsAddress,
|
|
1092
|
+
activeEarnings: expect.stringMatching(/^-?\d+$/),
|
|
1093
|
+
period: 'active',
|
|
1094
|
+
status: 'complete',
|
|
1095
|
+
})
|
|
1096
|
+
expect(earnings).not.toHaveProperty('lifetimeEarnings')
|
|
1097
|
+
},
|
|
1098
|
+
60_000,
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
1102
|
+
'omits lifetime earnings when venue shares make the cost basis incomplete',
|
|
1103
|
+
async () => {
|
|
1104
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1105
|
+
const response = await app.request(
|
|
1106
|
+
`/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.incompleteEarningsAddress}?chainId=${TestEarn.chain.id}`,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
expect(response.status).toBe(200)
|
|
1110
|
+
const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
|
|
1111
|
+
expect(earnings.status).toBe('incomplete_cost_basis')
|
|
1112
|
+
expect(earnings).not.toHaveProperty('lifetimeEarnings')
|
|
1113
|
+
},
|
|
1114
|
+
60_000,
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
1118
|
+
'returns not found for an address without an Earn vault',
|
|
1119
|
+
async () => {
|
|
1120
|
+
const response = await TestApp.create({ auth: false, cache }).request(
|
|
1121
|
+
`/v1/earn/vaults/0x1111111111111111111111111111111111111111/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`,
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
expect(response.status).toBe(404)
|
|
1125
|
+
expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
|
|
1126
|
+
error: { code: 'earn_vault_not_found' },
|
|
1127
|
+
})
|
|
1128
|
+
},
|
|
1129
|
+
60_000,
|
|
1130
|
+
)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
867
1133
|
describe('GET /v1/earn/vaults', () => {
|
|
868
1134
|
test.runIf(Runtime.get().mode === 'testnet')(
|
|
869
1135
|
'extracts an indexed deployment and applies onchain filters',
|
|
@@ -1039,6 +1305,7 @@ describe('OpenAPI', () => {
|
|
|
1039
1305
|
'/v1/earn/vaults',
|
|
1040
1306
|
'/v1/earn/vaults/verified',
|
|
1041
1307
|
'/v1/earn/vaults/{vaultId}',
|
|
1308
|
+
'/v1/earn/vaults/{vaultId}/earnings/{address}',
|
|
1042
1309
|
'/v1/earn/vaults/{vaultId}/positions/{address}',
|
|
1043
1310
|
]),
|
|
1044
1311
|
)
|
|
@@ -1105,4 +1372,20 @@ describe('OpenAPI', () => {
|
|
|
1105
1372
|
expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress])
|
|
1106
1373
|
expect(vaultId?.schema?.examples).toStrictEqual([TestEarn.mainnetVaultAddress])
|
|
1107
1374
|
})
|
|
1375
|
+
|
|
1376
|
+
test('documents vault-earnings variants', async () => {
|
|
1377
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1378
|
+
const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
|
|
1379
|
+
const operation = spec.paths['/v1/earn/vaults/{vaultId}/earnings/{address}']?.get
|
|
1380
|
+
const response = JSON.stringify(operation?.responses)
|
|
1381
|
+
|
|
1382
|
+
expect(response).toContain('"earn_vault_not_found"')
|
|
1383
|
+
expect(response).toContain('Stable resource ID for these vault earnings')
|
|
1384
|
+
expect(response).toContain('Current share value plus completed redemptions')
|
|
1385
|
+
expect(response).toContain('weighted-average asset cost basis')
|
|
1386
|
+
expect(response).toContain('cannot reconstruct the remaining asset cost basis')
|
|
1387
|
+
expect(response).toContain('"active"')
|
|
1388
|
+
expect(response).toContain('"incomplete_cost_basis"')
|
|
1389
|
+
expect(response).toContain('"pending_redemption"')
|
|
1390
|
+
})
|
|
1108
1391
|
})
|