tapimo 0.9.0 → 0.10.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.
Files changed (198) hide show
  1. package/dist/App.d.ts +4887 -5074
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +21 -20
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +66 -23
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  8. package/dist/apps/Zones.d.ts +34 -22
  9. package/dist/apps/Zones.d.ts.map +1 -1
  10. package/dist/apps/Zones.js +9 -8
  11. package/dist/apps/Zones.js.map +1 -1
  12. package/dist/apps/data/App.d.ts +5198 -5099
  13. package/dist/apps/data/App.d.ts.map +1 -1
  14. package/dist/apps/data/FxOracle.d.ts +4 -5
  15. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.js +19 -4
  17. package/dist/apps/data/FxOracle.js.map +1 -1
  18. package/dist/apps/data/routes/activities.d.ts +270 -270
  19. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  20. package/dist/apps/data/routes/activities.js +32 -27
  21. package/dist/apps/data/routes/activities.js.map +1 -1
  22. package/dist/apps/data/routes/addresses.d.ts +52 -52
  23. package/dist/apps/data/routes/balances.d.ts +36 -36
  24. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.js +135 -81
  26. package/dist/apps/data/routes/balances.js.map +1 -1
  27. package/dist/apps/data/routes/blocks.d.ts +125 -125
  28. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  29. package/dist/apps/data/routes/earn.d.ts +512 -58
  30. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  31. package/dist/apps/data/routes/earn.js +368 -44
  32. package/dist/apps/data/routes/earn.js.map +1 -1
  33. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  34. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  35. package/dist/apps/data/routes/exchanges.js +114 -118
  36. package/dist/apps/data/routes/exchanges.js.map +1 -1
  37. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  38. package/dist/apps/data/routes/receipts.js +68 -47
  39. package/dist/apps/data/routes/receipts.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts +31 -31
  41. package/dist/apps/data/routes/tokens.d.ts +461 -459
  42. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  43. package/dist/apps/data/routes/transactions.d.ts +59 -51
  44. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  45. package/dist/apps/data/routes/transactions.js +25 -23
  46. package/dist/apps/data/routes/transactions.js.map +1 -1
  47. package/dist/apps/data/routes/transfers.d.ts +108 -86
  48. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  49. package/dist/apps/data/routes/transfers.js +618 -105
  50. package/dist/apps/data/routes/transfers.js.map +1 -1
  51. package/dist/apps/data/routes/valuation.d.ts +21 -21
  52. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  53. package/dist/apps/data/routes/valuation.js +2 -2
  54. package/dist/apps/data/routes/valuation.js.map +1 -1
  55. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  56. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  57. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  58. package/dist/apps/data/routes/webhooks.js +484 -342
  59. package/dist/apps/data/routes/webhooks.js.map +1 -1
  60. package/dist/apps/data/routes/zones.d.ts +12 -12
  61. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  62. package/dist/apps/data/routes/zones.js +3 -1
  63. package/dist/apps/data/routes/zones.js.map +1 -1
  64. package/dist/apps/funding/App.d.ts +96 -96
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  66. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  67. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  68. package/dist/apps/management/App.d.ts +429 -727
  69. package/dist/apps/management/App.d.ts.map +1 -1
  70. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  71. package/dist/apps/management/routes/billing.d.ts +72 -72
  72. package/dist/apps/management/routes/faucet.d.ts +6 -6
  73. package/dist/apps/management/routes/invitations.d.ts +134 -134
  74. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  75. package/dist/apps/management/routes/me.d.ts +6 -6
  76. package/dist/apps/management/routes/members.d.ts +30 -30
  77. package/dist/apps/management/routes/orgs.d.ts +98 -98
  78. package/dist/apps/management/routes/projects.d.ts +20 -20
  79. package/dist/apps/management/routes/usage.d.ts +24 -24
  80. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  81. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  82. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/mpp/App.d.ts +56 -44
  84. package/dist/apps/mpp/App.d.ts.map +1 -1
  85. package/dist/apps/mpp/Mpp.d.ts +3 -0
  86. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  87. package/dist/apps/mpp/Mpp.js +3 -0
  88. package/dist/apps/mpp/Mpp.js.map +1 -1
  89. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  90. package/dist/apps/mpp/internal/Adapter.js +9 -2
  91. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  92. package/dist/db/Db.d.ts +2 -2
  93. package/dist/db/Db.d.ts.map +1 -1
  94. package/dist/db/Db.js +17 -15
  95. package/dist/db/Db.js.map +1 -1
  96. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  97. package/dist/db/tables/sponsoredTransactions.js +4 -1
  98. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +76 -3
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/MetricSink.d.ts +112 -3
  104. package/dist/internal/MetricSink.d.ts.map +1 -1
  105. package/dist/internal/MetricSink.js +116 -3
  106. package/dist/internal/MetricSink.js.map +1 -1
  107. package/dist/internal/Schema.js +1 -1
  108. package/dist/internal/Schema.js.map +1 -1
  109. package/dist/internal/Tidx.d.ts +1 -3
  110. package/dist/internal/Tidx.d.ts.map +1 -1
  111. package/dist/internal/Tidx.js +1 -3
  112. package/dist/internal/Tidx.js.map +1 -1
  113. package/dist/internal/Viem.d.ts.map +1 -1
  114. package/dist/internal/Viem.js +2 -1
  115. package/dist/internal/Viem.js.map +1 -1
  116. package/dist/internal/WebhookDestination.d.ts +2 -0
  117. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  118. package/dist/internal/WebhookDestination.js +11 -0
  119. package/dist/internal/WebhookDestination.js.map +1 -1
  120. package/dist/internal/WebhookTransfer.d.ts +55 -0
  121. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  122. package/dist/internal/WebhookTransfer.js +296 -0
  123. package/dist/internal/WebhookTransfer.js.map +1 -0
  124. package/dist/internal/Webhooks.d.ts +37 -0
  125. package/dist/internal/Webhooks.d.ts.map +1 -1
  126. package/dist/internal/Webhooks.js +87 -13
  127. package/dist/internal/Webhooks.js.map +1 -1
  128. package/dist/internal/funding/Provider.d.ts +4 -0
  129. package/dist/internal/funding/Provider.d.ts.map +1 -1
  130. package/dist/internal/funding/Provider.js +11 -7
  131. package/dist/internal/funding/Provider.js.map +1 -1
  132. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  133. package/dist/internal/funding/Reconciliation.js +128 -0
  134. package/dist/internal/funding/Reconciliation.js.map +1 -1
  135. package/dist/internal/funding/providers/relay.d.ts +3 -0
  136. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  137. package/dist/internal/funding/providers/relay.js +12 -1
  138. package/dist/internal/funding/providers/relay.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/Client.test-d.ts +60 -5
  141. package/src/Webhooks.bench.ts +714 -0
  142. package/src/Webhooks.memory.bench.ts +250 -0
  143. package/src/Webhooks.test.ts +77 -6
  144. package/src/Webhooks.ts +83 -29
  145. package/src/apps/Zones.test.ts +5 -4
  146. package/src/apps/Zones.ts +9 -8
  147. package/src/apps/data/FxOracle.test.ts +18 -0
  148. package/src/apps/data/FxOracle.ts +28 -5
  149. package/src/apps/data/routes/activities.test.ts +19 -0
  150. package/src/apps/data/routes/activities.ts +34 -30
  151. package/src/apps/data/routes/balances.test.ts +119 -10
  152. package/src/apps/data/routes/balances.ts +154 -95
  153. package/src/apps/data/routes/blocks.test.ts +12 -6
  154. package/src/apps/data/routes/earn.test.ts +197 -0
  155. package/src/apps/data/routes/earn.ts +486 -44
  156. package/src/apps/data/routes/exchanges.test.ts +7 -6
  157. package/src/apps/data/routes/exchanges.ts +124 -117
  158. package/src/apps/data/routes/receipts.test.ts +24 -7
  159. package/src/apps/data/routes/receipts.ts +67 -50
  160. package/src/apps/data/routes/tokens.test.ts +51 -0
  161. package/src/apps/data/routes/transactions.test.ts +42 -0
  162. package/src/apps/data/routes/transactions.ts +44 -39
  163. package/src/apps/data/routes/transfers.test.ts +402 -24
  164. package/src/apps/data/routes/transfers.ts +708 -101
  165. package/src/apps/data/routes/valuation.test.ts +26 -0
  166. package/src/apps/data/routes/valuation.ts +6 -2
  167. package/src/apps/data/routes/webhooks.test.ts +414 -130
  168. package/src/apps/data/routes/webhooks.ts +603 -419
  169. package/src/apps/data/routes/zones.ts +6 -1
  170. package/src/apps/funding/routes/quotes.test.ts +1 -1
  171. package/src/apps/mpp/Mpp.test.ts +13 -1
  172. package/src/apps/mpp/Mpp.ts +8 -0
  173. package/src/apps/mpp/internal/Adapter.ts +10 -2
  174. package/src/db/Db.test.ts +40 -6
  175. package/src/db/Db.ts +33 -19
  176. package/src/db/tables/sponsoredTransactions.ts +9 -2
  177. package/src/db/tables/webhookSubscriptions.ts +116 -4
  178. package/src/internal/EarnVaults.test.ts +2 -2
  179. package/src/internal/MetricSink.test.ts +518 -0
  180. package/src/internal/MetricSink.ts +266 -9
  181. package/src/internal/Schema.ts +1 -1
  182. package/src/internal/Tidx.test.ts +4 -4
  183. package/src/internal/Tidx.ts +1 -3
  184. package/src/internal/Viem.test.ts +8 -0
  185. package/src/internal/Viem.ts +2 -1
  186. package/src/internal/WebhookDestination.test.ts +20 -0
  187. package/src/internal/WebhookDestination.ts +10 -0
  188. package/src/internal/WebhookTransfer.ts +426 -0
  189. package/src/internal/Webhooks.test.ts +137 -0
  190. package/src/internal/Webhooks.ts +131 -17
  191. package/src/internal/funding/Provider.test.ts +47 -7
  192. package/src/internal/funding/Provider.ts +12 -7
  193. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  194. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  195. package/src/internal/funding/Reconciliation.ts +136 -0
  196. package/src/internal/funding/SourceObservation.test.ts +1 -0
  197. package/src/internal/funding/providers/relay.test.ts +1 -4
  198. package/src/internal/funding/providers/relay.ts +16 -1
@@ -176,7 +176,12 @@ export function zones() {
176
176
  name: 'tempo-api:zone-withdrawals:v1',
177
177
  key: (c) => {
178
178
  const principal = Auth.getPrincipal(c)
179
- return `${principal?.type ?? 'unknown'}:${principal?.id ?? 'unknown'}:${Cache.urlKey(c, schema.listZoneWithdrawals.Query)}`
179
+ const url = new URL(Cache.urlKey(c, schema.listZoneWithdrawals.Query))
180
+ url.searchParams.set(
181
+ 'principal',
182
+ `${principal?.type ?? 'unknown'}:${principal?.id ?? 'unknown'}`,
183
+ )
184
+ return url.toString()
180
185
  },
181
186
  }),
182
187
  async (c) => {
@@ -271,7 +271,7 @@ describe('GET /funding/quotes', () => {
271
271
  const response_promise = app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
272
272
  headers: TestApp.auth.headers,
273
273
  })
274
- await vi.advanceTimersByTimeAsync(2_000)
274
+ await vi.advanceTimersByTimeAsync(5_000)
275
275
  const response = await response_promise
276
276
 
277
277
  expect(response.status).toBe(502)
@@ -91,6 +91,17 @@ describe('MPP relay', () => {
91
91
  expect(await response.json()).toEqual({ success: true })
92
92
  })
93
93
 
94
+ test('verifies a zero-amount proof credential', async () => {
95
+ const app = TestApp.create({ auth: { keys: [key] } })
96
+ const input = await credential({ amount: '0' })
97
+ const response = await post(app, '/v1/mpp/validate', input)
98
+
99
+ expect(payloadType(input)).toBe('proof')
100
+ expect(input.source).toBeDefined()
101
+ expect(response.status).toBe(200)
102
+ expect(await response.json()).toEqual({ success: true })
103
+ })
104
+
94
105
  test('validates and finalizes a pushed credential without a second submission', async () => {
95
106
  const app = TestApp.create({ auth: { keys: [key] } })
96
107
  const input = await credential({ mode: 'push', supportedModes: ['push'] })
@@ -619,7 +630,7 @@ type CredentialPayload = { hash?: string; type?: unknown }
619
630
 
620
631
  async function credential(options: credential.Options = {}): Promise<Mpp.RelayInput> {
621
632
  const issuer = createIssuer(options)
622
- const challenge = await issuer.challenge.tempo.charge({ amount: '1' })
633
+ const challenge = await issuer.challenge.tempo.charge({ amount: options.amount ?? '1' })
623
634
  const authorization = await createClient(options).createCredential(
624
635
  new Response(null, {
625
636
  headers: { 'www-authenticate': Mppx.Challenge.serialize(challenge) },
@@ -631,6 +642,7 @@ async function credential(options: credential.Options = {}): Promise<Mpp.RelayIn
631
642
 
632
643
  declare namespace credential {
633
644
  type Options = {
645
+ amount?: string | undefined
634
646
  feePayer?: boolean | undefined
635
647
  mode?: 'pull' | 'push' | undefined
636
648
  supportedModes?: ('pull' | 'push')[] | undefined
@@ -67,6 +67,8 @@ export type RelayInput = {
67
67
  challenge: Challenge
68
68
  /** Method-specific MPP credential payload. */
69
69
  payload: unknown
70
+ /** Optional payer identifier supplied with the credential. */
71
+ source?: string | undefined
70
72
  }
71
73
 
72
74
  /** Broadcast MPP payment receipt. */
@@ -200,6 +202,12 @@ export namespace schema {
200
202
  z.describe('The method-specific MPP credential payload.'),
201
203
  z.meta({ examples: [{ signature: `0x${'aa'.repeat(65)}`, type: 'transaction' }] }),
202
204
  ),
205
+ source: z
206
+ .optional(z.string())
207
+ .check(
208
+ z.describe('An optional payer identifier supplied with the credential.'),
209
+ z.meta({ examples: ['did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678'] }),
210
+ ),
203
211
  }),
204
212
  'An MPP payment credential and its challenge.',
205
213
  )
@@ -49,7 +49,7 @@ export function create(options: create.Options): Adapter {
49
49
  /** Validates and screens a credential, rejecting when either check fails. */
50
50
  async function validateOrThrow(input: Mpp.RelayInput) {
51
51
  const result = await time('mpp_validate', () =>
52
- Mppx.Method.validateCredential(methods, Mppx.Credential.from(input)),
52
+ Mppx.Method.validateCredential(methods, credential(input)),
53
53
  )
54
54
  await time('mpp_screen', () => screenManager.screen(Utils.addresses(result)))
55
55
  }
@@ -65,7 +65,7 @@ export function create(options: create.Options): Adapter {
65
65
  try {
66
66
  await validateOrThrow(input)
67
67
  const result = await time('mpp_broadcast', () =>
68
- Mppx.Method.broadcastCredential(methods, Mppx.Credential.from(input)),
68
+ Mppx.Method.broadcastCredential(methods, credential(input)),
69
69
  )
70
70
  return {
71
71
  receipt: {
@@ -91,6 +91,14 @@ export function create(options: create.Options): Adapter {
91
91
  }
92
92
  }
93
93
 
94
+ function credential(input: Mpp.RelayInput) {
95
+ return Mppx.Credential.from({
96
+ challenge: input.challenge,
97
+ payload: input.payload,
98
+ ...(input.source === undefined ? {} : { source: input.source }),
99
+ })
100
+ }
101
+
94
102
  export declare namespace create {
95
103
  /** Dependencies used to validate and broadcast Tempo credentials. */
96
104
  type Options = {
package/src/db/Db.test.ts CHANGED
@@ -5,6 +5,7 @@ import * as Runtime from '../../test/runtime.js'
5
5
  import * as Db from './Db.js'
6
6
  import * as Invitations from './tables/invitations.js'
7
7
  import * as Organizations from './tables/organizations.js'
8
+ import * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
8
9
 
9
10
  const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
10
11
 
@@ -39,14 +40,47 @@ describe('postgres', () => {
39
40
  await db.close()
40
41
  })
41
42
 
42
- test('behavior: concurrent migrate tolerates the ledger race', async () => {
43
+ test('behavior: defers schema creation until migration', async () => {
43
44
  const schema = `t_${nanoid()}`
44
- // Pre-create the schema so concurrent migrate races only on the
45
- // `migrations` ledger, not `CREATE SCHEMA`.
46
- const seed = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
47
- await Organizations.get(seed, 'x').catch(() => {})
48
- await seed.close()
45
+ const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
46
+ const exists = async () =>
47
+ (
48
+ await sql<{
49
+ exists: boolean
50
+ }>`SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ${schema}) AS exists`.execute(
51
+ db.kysely,
52
+ )
53
+ ).rows[0]!.exists
54
+
55
+ await sql`SELECT 1`.execute(db.kysely)
56
+ expect(await exists()).toBe(false)
57
+ await db.migrate()
58
+ expect(await exists()).toBe(true)
59
+ await db.close()
60
+ })
61
+
62
+ test('behavior: qualifies repository queries with the configured schema', async () => {
63
+ const db = Db.postgres({
64
+ connectionString: Runtime.postgresUrl,
65
+ schema: `t_MixedCase_${nanoid()}`,
66
+ })
67
+ await sql`SET search_path TO public`.execute(db.kysely)
68
+ await db.migrate()
69
+ await sql`SET search_path TO public`.execute(db.kysely)
70
+ await db.transaction(async (tx) => {
71
+ await Organizations.create(tx, { id: 'org_qualified', name: 'Qualified' })
72
+ })
73
+ expect((await Organizations.get(db, 'org_qualified'))?.name).toBe('Qualified')
74
+ expect(
75
+ await WebhookSubscriptions.compareAndSetCursors(db, {
76
+ cursors: [{ cursor: 'next', expectedCursor: null, subscriptionId: 'wh_missing' }],
77
+ }),
78
+ ).toEqual(new Set())
79
+ await db.close()
80
+ })
49
81
 
82
+ test('behavior: concurrent migrate tolerates the ledger race', async () => {
83
+ const schema = `t_${nanoid()}`
50
84
  // Several runners migrate the same fresh schema at once (mirrors parallel
51
85
  // deploys). The session advisory lock serializes them; none throw.
52
86
  const dbs = Array.from({ length: 5 }, () =>
package/src/db/Db.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  PostgresDialect,
8
8
  type RawBuilder,
9
9
  sql,
10
+ WithSchemaPlugin,
10
11
  } from 'kysely'
11
12
  import * as pg from 'pg'
12
13
 
@@ -1301,6 +1302,15 @@ function raw(db: Kysely<Database>, statement: RawBuilder<unknown>) {
1301
1302
  * @returns The database.
1302
1303
  */
1303
1304
  export function from(dialect: Dialect): Db {
1305
+ return fromDialect(dialect)
1306
+ }
1307
+
1308
+ /** Builds a database with internal dialect-specific options. */
1309
+ function fromDialect(dialect: Dialect, options: fromDialect.Options = {}): Db {
1310
+ const root = new Kysely<Database>({ dialect, plugins: [new CamelCasePlugin()] })
1311
+ // Qualify repository queries instead of relying on session search_path;
1312
+ // transaction poolers reset session state between statements.
1313
+ const kysely = options.schema ? root.withPlugin(new WithSchemaPlugin(options.schema)) : root
1304
1314
  // Builds Db over a top-level Kysely handle or transaction-scoped Kysely handle.
1305
1315
  function build(kysely: Kysely<Database>): Db {
1306
1316
  // Prevents same-handle nested transactions from deadlocking capped pools.
@@ -1314,9 +1324,19 @@ export function from(dialect: Dialect): Db {
1314
1324
  async migrate() {
1315
1325
  // A session advisory lock serializes the full run, including bootstrap
1316
1326
  // DDL and ledger inserts. Pin one connection and unlock in `finally`.
1317
- await kysely.connection().execute(async (db) => {
1327
+ await root.connection().execute(async (db) => {
1318
1328
  await sql`SELECT pg_advisory_lock(${migrateLockKey})`.execute(db)
1319
1329
  try {
1330
+ if (options.schema) {
1331
+ await db.executeQuery(
1332
+ CompiledQuery.raw(
1333
+ `CREATE SCHEMA IF NOT EXISTS ${pg.escapeIdentifier(options.schema)}`,
1334
+ ),
1335
+ )
1336
+ await sql`SELECT set_config('search_path', quote_ident(${options.schema}), false)`.execute(
1337
+ db,
1338
+ )
1339
+ }
1320
1340
  await sql`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL)`.execute(
1321
1341
  db,
1322
1342
  )
@@ -1378,12 +1398,20 @@ export function from(dialect: Dialect): Db {
1378
1398
  },
1379
1399
  }
1380
1400
  }
1381
- return build(new Kysely<Database>({ dialect, plugins: [new CamelCasePlugin()] }))
1401
+ return build(kysely)
1402
+ }
1403
+
1404
+ declare namespace fromDialect {
1405
+ /** Internal database construction options. */
1406
+ type Options = {
1407
+ /** Schema that Kysely qualifies on every repository query. */
1408
+ schema?: string | undefined
1409
+ }
1382
1410
  }
1383
1411
 
1384
1412
  /**
1385
1413
  * Creates a Postgres-backed {@link Db} via Kysely's `PostgresDialect`. Optional
1386
- * `schema` scopes every pooled connection and caps the pool for test isolation.
1414
+ * `schema` qualifies repository queries, scopes migrations, and caps the pool.
1387
1415
  *
1388
1416
  * @param options - Postgres options.
1389
1417
  * @returns The database.
@@ -1391,21 +1419,7 @@ export function from(dialect: Dialect): Db {
1391
1419
  export function postgres(options: postgres.Options): Db {
1392
1420
  const { connectionString, schema } = options
1393
1421
  const pool = new pg.Pool(schema ? { connectionString, max: 1 } : { connectionString })
1394
- return from(
1395
- new PostgresDialect({
1396
- pool,
1397
- ...(schema
1398
- ? {
1399
- onCreateConnection: async (connection) => {
1400
- await connection.executeQuery(
1401
- CompiledQuery.raw(`CREATE SCHEMA IF NOT EXISTS "${schema}"`),
1402
- )
1403
- await connection.executeQuery(CompiledQuery.raw(`SET search_path TO "${schema}"`))
1404
- },
1405
- }
1406
- : {}),
1407
- }),
1408
- )
1422
+ return fromDialect(new PostgresDialect({ pool }), { schema })
1409
1423
  }
1410
1424
 
1411
1425
  export declare namespace postgres {
@@ -1413,7 +1427,7 @@ export declare namespace postgres {
1413
1427
  type Options = {
1414
1428
  /** Postgres connection string (`postgresql://…`). */
1415
1429
  connectionString: string
1416
- /** Schema to create and scope the session to (isolates concurrent tests). */
1430
+ /** Schema to qualify and create during migration (isolates concurrent tests). */
1417
1431
  schema?: string | undefined
1418
1432
  }
1419
1433
  }
@@ -1,4 +1,4 @@
1
- import type { ExpressionBuilder, Selectable, SqlBool } from 'kysely'
1
+ import type { ExpressionBuilder, Selectable } from 'kysely'
2
2
  import { sql } from 'kysely'
3
3
 
4
4
  import * as Id from '../../internal/Id.js'
@@ -222,7 +222,14 @@ export async function assignTransactionHash(
222
222
  .where('status', '=', record.status)
223
223
  .where('transactionHash', 'is', null)
224
224
  if (record.status === 'failed')
225
- query = query.where(sql<SqlBool>`exists (select 1 from organizations where organizations.id = sponsored_transactions.org_id)`) // prettier-ignore
225
+ query = query.where((eb) =>
226
+ eb.exists(
227
+ eb
228
+ .selectFrom('organizations')
229
+ .select('organizations.id')
230
+ .whereRef('organizations.id', '=', 'sponsored_transactions.orgId'),
231
+ ),
232
+ )
226
233
  const updated = await query.executeTakeFirst()
227
234
  if (updated.numUpdatedRows === 0n) return
228
235
  if (record.status === 'failed' && record.billable && limit) {
@@ -273,20 +273,21 @@ export async function remove(
273
273
 
274
274
  /**
275
275
  * Lists live `active` subscriptions for a `(chainId, eventType)` stream — the
276
- * poller's per-stream lookup, served by the partial status index.
276
+ * poller's per-stream lookup, served by the partial status index. Each row
277
+ * carries its poller cursor so the poller avoids a per-subscription read.
277
278
  *
278
279
  * @param db - The database.
279
280
  * @param chainId - Chain the stream belongs to.
280
281
  * @param eventType - Event type of the stream.
281
282
  * @param now - ISO timestamp expiry is evaluated against.
282
- * @returns The subscriptions, in id (chronological) order.
283
+ * @returns Cursor-and-subscription pairs, in id (chronological) order.
283
284
  */
284
285
  export async function listActive(
285
286
  db: Db.Db,
286
287
  chainId: number,
287
288
  eventType: Webhooks.EventType,
288
289
  now: string,
289
- ): Promise<Webhooks.Subscription[]> {
290
+ ): Promise<{ cursor: string | null; subscription: Webhooks.Subscription }[]> {
290
291
  const rows = await db.kysely
291
292
  .selectFrom('webhook_subscriptions')
292
293
  .selectAll()
@@ -296,7 +297,69 @@ export async function listActive(
296
297
  .where(live(now))
297
298
  .orderBy('id')
298
299
  .execute()
299
- return rows.map(toSubscription)
300
+ return rows.map((row) => ({ cursor: row.pollerCursor, subscription: toSubscription(row) }))
301
+ }
302
+
303
+ /**
304
+ * Lists every live `active` subscription on a chain in one read. The poller
305
+ * groups the rows by event type so empty streams cost no extra database reads.
306
+ *
307
+ * @param db - The database.
308
+ * @param options - Chain, configured event types, and expiry time.
309
+ * @returns Cursor-and-subscription pairs ordered by event type, then id.
310
+ */
311
+ export async function listActiveForChain(
312
+ db: Db.Db,
313
+ options: listActiveForChain.Options,
314
+ ): Promise<{ cursor: string | null; subscription: Webhooks.Subscription }[]> {
315
+ if (options.eventTypes.length === 0) return []
316
+ const rows = await db.kysely
317
+ .selectFrom('webhook_subscriptions')
318
+ .selectAll()
319
+ .where('chainId', '=', options.chainId)
320
+ .where('eventType', 'in', options.eventTypes)
321
+ .where('status', '=', 'active')
322
+ .where(live(options.now))
323
+ .orderBy('eventType')
324
+ .orderBy('id')
325
+ .execute()
326
+ return rows.map((row) => ({ cursor: row.pollerCursor, subscription: toSubscription(row) }))
327
+ }
328
+
329
+ export declare namespace listActiveForChain {
330
+ /** Filters for the poller's chain-level subscription read. */
331
+ type Options = {
332
+ /** Chain whose subscriptions to read. */
333
+ chainId: number
334
+ /** Event types configured for this poller tick. */
335
+ eventTypes: readonly Webhooks.EventType[]
336
+ /** ISO timestamp expiry is evaluated against. */
337
+ now: string
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Reads the poller cursors of every live `active` subscription on a chain,
343
+ * across event types. Feeds the cron's cursor-freshness fallback gate.
344
+ *
345
+ * @param db - The database.
346
+ * @param chainId - Chain whose cursors to read.
347
+ * @param now - ISO timestamp expiry is evaluated against.
348
+ * @returns The non-null cursors, unordered.
349
+ */
350
+ export async function listActiveCursors(
351
+ db: Db.Db,
352
+ chainId: number,
353
+ now: string,
354
+ ): Promise<string[]> {
355
+ const rows = await db.kysely
356
+ .selectFrom('webhook_subscriptions')
357
+ .select('pollerCursor')
358
+ .where('chainId', '=', chainId)
359
+ .where('status', '=', 'active')
360
+ .where(live(now))
361
+ .execute()
362
+ return rows.flatMap((row) => (row.pollerCursor === null ? [] : [row.pollerCursor]))
300
363
  }
301
364
 
302
365
  /**
@@ -367,6 +430,55 @@ export declare namespace compareAndSetCursor {
367
430
  }
368
431
  }
369
432
 
433
+ /**
434
+ * Writes multiple poller cursors when each stored value still matches the
435
+ * caller's observed value.
436
+ *
437
+ * @param db - The database.
438
+ * @param options - Candidate cursor writes.
439
+ * @returns Subscription ids whose cursors were updated.
440
+ */
441
+ export async function compareAndSetCursors(
442
+ db: Db.Db,
443
+ options: compareAndSetCursors.Options,
444
+ ): Promise<Set<string>> {
445
+ if (options.cursors.length === 0) return new Set()
446
+ const values = JSON.stringify(options.cursors)
447
+ type Candidate = {
448
+ cursor: string
449
+ expectedCursor: string | null
450
+ subscriptionId: string
451
+ }
452
+ const result = await db.kysely
453
+ .with(
454
+ 'candidates',
455
+ () => sql<Candidate>`(
456
+ SELECT
457
+ candidate."subscriptionId" AS subscription_id,
458
+ candidate."expectedCursor" AS expected_cursor,
459
+ candidate.cursor
460
+ FROM jsonb_to_recordset(${values}::jsonb)
461
+ AS candidate("subscriptionId" text, "expectedCursor" text, cursor text)
462
+ )`,
463
+ )
464
+ .updateTable('webhook_subscriptions as subscription')
465
+ .from('candidates as candidate')
466
+ .set((eb) => ({ pollerCursor: eb.ref('candidate.cursor') }))
467
+ .whereRef('subscription.id', '=', 'candidate.subscriptionId')
468
+ .whereRef('subscription.pollerCursor', 'is not distinct from', 'candidate.expectedCursor')
469
+ .returning('subscription.id as subscriptionId')
470
+ .execute()
471
+ return new Set(result.map((row) => row.subscriptionId))
472
+ }
473
+
474
+ export declare namespace compareAndSetCursors {
475
+ /** Options for {@link compareAndSetCursors}. */
476
+ type Options = {
477
+ /** Candidate cursor writes. */
478
+ cursors: readonly compareAndSetCursor.Options[]
479
+ }
480
+ }
481
+
370
482
  /**
371
483
  * Records a successful delivery: resets the consecutive-failure counter and
372
484
  * stamps `lastDeliveryAt`.
@@ -76,7 +76,7 @@ describe('resolve', () => {
76
76
  const getClient = Viem.createGetClient({
77
77
  defaultChainId: tempoTestnet.id,
78
78
  rpc: TestViem.rpc,
79
- zones: [Zones.zoneE],
79
+ zones: [Zones.zoneModerato],
80
80
  })
81
81
  const { instantLiquidity, sharePrice, state, ...discovery } = await EarnVaults.resolve({
82
82
  getClient,
@@ -84,7 +84,7 @@ describe('resolve', () => {
84
84
  chainId: tempoTestnet.id,
85
85
  vaultAddress,
86
86
  },
87
- zones: [Zones.zoneE],
87
+ zones: [Zones.zoneModerato],
88
88
  })
89
89
  expect({
90
90
  depositsPaused: state.depositsPaused,