tapimo 0.7.0 → 0.7.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.
- package/dist/App.d.ts +5 -5
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +95 -95
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +3283 -3283
- package/dist/apps/data/routes/exchanges.d.ts +664 -664
- package/dist/apps/data/routes/fee-amm.d.ts +108 -108
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/receipts.d.ts +2 -2
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +12 -3
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +2 -2
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +4 -0
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +2 -2
- package/dist/apps/data/routes/valuation.d.ts +32 -32
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +34 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +363 -363
- package/dist/apps/data/routes/webhooks.js +14 -6
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +36 -36
- package/dist/apps/management/routes/members.d.ts +78 -78
- package/dist/apps/management/routes/usage.d.ts +80 -80
- package/dist/apps/relay/App.js +1 -1
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.js +1 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +19 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +13 -3
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +3 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Schema.d.ts +2 -2
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +4 -2
- package/dist/internal/Schema.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/ui.generated.ts +119 -95
- package/src/apps/data/App.test.ts +13 -1
- package/src/apps/data/routes/tokenlist.test.ts +15 -0
- package/src/apps/data/routes/tokenlist.ts +18 -5
- package/src/apps/data/routes/tokens.ts +4 -0
- package/src/apps/data/routes/verified-tokens.test.ts +51 -0
- package/src/apps/data/routes/verified-tokens.ts +52 -15
- package/src/apps/data/routes/webhooks.test.ts +56 -0
- package/src/apps/data/routes/webhooks.ts +17 -8
- package/src/apps/relay/App.test.ts +2 -2
- package/src/apps/relay/App.ts +1 -1
- package/src/apps/relay/Sponsorships.test.ts +2 -0
- package/src/apps/relay/Sponsorships.ts +1 -1
- package/src/db/tables/organizations.test.ts +88 -0
- package/src/db/tables/organizations.ts +26 -1
- package/src/db/tables/sponsoredTransactions.test.ts +41 -2
- package/src/db/tables/sponsoredTransactions.ts +15 -3
- package/src/internal/EdgeCache.test.ts +18 -5
- package/src/internal/EdgeCache.ts +4 -2
- package/src/internal/Schema.test.ts +13 -0
- package/src/internal/Schema.ts +14 -7
|
@@ -372,7 +372,7 @@ describe('zone chain gate', () => {
|
|
|
372
372
|
test('public chains remain edge-cacheable on a Zone deployment', async () => {
|
|
373
373
|
const store = Store.memory()
|
|
374
374
|
const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
|
|
375
|
-
const path =
|
|
375
|
+
const path = '/v1/funding/chains'
|
|
376
376
|
|
|
377
377
|
const first = await app.request(path)
|
|
378
378
|
const second = await app.request(path)
|
|
@@ -380,6 +380,18 @@ describe('zone chain gate', () => {
|
|
|
380
380
|
expect(second.status).toBe(200)
|
|
381
381
|
expect(second.headers.get('cache-control')).toContain('public')
|
|
382
382
|
})
|
|
383
|
+
|
|
384
|
+
test('preserves route-specific edge-cache opt-outs on a Zone deployment', async () => {
|
|
385
|
+
const store = Store.memory()
|
|
386
|
+
const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
|
|
387
|
+
const path = `/v1/tokenlist?chainId=${Viem.chainId.testnet}`
|
|
388
|
+
|
|
389
|
+
const first = await app.request(path)
|
|
390
|
+
const second = await app.request(path)
|
|
391
|
+
expect(first.status).toBe(200)
|
|
392
|
+
expect(second.status).toBe(200)
|
|
393
|
+
expect(second.headers.get('cache-control')).toContain('private')
|
|
394
|
+
})
|
|
383
395
|
})
|
|
384
396
|
|
|
385
397
|
describe('sandbox chain guard', () => {
|
|
@@ -75,4 +75,19 @@ describe('GET /tokenlist', () => {
|
|
|
75
75
|
expect(body.tokens).toEqual([])
|
|
76
76
|
expect(body.version.patch).toBe(0)
|
|
77
77
|
})
|
|
78
|
+
|
|
79
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
80
|
+
const db = TestApp.database()
|
|
81
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
82
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
83
|
+
|
|
84
|
+
await client.v1.tokenlist.$get({ query: {} }, as(reader))
|
|
85
|
+
await VerifiedTokens.replace(db, chainId, seedList.slice(1))
|
|
86
|
+
|
|
87
|
+
const response = await client.v1.tokenlist.$get({ query: {} }, as(reader))
|
|
88
|
+
const body = await TestApp.json(response, Tokenlist.schema.getTokenList.Response)
|
|
89
|
+
expect(body.tokens.map((token) => token.address)).toEqual(
|
|
90
|
+
seedList.slice(1).map((token) => token.address),
|
|
91
|
+
)
|
|
92
|
+
})
|
|
78
93
|
})
|
|
@@ -4,6 +4,7 @@ import * as z from 'zod/mini'
|
|
|
4
4
|
import type * as App from '../../../App.js'
|
|
5
5
|
import * as Auth from '../../../internal/Auth.js'
|
|
6
6
|
import * as Cache from '../../../internal/Cache.js'
|
|
7
|
+
import * as EdgeCache from '../../../internal/EdgeCache.js'
|
|
7
8
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
8
9
|
import * as Response from '../../../internal/Response.js'
|
|
9
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
@@ -159,11 +160,23 @@ export function tokenlist() {
|
|
|
159
160
|
summary: 'Get token list',
|
|
160
161
|
tags: ['Verified Tokens'],
|
|
161
162
|
}),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
164
|
+
EdgeCache.setEligibility(
|
|
165
|
+
Cache.response<App.Environment>({
|
|
166
|
+
cacheControl: Cache.policies.stable,
|
|
167
|
+
name: 'tempo-api:tokenlist:v1',
|
|
168
|
+
key: async (c) => {
|
|
169
|
+
const base = Cache.urlKey(c, schema.getTokenList.Query)
|
|
170
|
+
const query = schema.getTokenList.Query.parse(
|
|
171
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
172
|
+
)
|
|
173
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
174
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
175
|
+
return `${base}:v:${version}`
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
() => false,
|
|
179
|
+
),
|
|
167
180
|
async (c) => {
|
|
168
181
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
169
182
|
if (OpenApi.narrowValidation)
|
|
@@ -1821,6 +1821,8 @@ export async function getTokenCreatedAtByAddress(
|
|
|
1821
1821
|
try {
|
|
1822
1822
|
const result = await tidx.fetch({
|
|
1823
1823
|
chainId: options.chainId,
|
|
1824
|
+
// TODO: Remove the explicit engine after TIDX 1.0 is released.
|
|
1825
|
+
engine: 'clickhouse',
|
|
1824
1826
|
query: `
|
|
1825
1827
|
SELECT token, block_timestamp
|
|
1826
1828
|
FROM tokencreated
|
|
@@ -1916,6 +1918,8 @@ async function getTokenCreatedByAddress(
|
|
|
1916
1918
|
// needs no event signature for the `logs` table.
|
|
1917
1919
|
const result = await tidx.fetch({
|
|
1918
1920
|
chainId: options.chainId,
|
|
1921
|
+
// TODO: Remove the explicit engine after TIDX 1.0 is released.
|
|
1922
|
+
engine: 'clickhouse',
|
|
1919
1923
|
query: `
|
|
1920
1924
|
SELECT topic1, data, block_timestamp
|
|
1921
1925
|
FROM logs
|
|
@@ -87,6 +87,21 @@ describe('GET /verified-tokens', () => {
|
|
|
87
87
|
)
|
|
88
88
|
expect(second.status).toBe(304)
|
|
89
89
|
})
|
|
90
|
+
|
|
91
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
92
|
+
const db = TestApp.database()
|
|
93
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
94
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
95
|
+
|
|
96
|
+
await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
|
|
97
|
+
await VerifiedTokens.replace(db, chainId, seedList.slice(1))
|
|
98
|
+
|
|
99
|
+
const response = await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
|
|
100
|
+
const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response)
|
|
101
|
+
expect(body.data.map((token) => token.address)).toEqual(
|
|
102
|
+
seedList.slice(1).map((token) => token.address),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
90
105
|
})
|
|
91
106
|
|
|
92
107
|
describe('GET /verified-tokens/currencies', () => {
|
|
@@ -97,6 +112,23 @@ describe('GET /verified-tokens/currencies', () => {
|
|
|
97
112
|
const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
|
|
98
113
|
expect([...body.data]).toEqual(['EUR', 'USD'])
|
|
99
114
|
})
|
|
115
|
+
|
|
116
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
117
|
+
const db = TestApp.database()
|
|
118
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
119
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
120
|
+
|
|
121
|
+
await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
|
|
122
|
+
await VerifiedTokens.replace(
|
|
123
|
+
db,
|
|
124
|
+
chainId,
|
|
125
|
+
seedList.filter((token) => token.currency === 'USD'),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const response = await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
|
|
129
|
+
const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
|
|
130
|
+
expect([...body.data]).toEqual(['USD'])
|
|
131
|
+
})
|
|
100
132
|
})
|
|
101
133
|
|
|
102
134
|
describe('GET /verified-tokens/:address', () => {
|
|
@@ -123,4 +155,23 @@ describe('GET /verified-tokens/:address', () => {
|
|
|
123
155
|
const body = (await response.json()) as { error?: { code?: string } }
|
|
124
156
|
expect(body.error?.code).toBe('verified_token_not_found')
|
|
125
157
|
})
|
|
158
|
+
|
|
159
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
160
|
+
const db = TestApp.database()
|
|
161
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
162
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
163
|
+
const address = seedList[0].address
|
|
164
|
+
|
|
165
|
+
await client.v1['verified-tokens'][':address'].$get(
|
|
166
|
+
{ param: { address }, query: {} },
|
|
167
|
+
as(reader),
|
|
168
|
+
)
|
|
169
|
+
await VerifiedTokens.replace(db, chainId, seedList.slice(1))
|
|
170
|
+
|
|
171
|
+
const response = await client.v1['verified-tokens'][':address'].$get(
|
|
172
|
+
{ param: { address }, query: {} },
|
|
173
|
+
as(reader),
|
|
174
|
+
)
|
|
175
|
+
expect(response.status).toBe(404)
|
|
176
|
+
})
|
|
126
177
|
})
|
|
@@ -4,6 +4,7 @@ import * as z from 'zod/mini'
|
|
|
4
4
|
import type * as App from '../../../App.js'
|
|
5
5
|
import * as Auth from '../../../internal/Auth.js'
|
|
6
6
|
import * as Cache from '../../../internal/Cache.js'
|
|
7
|
+
import * as EdgeCache from '../../../internal/EdgeCache.js'
|
|
7
8
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
8
9
|
import * as Response from '../../../internal/Response.js'
|
|
9
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
@@ -168,11 +169,23 @@ export function verifiedTokens() {
|
|
|
168
169
|
summary: 'List verified tokens',
|
|
169
170
|
tags: ['Verified Tokens'],
|
|
170
171
|
}),
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
173
|
+
EdgeCache.setEligibility(
|
|
174
|
+
Cache.response<App.Environment>({
|
|
175
|
+
cacheControl: Cache.policies.stable,
|
|
176
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
177
|
+
key: async (c) => {
|
|
178
|
+
const base = Cache.urlKey(c, schema.listVerifiedTokens.Query)
|
|
179
|
+
const query = schema.listVerifiedTokens.Query.parse(
|
|
180
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
181
|
+
)
|
|
182
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
183
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
184
|
+
return `${base}:v:${version}`
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
() => false,
|
|
188
|
+
),
|
|
176
189
|
async (c) => {
|
|
177
190
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
178
191
|
if (OpenApi.narrowValidation)
|
|
@@ -213,11 +226,23 @@ export function verifiedTokens() {
|
|
|
213
226
|
summary: 'List verified currencies',
|
|
214
227
|
tags: ['Verified Tokens'],
|
|
215
228
|
}),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
230
|
+
EdgeCache.setEligibility(
|
|
231
|
+
Cache.response<App.Environment>({
|
|
232
|
+
cacheControl: Cache.policies.stable,
|
|
233
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
234
|
+
key: async (c) => {
|
|
235
|
+
const base = Cache.urlKey(c, schema.getVerifiedTokenCurrencies.Query)
|
|
236
|
+
const query = schema.getVerifiedTokenCurrencies.Query.parse(
|
|
237
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
238
|
+
)
|
|
239
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
240
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
241
|
+
return `${base}:v:${version}`
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
() => false,
|
|
245
|
+
),
|
|
221
246
|
async (c) => {
|
|
222
247
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
223
248
|
if (OpenApi.narrowValidation)
|
|
@@ -265,11 +290,23 @@ export function verifiedTokens() {
|
|
|
265
290
|
summary: 'Get verified token',
|
|
266
291
|
tags: ['Verified Tokens'],
|
|
267
292
|
}),
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
293
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
294
|
+
EdgeCache.setEligibility(
|
|
295
|
+
Cache.response<App.Environment>({
|
|
296
|
+
cacheControl: Cache.policies.stable,
|
|
297
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
298
|
+
key: async (c) => {
|
|
299
|
+
const base = Cache.urlKey(c, schema.getVerifiedToken.Query)
|
|
300
|
+
const query = schema.getVerifiedToken.Query.parse(
|
|
301
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
302
|
+
)
|
|
303
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
304
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
305
|
+
return `${base}:v:${version}`
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
() => false,
|
|
309
|
+
),
|
|
273
310
|
async (c) => {
|
|
274
311
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
275
312
|
if (OpenApi.narrowValidation)
|
|
@@ -1736,6 +1736,33 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
|
1736
1736
|
`)
|
|
1737
1737
|
})
|
|
1738
1738
|
|
|
1739
|
+
test('block:created preserves the cursor when a block header cannot be read', async () => {
|
|
1740
|
+
const db = TestApp.database()
|
|
1741
|
+
const missingBlock = 4_000_000_000
|
|
1742
|
+
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
1743
|
+
const subscription = await Core.createSubscription(db, {
|
|
1744
|
+
chainId: 42431,
|
|
1745
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1746
|
+
eventType: 'block:created',
|
|
1747
|
+
filters: { number: { eq: `0x${missingBlock.toString(16)}` } },
|
|
1748
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1749
|
+
})
|
|
1750
|
+
await Core.setCursor(db, subscription.id, cursor)
|
|
1751
|
+
|
|
1752
|
+
await expect(
|
|
1753
|
+
Webhooks.poll({
|
|
1754
|
+
chainId: 42431,
|
|
1755
|
+
eventTypes: ['block:created'],
|
|
1756
|
+
headBlockNumber: missingBlock + 2,
|
|
1757
|
+
client,
|
|
1758
|
+
db,
|
|
1759
|
+
tidx: realTidx,
|
|
1760
|
+
}),
|
|
1761
|
+
).rejects.toThrow()
|
|
1762
|
+
|
|
1763
|
+
expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
|
|
1764
|
+
})
|
|
1765
|
+
|
|
1739
1766
|
test('transaction:included filters on txs columns and inner calls', async () => {
|
|
1740
1767
|
const db = TestApp.database()
|
|
1741
1768
|
const subscription = await Core.createSubscription(db, {
|
|
@@ -1778,6 +1805,35 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
|
1778
1805
|
} else expect(tx?.feeToken).toBeUndefined()
|
|
1779
1806
|
})
|
|
1780
1807
|
|
|
1808
|
+
test('transaction:included preserves the cursor when a confirmed block is unavailable', async () => {
|
|
1809
|
+
const db = TestApp.database()
|
|
1810
|
+
const missingBlock = 4_000_000_000
|
|
1811
|
+
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
1812
|
+
const subscription = await Core.createSubscription(db, {
|
|
1813
|
+
chainId: 42431,
|
|
1814
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1815
|
+
eventType: 'transaction:included',
|
|
1816
|
+
filters: { blockNumber: { eq: `0x${missingBlock.toString(16)}` } },
|
|
1817
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1818
|
+
})
|
|
1819
|
+
await Core.setCursor(db, subscription.id, cursor)
|
|
1820
|
+
|
|
1821
|
+
await expect(
|
|
1822
|
+
Webhooks.poll({
|
|
1823
|
+
chainId: 42431,
|
|
1824
|
+
eventTypes: ['transaction:included'],
|
|
1825
|
+
headBlockNumber: missingBlock + 2,
|
|
1826
|
+
client,
|
|
1827
|
+
db,
|
|
1828
|
+
tidx: realTidx,
|
|
1829
|
+
}),
|
|
1830
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
1831
|
+
[Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
|
|
1832
|
+
`)
|
|
1833
|
+
|
|
1834
|
+
expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1781
1837
|
test('transaction:included matches exact empty calldata through the poller', async () => {
|
|
1782
1838
|
const db = TestApp.database()
|
|
1783
1839
|
const subscription = await Core.createSubscription(db, {
|
|
@@ -2485,7 +2485,7 @@ type RpcBlock = Awaited<ReturnType<Viem.getClient.ReturnType['getBlock']>>
|
|
|
2485
2485
|
/** Per-tick lazy cache of fetched block headers, full blocks, and receipts. */
|
|
2486
2486
|
type ChainCache = {
|
|
2487
2487
|
/** Full raw block (with transactions) via `eth_getBlockByNumber(n, true)`. */
|
|
2488
|
-
block(n: number): Promise<Record<string, unknown
|
|
2488
|
+
block(n: number): Promise<Record<string, unknown>>
|
|
2489
2489
|
/** Viem-formatted block header (transaction hashes only) via `getBlock`. */
|
|
2490
2490
|
header(n: number): Promise<RpcBlock | undefined>
|
|
2491
2491
|
/** Receipts for a block keyed by lowercased transaction hash. */
|
|
@@ -2495,7 +2495,7 @@ type ChainCache = {
|
|
|
2495
2495
|
/** Builds a per-tick {@link ChainCache} bound to a viem client. */
|
|
2496
2496
|
function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
2497
2497
|
const headers = new Map<number, Promise<RpcBlock | undefined>>()
|
|
2498
|
-
const blocks = new Map<number, Promise<Record<string, unknown
|
|
2498
|
+
const blocks = new Map<number, Promise<Record<string, unknown>>>()
|
|
2499
2499
|
const receipts = new Map<number, Promise<Map<string, Record<string, unknown>>>>()
|
|
2500
2500
|
return {
|
|
2501
2501
|
block(n) {
|
|
@@ -2506,7 +2506,10 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
|
2506
2506
|
p = rpc<Record<string, unknown> | null>(client, 'eth_getBlockByNumber', [
|
|
2507
2507
|
blockTag(n),
|
|
2508
2508
|
true,
|
|
2509
|
-
]).then((
|
|
2509
|
+
]).then((block) => {
|
|
2510
|
+
if (!block) throw new BlockUnavailableError(n)
|
|
2511
|
+
return block
|
|
2512
|
+
})
|
|
2510
2513
|
blocks.set(n, p)
|
|
2511
2514
|
}
|
|
2512
2515
|
return p
|
|
@@ -2514,9 +2517,7 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
|
2514
2517
|
header(n) {
|
|
2515
2518
|
let p = headers.get(n)
|
|
2516
2519
|
if (!p) {
|
|
2517
|
-
p = client
|
|
2518
|
-
.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
|
|
2519
|
-
.catch(() => undefined)
|
|
2520
|
+
p = client.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
|
|
2520
2521
|
headers.set(n, p)
|
|
2521
2522
|
}
|
|
2522
2523
|
return p
|
|
@@ -2999,10 +3000,10 @@ async function scanTxs(options: {
|
|
|
2999
3000
|
const items: ScanItem[] = []
|
|
3000
3001
|
for (let n = window.from; n <= window.to; n++) {
|
|
3001
3002
|
const block = await cache.block(n)
|
|
3002
|
-
const txs = Array.isArray(block
|
|
3003
|
+
const txs = Array.isArray(block['transactions']) ? (block['transactions'] as unknown[]) : []
|
|
3003
3004
|
if (txs.length === 0) continue
|
|
3004
3005
|
const receipts = needsReceipt || includeReceipt ? await cache.receipts(n) : undefined
|
|
3005
|
-
const timestamp = asString(block
|
|
3006
|
+
const timestamp = asString(block['timestamp'])
|
|
3006
3007
|
|
|
3007
3008
|
for (let i = 0; i < txs.length; i++) {
|
|
3008
3009
|
const tx = txs[i] as Record<string, unknown>
|
|
@@ -3195,3 +3196,11 @@ function unauthorized(c: Context<App.Environment>) {
|
|
|
3195
3196
|
status: 401,
|
|
3196
3197
|
})
|
|
3197
3198
|
}
|
|
3199
|
+
|
|
3200
|
+
class BlockUnavailableError extends Error {
|
|
3201
|
+
override name = 'Webhooks.BlockUnavailableError'
|
|
3202
|
+
|
|
3203
|
+
constructor(blockNumber: number) {
|
|
3204
|
+
super(`Block ${blockNumber} is unavailable.`)
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
@@ -450,7 +450,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
450
450
|
`)
|
|
451
451
|
})
|
|
452
452
|
|
|
453
|
-
test('behavior: non-mainnet production keys record billable rows', async () => {
|
|
453
|
+
test('behavior: non-mainnet production keys record non-billable rows', async () => {
|
|
454
454
|
const db = TestApp.database()
|
|
455
455
|
const app = TestApp.create({
|
|
456
456
|
auth: { keys },
|
|
@@ -466,7 +466,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
466
466
|
expect(body.error).toBeUndefined()
|
|
467
467
|
|
|
468
468
|
const [row] = await SponsoredTransactions.listPending(db)
|
|
469
|
-
expect(row?.billable).toBe(
|
|
469
|
+
expect(row?.billable).toBe(false)
|
|
470
470
|
expect(row?.environment).toMatchInlineSnapshot(`"production"`)
|
|
471
471
|
})
|
|
472
472
|
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -238,7 +238,7 @@ function middleware(options: middleware.Options) {
|
|
|
238
238
|
|
|
239
239
|
const input = {
|
|
240
240
|
apiKeyId: attribution.apiKeyId,
|
|
241
|
-
billable: attribution.environment === 'production',
|
|
241
|
+
billable: mainnet && attribution.environment === 'production',
|
|
242
242
|
chainId: event.chainId,
|
|
243
243
|
environment: attribution.environment,
|
|
244
244
|
orgId: attribution.orgId,
|
|
@@ -4,6 +4,7 @@ import type { Client } from 'viem'
|
|
|
4
4
|
|
|
5
5
|
import * as Runtime from '../../../test/runtime.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
|
+
import * as Organizations from '../../db/tables/organizations.js'
|
|
7
8
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
8
9
|
import * as Sponsorships from './Sponsorships.js'
|
|
9
10
|
|
|
@@ -269,6 +270,7 @@ describe('reconcile', () => {
|
|
|
269
270
|
test('behavior: recovers recently failed intents by sign payload', async () => {
|
|
270
271
|
const db = create()
|
|
271
272
|
await db.migrate()
|
|
273
|
+
await Organizations.create(db, { id: 'org_1', name: 'One' })
|
|
272
274
|
const { transactionHash: _, ...rest } = input
|
|
273
275
|
const intent = await SponsoredTransactions.upsert(db, { ...rest, signPayload })
|
|
274
276
|
await SponsoredTransactions.fail(db, intent.id, '2026-01-01T00:00:00.000Z')
|
|
@@ -126,7 +126,7 @@ export async function reconcile(db: Db.Db, options: reconcile.Options): Promise<
|
|
|
126
126
|
const {
|
|
127
127
|
batchSize = 100,
|
|
128
128
|
candidateCap = 500,
|
|
129
|
-
failedIntentTtlMs =
|
|
129
|
+
failedIntentTtlMs = SponsoredTransactions.failedIntentRecoveryTtlMs,
|
|
130
130
|
feePayer,
|
|
131
131
|
getClient,
|
|
132
132
|
getTidx,
|
|
@@ -2,6 +2,7 @@ import { nanoid } from 'nanoid'
|
|
|
2
2
|
|
|
3
3
|
import * as Runtime from '../../../test/runtime.js'
|
|
4
4
|
import * as Webhooks from '../../internal/Webhooks.js'
|
|
5
|
+
import * as Viem from '../../internal/Viem.js'
|
|
5
6
|
import * as Db from '../Db.js'
|
|
6
7
|
import * as EnabledBillingSources from './enabledBillingSources.js'
|
|
7
8
|
import * as Memberships from './memberships.js'
|
|
@@ -216,6 +217,7 @@ describe('deleteOrganization', () => {
|
|
|
216
217
|
transactionHash: `0x${'aa'.repeat(32)}`,
|
|
217
218
|
})
|
|
218
219
|
|
|
220
|
+
expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(true)
|
|
219
221
|
await expect(Organizations.deleteOrganization(db, 'org_1')).rejects.toThrow(
|
|
220
222
|
Organizations.UnreportedSponsorshipsError,
|
|
221
223
|
)
|
|
@@ -227,6 +229,92 @@ describe('deleteOrganization', () => {
|
|
|
227
229
|
await db.close()
|
|
228
230
|
})
|
|
229
231
|
|
|
232
|
+
test('behavior: deletes an org with only failed or non-mainnet sponsorships', async () => {
|
|
233
|
+
const db = create()
|
|
234
|
+
await db.migrate()
|
|
235
|
+
await Organizations.create(db, { id: 'org_1', name: 'One' })
|
|
236
|
+
const failed = await SponsoredTransactions.upsert(db, {
|
|
237
|
+
apiKeyId: 'key_1',
|
|
238
|
+
billable: true,
|
|
239
|
+
chainId: Viem.chainId.mainnet,
|
|
240
|
+
environment: 'production',
|
|
241
|
+
orgId: 'org_1',
|
|
242
|
+
projectId: 'prj_1',
|
|
243
|
+
signPayload: `0x${'bb'.repeat(32)}`,
|
|
244
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
245
|
+
transactionHash: `0x${'aa'.repeat(32)}`,
|
|
246
|
+
})
|
|
247
|
+
await SponsoredTransactions.fail(db, failed.id, '2026-01-01T00:00:00.000Z')
|
|
248
|
+
const testnet = await SponsoredTransactions.upsert(db, {
|
|
249
|
+
apiKeyId: 'key_1',
|
|
250
|
+
billable: true,
|
|
251
|
+
chainId: Viem.chainId.testnet,
|
|
252
|
+
environment: 'production',
|
|
253
|
+
orgId: 'org_1',
|
|
254
|
+
projectId: 'prj_1',
|
|
255
|
+
signPayload: `0x${'dd'.repeat(32)}`,
|
|
256
|
+
transaction: `0x76${'ee'.repeat(16)}`,
|
|
257
|
+
transactionHash: `0x${'ff'.repeat(32)}`,
|
|
258
|
+
})
|
|
259
|
+
await SponsoredTransactions.finalize(db, testnet.id, {
|
|
260
|
+
feeAmount: '100',
|
|
261
|
+
finalizedAt: '2026-01-01T00:00:00.000Z',
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(false)
|
|
265
|
+
expect(await Organizations.deleteOrganization(db, 'org_1')).toBe(true)
|
|
266
|
+
await db.close()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('behavior: preserves an org with a recoverable failed fill intent', async () => {
|
|
270
|
+
const db = create()
|
|
271
|
+
await db.migrate()
|
|
272
|
+
await Organizations.create(db, { id: 'org_1', name: 'One' })
|
|
273
|
+
const failed = await SponsoredTransactions.upsert(db, {
|
|
274
|
+
apiKeyId: 'key_1',
|
|
275
|
+
billable: true,
|
|
276
|
+
chainId: Viem.chainId.mainnet,
|
|
277
|
+
environment: 'production',
|
|
278
|
+
orgId: 'org_1',
|
|
279
|
+
projectId: 'prj_1',
|
|
280
|
+
signPayload: `0x${'bb'.repeat(32)}`,
|
|
281
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
282
|
+
})
|
|
283
|
+
await SponsoredTransactions.fail(db, failed.id, new Date().toISOString())
|
|
284
|
+
|
|
285
|
+
expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(true)
|
|
286
|
+
await expect(Organizations.deleteOrganization(db, 'org_1')).rejects.toThrow(
|
|
287
|
+
Organizations.UnreportedSponsorshipsError,
|
|
288
|
+
)
|
|
289
|
+
expect(await Organizations.get(db, 'org_1')).toBeDefined()
|
|
290
|
+
await db.close()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('behavior: deletes an org with only expired failed fill intents', async () => {
|
|
294
|
+
const db = create()
|
|
295
|
+
await db.migrate()
|
|
296
|
+
await Organizations.create(db, { id: 'org_1', name: 'One' })
|
|
297
|
+
const failed = await SponsoredTransactions.upsert(db, {
|
|
298
|
+
apiKeyId: 'key_1',
|
|
299
|
+
billable: true,
|
|
300
|
+
chainId: Viem.chainId.mainnet,
|
|
301
|
+
environment: 'production',
|
|
302
|
+
orgId: 'org_1',
|
|
303
|
+
projectId: 'prj_1',
|
|
304
|
+
signPayload: `0x${'bb'.repeat(32)}`,
|
|
305
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
306
|
+
})
|
|
307
|
+
await SponsoredTransactions.fail(
|
|
308
|
+
db,
|
|
309
|
+
failed.id,
|
|
310
|
+
new Date(Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs - 1).toISOString(),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(await Organizations.hasUnreportedSponsorships(db, 'org_1')).toBe(false)
|
|
314
|
+
expect(await Organizations.deleteOrganization(db, 'org_1')).toBe(true)
|
|
315
|
+
await db.close()
|
|
316
|
+
})
|
|
317
|
+
|
|
230
318
|
test('behavior: deletes enabled billing sources', async () => {
|
|
231
319
|
const db = create()
|
|
232
320
|
await db.migrate()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { sql, type Selectable } from 'kysely'
|
|
1
|
+
import { sql, type ExpressionBuilder, type Selectable } from 'kysely'
|
|
2
2
|
|
|
3
3
|
import * as Id from '../../internal/Id.js'
|
|
4
|
+
import * as Viem from '../../internal/Viem.js'
|
|
5
|
+
import * as SponsoredTransactions from './sponsoredTransactions.js'
|
|
4
6
|
import type * as Db from '../Db.js'
|
|
5
7
|
import type * as db_Schema from '../Schema.js'
|
|
6
8
|
import type * as EnabledBillingSources from './enabledBillingSources.js'
|
|
@@ -120,12 +122,17 @@ export declare namespace createOwned {
|
|
|
120
122
|
*/
|
|
121
123
|
export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
|
|
122
124
|
return db.kysely.transaction().execute(async (trx) => {
|
|
125
|
+
const failedSince = new Date(
|
|
126
|
+
Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs,
|
|
127
|
+
).toISOString()
|
|
123
128
|
const unreported = await trx
|
|
124
129
|
.selectFrom('sponsored_transactions')
|
|
125
130
|
.select('id')
|
|
126
131
|
.where('orgId', '=', id)
|
|
127
132
|
.where('billable', '=', true)
|
|
133
|
+
.where('chainId', '=', Viem.chainId.mainnet)
|
|
128
134
|
.where('meterReportedAt', 'is', null)
|
|
135
|
+
.where(unreportedSponsorshipStatus(failedSince))
|
|
129
136
|
.limit(1)
|
|
130
137
|
.executeTakeFirst()
|
|
131
138
|
if (unreported) throw new UnreportedSponsorshipsError()
|
|
@@ -152,12 +159,17 @@ export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
|
|
|
152
159
|
|
|
153
160
|
/** Whether an organization has billable sponsorships awaiting settlement. */
|
|
154
161
|
export async function hasUnreportedSponsorships(db: Db.Db, id: string): Promise<boolean> {
|
|
162
|
+
const failedSince = new Date(
|
|
163
|
+
Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs,
|
|
164
|
+
).toISOString()
|
|
155
165
|
const record = await db.kysely
|
|
156
166
|
.selectFrom('sponsored_transactions')
|
|
157
167
|
.select('id')
|
|
158
168
|
.where('orgId', '=', id)
|
|
159
169
|
.where('billable', '=', true)
|
|
170
|
+
.where('chainId', '=', Viem.chainId.mainnet)
|
|
160
171
|
.where('meterReportedAt', 'is', null)
|
|
172
|
+
.where(unreportedSponsorshipStatus(failedSince))
|
|
161
173
|
.limit(1)
|
|
162
174
|
.executeTakeFirst()
|
|
163
175
|
return record !== undefined
|
|
@@ -429,6 +441,19 @@ export declare namespace upsert {
|
|
|
429
441
|
}
|
|
430
442
|
}
|
|
431
443
|
|
|
444
|
+
/** Blocks rows that can still settle or recover into billable mainnet spend. */
|
|
445
|
+
function unreportedSponsorshipStatus(failedSince: string) {
|
|
446
|
+
return (eb: ExpressionBuilder<Db.Database, 'sponsored_transactions'>) =>
|
|
447
|
+
eb.or([
|
|
448
|
+
eb('status', '!=', 'failed'),
|
|
449
|
+
eb.and([
|
|
450
|
+
eb('status', '=', 'failed'),
|
|
451
|
+
eb('transactionHash', 'is', null),
|
|
452
|
+
eb('finalizedAt', '>=', failedSince),
|
|
453
|
+
]),
|
|
454
|
+
])
|
|
455
|
+
}
|
|
456
|
+
|
|
432
457
|
/** Organization deletion is blocked until billable sponsorships settle. */
|
|
433
458
|
export class UnreportedSponsorshipsError extends Error {
|
|
434
459
|
override name = 'Organizations.UnreportedSponsorshipsError'
|