tapimo 0.13.0 → 0.14.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/dist/App.d.ts +6064 -4829
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.js +2 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +1008 -917
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +48 -0
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.js +2 -1
- package/dist/admin/apps/earn-vaults.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +92 -3
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +69 -7
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +12 -12
- package/dist/apps/Zones.js +3 -3
- package/dist/apps/data/App.d.ts +2909 -2523
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +22 -22
- package/dist/apps/data/routes/earn.d.ts +183 -7
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +434 -75
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +15 -0
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +180 -114
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +18 -19
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +93 -93
- package/dist/apps/data/routes/transfers.d.ts +12 -12
- package/dist/apps/data/routes/valuation.d.ts +30 -30
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +805 -102
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +47 -5
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/funding/App.d.ts +456 -45
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +1 -1
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +654 -73
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +211 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +831 -387
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +17 -1
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +76 -76
- package/dist/apps/management/routes/billing.d.ts +76 -76
- package/dist/apps/management/routes/faucet.d.ts +12 -12
- package/dist/apps/management/routes/invitations.d.ts +24 -24
- package/dist/apps/management/routes/invite-links.d.ts +46 -46
- package/dist/apps/management/routes/members.d.ts +46 -46
- package/dist/apps/management/routes/orgs.d.ts +20 -20
- package/dist/apps/management/routes/projects.d.ts +44 -44
- package/dist/apps/management/routes/usage.d.ts +32 -32
- package/dist/apps/management/routes/verified-token-requests.d.ts +16 -16
- package/dist/apps/management/routes/webhooks.d.ts +707 -57
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +21 -21
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +115 -23
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +27 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +22 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +8 -3
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
- package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
- package/dist/db/tables/fundingTransferTransactions.js +44 -3
- package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
- package/dist/db/tables/fundingTransfers.d.ts +17 -0
- package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
- package/dist/db/tables/fundingTransfers.js +21 -0
- package/dist/db/tables/fundingTransfers.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +24 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +27 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -1
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +26 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +146 -13
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +9 -6
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/EarnVaults.js +5 -5
- package/dist/internal/EarnVaults.js.map +1 -1
- package/dist/internal/Log.d.ts +1 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Viem.d.ts +16 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
- package/dist/internal/funding/DepositWebhook.js +2 -1
- package/dist/internal/funding/DepositWebhook.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +2 -10
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +40 -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/SourceObservation.d.ts.map +1 -1
- package/dist/internal/funding/SourceObservation.js +5 -7
- package/dist/internal/funding/SourceObservation.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +101 -6
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +162 -34
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
- package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
- package/dist/internal/funding/TransferReconciliation.js +192 -0
- package/dist/internal/funding/TransferReconciliation.js.map +1 -0
- package/dist/internal/funding/TransferWebhook.d.ts +21 -0
- package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
- package/dist/internal/funding/TransferWebhook.js +43 -0
- package/dist/internal/funding/TransferWebhook.js.map +1 -0
- 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.map +1 -1
- package/dist/internal/funding/providers/relay.js +3 -12
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +99 -0
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +337 -35
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
- package/dist/internal/webhookDestinations/slack.js +16 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -1
- package/package.json +1 -1
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +83 -2
- package/src/Webhooks.ts +2 -2
- package/src/admin/apps/earn-vaults.ts +2 -1
- package/src/admin/apps/organizations.test.ts +53 -1
- package/src/admin/apps/organizations.ts +90 -5
- package/src/admin/ui/src/lib/organizations.test.ts +112 -1
- package/src/admin/ui/src/lib/organizations.ts +55 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
- package/src/admin/ui.generated.ts +69 -69
- package/src/apps/Zones.test.ts +3 -3
- package/src/apps/Zones.ts +3 -3
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.ts +15 -6
- package/src/apps/data/routes/earn.test.ts +369 -14
- package/src/apps/data/routes/earn.ts +548 -79
- package/src/apps/data/routes/exchanges.test.ts +740 -15
- package/src/apps/data/routes/exchanges.ts +954 -753
- package/src/apps/data/routes/fee-amm.ts +17 -18
- package/src/apps/data/routes/webhooks.test.ts +21 -5
- package/src/apps/data/routes/webhooks.ts +63 -8
- package/src/apps/funding/App.ts +12 -2
- package/src/apps/funding/routes/deposit-addresses.test.ts +4 -4
- package/src/apps/funding/routes/deposit-addresses.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +510 -19
- package/src/apps/funding/routes/transfers.ts +271 -5
- package/src/apps/management/Billing.ts +18 -5
- package/src/apps/management/routes/webhooks.test.ts +3 -3
- package/src/apps/management/routes/webhooks.ts +24 -25
- package/src/apps/relay/App.test.ts +212 -1
- package/src/apps/relay/App.ts +158 -29
- package/src/apps/relay/Sponsorships.ts +24 -1
- package/src/db/Db.ts +35 -0
- package/src/db/Schema.ts +17 -2
- package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
- package/src/db/tables/fundingTransferTransactions.ts +79 -3
- package/src/db/tables/fundingTransfers.test.ts +26 -0
- package/src/db/tables/fundingTransfers.ts +36 -0
- package/src/db/tables/organizations.test.ts +41 -0
- package/src/db/tables/organizations.ts +49 -0
- package/src/db/tables/projects.test.ts +55 -0
- package/src/db/tables/projects.ts +46 -1
- package/src/db/tables/sponsoredTransactions.test.ts +338 -0
- package/src/db/tables/sponsoredTransactions.ts +197 -13
- package/src/db/tables/webhookSubscriptions.ts +18 -11
- package/src/internal/EarnVaults.ts +5 -5
- package/src/internal/Log.test-d.ts +2 -0
- package/src/internal/Log.ts +2 -0
- package/src/internal/Viem.ts +39 -3
- package/src/internal/Webhooks.test.ts +45 -0
- package/src/internal/funding/DepositWebhook.ts +2 -1
- package/src/internal/funding/Evidence.ts +2 -13
- package/src/internal/funding/Provider.ts +43 -0
- package/src/internal/funding/SourceObservation.ts +5 -9
- package/src/internal/funding/Transfer.test.ts +155 -0
- package/src/internal/funding/Transfer.ts +254 -37
- package/src/internal/funding/TransferReconciliation.test.ts +147 -0
- package/src/internal/funding/TransferReconciliation.ts +264 -0
- package/src/internal/funding/TransferWebhook.test.ts +128 -0
- package/src/internal/funding/TransferWebhook.ts +67 -0
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.ts +3 -15
- package/src/internal/funding/providers/stargate.test.ts +169 -4
- package/src/internal/funding/providers/stargate.ts +498 -51
- package/src/internal/webhookDestinations/slack.ts +15 -0
|
@@ -2,12 +2,15 @@ import * as z from 'zod/mini'
|
|
|
2
2
|
|
|
3
3
|
import type * as Db from '../../db/Db.js'
|
|
4
4
|
import * as FundingTransferEvents from '../../db/tables/fundingTransferEvents.js'
|
|
5
|
+
import * as FundingTransferTransactions from '../../db/tables/fundingTransferTransactions.js'
|
|
5
6
|
import * as FundingTransfers from '../../db/tables/fundingTransfers.js'
|
|
6
7
|
import * as Id from '../Id.js'
|
|
7
8
|
import * as OpenApi from '../OpenApi.js'
|
|
8
9
|
import * as Schema from '../Schema.js'
|
|
10
|
+
import type * as Webhooks from '../Webhooks.js'
|
|
9
11
|
import * as Action from './Action.js'
|
|
10
12
|
import * as Provider from './Provider.js'
|
|
13
|
+
import * as TransferWebhook from './TransferWebhook.js'
|
|
11
14
|
|
|
12
15
|
/** Public funding transfer statuses, in the order documented by the API. */
|
|
13
16
|
export const statuses = [
|
|
@@ -61,7 +64,7 @@ const transitions: Record<Status, readonly Status[]> = {
|
|
|
61
64
|
'awaiting-source': ['action-required', 'expired', 'processing'],
|
|
62
65
|
completed: [],
|
|
63
66
|
expired: [],
|
|
64
|
-
processing: ['action-required', 'completed', 'refunding'],
|
|
67
|
+
processing: ['action-required', 'awaiting-source', 'completed', 'refunding'],
|
|
65
68
|
refunded: [],
|
|
66
69
|
refunding: ['action-required', 'refunded'],
|
|
67
70
|
}
|
|
@@ -383,12 +386,22 @@ export const transactionRoles = ['destination', 'refund', 'source'] as const
|
|
|
383
386
|
/** Role of a verified transaction reference on a transfer. */
|
|
384
387
|
export type TransactionRole = (typeof transactionRoles)[number]
|
|
385
388
|
|
|
389
|
+
/** Bounded private provider state retained for transfer reconciliation. */
|
|
390
|
+
export type ProviderState = Record<string, unknown>
|
|
391
|
+
|
|
392
|
+
type Staged = {
|
|
393
|
+
/** Funding transfer record committed with the webhook obligations. */
|
|
394
|
+
record: FundingTransfers.Record
|
|
395
|
+
/** Webhook queue references committed with the transfer. */
|
|
396
|
+
references: Webhooks.QueueReference[]
|
|
397
|
+
}
|
|
398
|
+
|
|
386
399
|
/**
|
|
387
400
|
* Creates a durable funding transfer: the row plus its version-1 event commit
|
|
388
401
|
* in one transaction, so every stored version has an immutable public record.
|
|
389
402
|
*/
|
|
390
403
|
export async function create(db: Db.Db, input: create.Input): Promise<FundingTransfers.Record> {
|
|
391
|
-
return db.transaction((tx) => createIn(tx, input))
|
|
404
|
+
return db.transaction(async (tx) => (await createIn(tx, input)).record)
|
|
392
405
|
}
|
|
393
406
|
|
|
394
407
|
/**
|
|
@@ -396,7 +409,7 @@ export async function create(db: Db.Db, input: create.Input): Promise<FundingTra
|
|
|
396
409
|
* transaction, for writes that must commit atomically with sibling state
|
|
397
410
|
* (idempotency completion).
|
|
398
411
|
*/
|
|
399
|
-
export async function createIn(tx: Db.Db, input: create.Input): Promise<
|
|
412
|
+
export async function createIn(tx: Db.Db, input: create.Input): Promise<createIn.Result> {
|
|
400
413
|
const now = new Date()
|
|
401
414
|
const snapshot = parseSnapshot(input.snapshot)
|
|
402
415
|
const record: FundingTransfers.Record = {
|
|
@@ -418,14 +431,22 @@ export async function createIn(tx: Db.Db, input: create.Input): Promise<FundingT
|
|
|
418
431
|
version: 1,
|
|
419
432
|
}
|
|
420
433
|
const created = await FundingTransfers.insert(tx, record)
|
|
421
|
-
await FundingTransferEvents.insert(tx, {
|
|
434
|
+
const event = await FundingTransferEvents.insert(tx, {
|
|
422
435
|
createdAt: record.createdAt,
|
|
423
436
|
snapshot: toPublic(created),
|
|
424
437
|
status: created.status,
|
|
425
438
|
transferId: created.id,
|
|
426
439
|
version: created.version,
|
|
427
440
|
})
|
|
428
|
-
|
|
441
|
+
const { references } = await TransferWebhook.stage(tx, {
|
|
442
|
+
changes: [{ event, record: created }],
|
|
443
|
+
})
|
|
444
|
+
return { record: created, references }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export declare namespace createIn {
|
|
448
|
+
/** Created transfer and webhook queue references committed with it. */
|
|
449
|
+
type Result = Staged
|
|
429
450
|
}
|
|
430
451
|
|
|
431
452
|
export declare namespace create {
|
|
@@ -442,12 +463,193 @@ export declare namespace create {
|
|
|
442
463
|
/** Attributed project id, when the creating key is project-attributed. */
|
|
443
464
|
projectId?: string | undefined
|
|
444
465
|
/** Private provider correlation retained for reconciliation. */
|
|
445
|
-
providerState?:
|
|
466
|
+
providerState?: ProviderState | undefined
|
|
446
467
|
/** The public snapshot captured from the selected quote. */
|
|
447
468
|
snapshot: Snapshot
|
|
448
469
|
}
|
|
449
470
|
}
|
|
450
471
|
|
|
472
|
+
/** Atomically assigns one verified source transaction and starts reconciliation. */
|
|
473
|
+
export async function registerSourceTransaction(
|
|
474
|
+
db: Db.Db,
|
|
475
|
+
options: registerSourceTransaction.Options,
|
|
476
|
+
): Promise<registerSourceTransaction.Result> {
|
|
477
|
+
return db.transaction(async (tx) => {
|
|
478
|
+
const current = await FundingTransfers.getForUpdate(tx, options.id)
|
|
479
|
+
if (!current) return { type: 'not_found' }
|
|
480
|
+
|
|
481
|
+
const existing = await FundingTransferTransactions.getSource(tx, {
|
|
482
|
+
chainId: options.chainId,
|
|
483
|
+
transactionRef: options.transactionHash,
|
|
484
|
+
})
|
|
485
|
+
if (existing) {
|
|
486
|
+
if (existing.transferId !== current.id) return { current, type: 'conflict' }
|
|
487
|
+
if (current.status !== 'processing' && current.status !== 'completed')
|
|
488
|
+
return { current, type: 'invalid' }
|
|
489
|
+
return { record: current, type: 'replayed' }
|
|
490
|
+
}
|
|
491
|
+
if (current.status !== 'awaiting-source') return { current, type: 'invalid' }
|
|
492
|
+
|
|
493
|
+
const claimed = await FundingTransferTransactions.claimSource(tx, {
|
|
494
|
+
chainId: options.chainId,
|
|
495
|
+
createdAt: new Date().toISOString(),
|
|
496
|
+
role: 'source',
|
|
497
|
+
transactionRef: options.transactionHash,
|
|
498
|
+
transferId: options.id,
|
|
499
|
+
})
|
|
500
|
+
if (claimed.type === 'conflict') return { current, type: 'conflict' }
|
|
501
|
+
if (claimed.type === 'replayed') return { record: current, type: 'replayed' }
|
|
502
|
+
|
|
503
|
+
const result = await transitionIn(tx, {
|
|
504
|
+
current,
|
|
505
|
+
expectedVersion: current.version,
|
|
506
|
+
id: current.id,
|
|
507
|
+
providerState: { ...current.providerState, ...options.providerState },
|
|
508
|
+
snapshot: {
|
|
509
|
+
...current.snapshot,
|
|
510
|
+
sourceTransactionHashes: [claimed.record.transactionRef],
|
|
511
|
+
},
|
|
512
|
+
status: 'processing',
|
|
513
|
+
})
|
|
514
|
+
if (result.type !== 'applied') throw new AtomicTransitionError()
|
|
515
|
+
return { record: result.record, references: result.references, type: 'registered' }
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export declare namespace registerSourceTransaction {
|
|
520
|
+
/** Verified source evidence and private provider correlation. */
|
|
521
|
+
type Options = {
|
|
522
|
+
/** Source chain CAIP-2 id. */
|
|
523
|
+
chainId: string
|
|
524
|
+
/** Transfer id (`ftr_…`). */
|
|
525
|
+
id: string
|
|
526
|
+
/** Private state added for destination reconciliation. */
|
|
527
|
+
providerState: ProviderState
|
|
528
|
+
/** Verified source transaction hash. */
|
|
529
|
+
transactionHash: string
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Source transaction registration outcome. */
|
|
533
|
+
type Result =
|
|
534
|
+
| { current: FundingTransfers.Record; type: 'conflict' }
|
|
535
|
+
| { current: FundingTransfers.Record; type: 'invalid' }
|
|
536
|
+
| (Staged & { type: 'registered' })
|
|
537
|
+
| { record: FundingTransfers.Record; type: 'replayed' }
|
|
538
|
+
| { type: 'not_found' }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Atomically removes invalidated source evidence and accepts a replacement. */
|
|
542
|
+
export async function reopenSourceTransaction(
|
|
543
|
+
db: Db.Db,
|
|
544
|
+
options: reopenSourceTransaction.Options,
|
|
545
|
+
): Promise<reopenSourceTransaction.Result> {
|
|
546
|
+
return db.transaction(async (tx) => {
|
|
547
|
+
const current = await FundingTransfers.getForUpdate(tx, options.id)
|
|
548
|
+
if (!current) return { type: 'not_found' }
|
|
549
|
+
if (current.status !== 'processing') return { current, type: 'invalid' }
|
|
550
|
+
|
|
551
|
+
const removed = await FundingTransferTransactions.removeSource(tx, {
|
|
552
|
+
chainId: options.chainId,
|
|
553
|
+
transactionHash: options.transactionHash,
|
|
554
|
+
transferId: current.id,
|
|
555
|
+
})
|
|
556
|
+
if (!removed) return { current, type: 'invalid' }
|
|
557
|
+
const { sourceTransactionHashes: _sourceTransactionHashes, ...snapshot } = current.snapshot
|
|
558
|
+
const result = await transitionIn(tx, {
|
|
559
|
+
current,
|
|
560
|
+
expectedVersion: current.version,
|
|
561
|
+
id: current.id,
|
|
562
|
+
providerState: options.providerState,
|
|
563
|
+
snapshot,
|
|
564
|
+
status: 'awaiting-source',
|
|
565
|
+
})
|
|
566
|
+
if (result.type !== 'applied') throw new AtomicTransitionError()
|
|
567
|
+
return { record: result.record, references: result.references, type: 'reopened' }
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export declare namespace reopenSourceTransaction {
|
|
572
|
+
/** Source evidence to remove after it disappears from the canonical chain. */
|
|
573
|
+
type Options = {
|
|
574
|
+
/** Source chain CAIP-2 id. */
|
|
575
|
+
chainId: string
|
|
576
|
+
/** Transfer id (`ftr_…`). */
|
|
577
|
+
id: string
|
|
578
|
+
/** Private provider state retained while awaiting replacement evidence. */
|
|
579
|
+
providerState: ProviderState
|
|
580
|
+
/** Invalidated source transaction hash. */
|
|
581
|
+
transactionHash: string
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Source transaction reopening outcome. */
|
|
585
|
+
type Result =
|
|
586
|
+
| { current: FundingTransfers.Record; type: 'invalid' }
|
|
587
|
+
| (Staged & { type: 'reopened' })
|
|
588
|
+
| { type: 'not_found' }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Atomically records a verified destination transaction and completes a transfer. */
|
|
592
|
+
export async function complete(db: Db.Db, options: complete.Options): Promise<complete.Result> {
|
|
593
|
+
return db.transaction(async (tx) => {
|
|
594
|
+
const current = await FundingTransfers.getForUpdate(tx, options.id)
|
|
595
|
+
if (!current) return { type: 'not_found' }
|
|
596
|
+
if (current.status === 'completed') {
|
|
597
|
+
const transactions = await FundingTransferTransactions.listByTransfer(tx, current.id)
|
|
598
|
+
const existing = transactions.some(
|
|
599
|
+
(transaction) =>
|
|
600
|
+
transaction.chainId === options.chainId &&
|
|
601
|
+
transaction.role === 'destination' &&
|
|
602
|
+
transaction.transactionRef === options.transactionHash.toLowerCase(),
|
|
603
|
+
)
|
|
604
|
+
return existing ? { record: current, type: 'replayed' } : { current, type: 'invalid' }
|
|
605
|
+
}
|
|
606
|
+
if (current.status !== 'processing') return { current, type: 'invalid' }
|
|
607
|
+
|
|
608
|
+
const transaction = await FundingTransferTransactions.insert(tx, {
|
|
609
|
+
chainId: options.chainId,
|
|
610
|
+
createdAt: new Date().toISOString(),
|
|
611
|
+
role: 'destination',
|
|
612
|
+
transactionRef: options.transactionHash,
|
|
613
|
+
transferId: options.id,
|
|
614
|
+
})
|
|
615
|
+
if (!transaction) throw new AtomicTransitionError()
|
|
616
|
+
const result = await transitionIn(tx, {
|
|
617
|
+
current,
|
|
618
|
+
expectedVersion: current.version,
|
|
619
|
+
id: current.id,
|
|
620
|
+
snapshot: {
|
|
621
|
+
...current.snapshot,
|
|
622
|
+
destinationAmount: options.destinationAmount,
|
|
623
|
+
destinationTransactionHashes: [transaction.transactionRef],
|
|
624
|
+
},
|
|
625
|
+
status: 'completed',
|
|
626
|
+
})
|
|
627
|
+
if (result.type !== 'applied') throw new AtomicTransitionError()
|
|
628
|
+
return { record: result.record, references: result.references, type: 'completed' }
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export declare namespace complete {
|
|
633
|
+
/** Verified destination evidence for one processing transfer. */
|
|
634
|
+
type Options = {
|
|
635
|
+
/** Destination chain CAIP-2 id. */
|
|
636
|
+
chainId: string
|
|
637
|
+
/** Exact verified destination amount. */
|
|
638
|
+
destinationAmount: NonNullable<Snapshot['destinationAmount']>
|
|
639
|
+
/** Transfer id (`ftr_…`). */
|
|
640
|
+
id: string
|
|
641
|
+
/** Verified destination transaction hash. */
|
|
642
|
+
transactionHash: string
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Transfer completion outcome. */
|
|
646
|
+
type Result =
|
|
647
|
+
| { current: FundingTransfers.Record; type: 'invalid' }
|
|
648
|
+
| (Staged & { type: 'completed' })
|
|
649
|
+
| { record: FundingTransfers.Record; type: 'replayed' }
|
|
650
|
+
| { type: 'not_found' }
|
|
651
|
+
}
|
|
652
|
+
|
|
451
653
|
/**
|
|
452
654
|
* Applies a version-guarded material change: a status transition, a snapshot
|
|
453
655
|
* update, or both. The row update and its immutable event commit in one
|
|
@@ -457,39 +659,48 @@ export declare namespace create {
|
|
|
457
659
|
export async function transition(
|
|
458
660
|
db: Db.Db,
|
|
459
661
|
options: transition.Options,
|
|
662
|
+
): Promise<transition.Result> {
|
|
663
|
+
return db.transaction((tx) => transitionIn(tx, options))
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function transitionIn(
|
|
667
|
+
tx: Db.Db,
|
|
668
|
+
options: transition.Options & { current?: FundingTransfers.Record | undefined },
|
|
460
669
|
): Promise<transition.Result> {
|
|
461
670
|
const now = new Date()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
})
|
|
480
|
-
if (!updated) {
|
|
481
|
-
const latest = await FundingTransfers.get(tx, options.id)
|
|
482
|
-
return latest ? { current: latest, type: 'stale' } : { type: 'not_found' }
|
|
483
|
-
}
|
|
484
|
-
await FundingTransferEvents.insert(tx, {
|
|
485
|
-
createdAt: updated.updatedAt,
|
|
486
|
-
snapshot: toPublic(updated),
|
|
487
|
-
status: updated.status,
|
|
488
|
-
transferId: updated.id,
|
|
489
|
-
version: updated.version,
|
|
490
|
-
})
|
|
491
|
-
return { record: updated, type: 'applied' }
|
|
671
|
+
const current = options.current ?? (await FundingTransfers.get(tx, options.id))
|
|
672
|
+
if (!current) return { type: 'not_found' }
|
|
673
|
+
if (current.version !== options.expectedVersion) return { current, type: 'stale' }
|
|
674
|
+
const status = options.status ?? current.status
|
|
675
|
+
if (status !== current.status && !canTransition(current.status, status))
|
|
676
|
+
return { current, type: 'invalid' }
|
|
677
|
+
|
|
678
|
+
const updated = await FundingTransfers.update(tx, {
|
|
679
|
+
expectedVersion: options.expectedVersion,
|
|
680
|
+
id: options.id,
|
|
681
|
+
providerState:
|
|
682
|
+
options.providerState === undefined ? current.providerState : options.providerState,
|
|
683
|
+
snapshot: options.snapshot === undefined ? current.snapshot : parseSnapshot(options.snapshot),
|
|
684
|
+
status,
|
|
685
|
+
statusReason: options.statusReason ?? (status === current.status ? current.statusReason : null),
|
|
686
|
+
updatedAt: now.toISOString(),
|
|
687
|
+
version: current.version + 1,
|
|
492
688
|
})
|
|
689
|
+
if (!updated) {
|
|
690
|
+
const latest = await FundingTransfers.get(tx, options.id)
|
|
691
|
+
return latest ? { current: latest, type: 'stale' } : { type: 'not_found' }
|
|
692
|
+
}
|
|
693
|
+
const event = await FundingTransferEvents.insert(tx, {
|
|
694
|
+
createdAt: updated.updatedAt,
|
|
695
|
+
snapshot: toPublic(updated),
|
|
696
|
+
status: updated.status,
|
|
697
|
+
transferId: updated.id,
|
|
698
|
+
version: updated.version,
|
|
699
|
+
})
|
|
700
|
+
const { references } = await TransferWebhook.stage(tx, {
|
|
701
|
+
changes: [{ event, record: updated }],
|
|
702
|
+
})
|
|
703
|
+
return { record: updated, references, type: 'applied' }
|
|
493
704
|
}
|
|
494
705
|
|
|
495
706
|
export declare namespace transition {
|
|
@@ -499,6 +710,8 @@ export declare namespace transition {
|
|
|
499
710
|
expectedVersion: number
|
|
500
711
|
/** Transfer id (`ftr_…`). */
|
|
501
712
|
id: string
|
|
713
|
+
/** Replacement private provider state; omitted keeps the stored value. */
|
|
714
|
+
providerState?: ProviderState | null | undefined
|
|
502
715
|
/** Replacement public snapshot; omitted keeps the stored snapshot. */
|
|
503
716
|
snapshot?: Snapshot | undefined
|
|
504
717
|
/** New status; omitted keeps the current status (snapshot-only change). */
|
|
@@ -509,7 +722,7 @@ export declare namespace transition {
|
|
|
509
722
|
|
|
510
723
|
/** Outcome of a guarded transition. */
|
|
511
724
|
type Result =
|
|
512
|
-
|
|
|
725
|
+
| (Staged & { type: 'applied' })
|
|
513
726
|
| { current: FundingTransfers.Record; type: 'invalid' }
|
|
514
727
|
| { current: FundingTransfers.Record; type: 'stale' }
|
|
515
728
|
| { type: 'not_found' }
|
|
@@ -543,3 +756,7 @@ function parseSnapshot(snapshot: Snapshot): Snapshot {
|
|
|
543
756
|
},
|
|
544
757
|
}
|
|
545
758
|
}
|
|
759
|
+
|
|
760
|
+
class AtomicTransitionError extends Error {
|
|
761
|
+
override name = 'Funding.Transfer.AtomicTransitionError'
|
|
762
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { tempoMainnet } from 'viem/tempo/chains'
|
|
2
|
+
|
|
3
|
+
import * as Anvil from '../../../test/Anvil.js'
|
|
4
|
+
import * as TestApp from '../../../test/App.js'
|
|
5
|
+
import * as TestFunding from '../../../test/Funding.js'
|
|
6
|
+
import * as Catalog from './Catalog.js'
|
|
7
|
+
import * as Transfer from './Transfer.js'
|
|
8
|
+
import * as TransferReconciliation from './TransferReconciliation.js'
|
|
9
|
+
|
|
10
|
+
const db = TestApp.database()
|
|
11
|
+
const destinationTransactionHash =
|
|
12
|
+
'0x3dfaba0a78b8f4bc09e55e05211c8120729e7b6879efe32613883befbca666da'
|
|
13
|
+
const guid = '0x5549d117c8b23f0ecdf05c2604ed989cf0034f378707bd81319e93834f2f85f2'
|
|
14
|
+
const recipient = '0x7212623e4cff9ad010d28aa065fad2bf58578783'
|
|
15
|
+
const sender = '0x52b650a62e384f68880d0a1dc2b84dfed47d5660'
|
|
16
|
+
const sourceTransactionHash = '0xfd71359adf5095f2ab512a19e0a2df6c8cc0db14ac8a52286d61287daabdccd4'
|
|
17
|
+
const routeConfiguration = TestFunding.snapshot.routesByProvider
|
|
18
|
+
.get('stargate')!
|
|
19
|
+
.find((candidate) => candidate.route.source.chain.id === 'eip155:8453')!.configuration!
|
|
20
|
+
|
|
21
|
+
describe.skipIf(!Anvil.available)('createTracker', () => {
|
|
22
|
+
let destination: Anvil.start.ReturnType
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
destination = await Anvil.start({
|
|
26
|
+
chain: tempoMainnet,
|
|
27
|
+
forkUrl: tempoMainnet.rpcUrls.default.http[0]!,
|
|
28
|
+
})
|
|
29
|
+
await Catalog.publish(db, {
|
|
30
|
+
...TestFunding.data,
|
|
31
|
+
chains: TestFunding.data.chains.map((chain) =>
|
|
32
|
+
chain.id === 'eip155:4217' ? { ...chain, rpcUrls: [destination.rpcUrl] } : chain,
|
|
33
|
+
),
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
await destination?.stop()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('completes a registered transfer from Tempo evidence', async () => {
|
|
42
|
+
const amount = {
|
|
43
|
+
baseUnits: '200000',
|
|
44
|
+
currency: 'USD',
|
|
45
|
+
decimals: 6,
|
|
46
|
+
formatted: '0.2',
|
|
47
|
+
} as const
|
|
48
|
+
const transfer = await Transfer.create(db, {
|
|
49
|
+
apiKeyId: 'key_test',
|
|
50
|
+
environment: 'production',
|
|
51
|
+
orgId: 'org_test',
|
|
52
|
+
providerState: {
|
|
53
|
+
destinationBlockNumber: (
|
|
54
|
+
await destination.client.getTransactionReceipt({ hash: destinationTransactionHash })
|
|
55
|
+
).blockNumber.toString(),
|
|
56
|
+
routeConfiguration,
|
|
57
|
+
},
|
|
58
|
+
snapshot: TestFunding.transferSnapshot({
|
|
59
|
+
destinationAmount: amount,
|
|
60
|
+
destinationAmountMin: amount,
|
|
61
|
+
provider: { id: 'stargate', name: 'Stargate' },
|
|
62
|
+
recipient,
|
|
63
|
+
sender,
|
|
64
|
+
sourceAmount: amount,
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
await Transfer.registerSourceTransaction(db, {
|
|
68
|
+
chainId: 'eip155:8453',
|
|
69
|
+
id: transfer.id,
|
|
70
|
+
providerState: { stargate: { guid, sourceEid: 30_184 } },
|
|
71
|
+
transactionHash: sourceTransactionHash,
|
|
72
|
+
})
|
|
73
|
+
await Catalog.publish(db, {
|
|
74
|
+
...TestFunding.data,
|
|
75
|
+
chains: TestFunding.data.chains.map((chain) =>
|
|
76
|
+
chain.id === 'eip155:4217' ? { ...chain, rpcUrls: [destination.rpcUrl] } : chain,
|
|
77
|
+
),
|
|
78
|
+
routes: TestFunding.data.routes.filter((route) => route.providerId !== 'stargate'),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const tracker = TransferReconciliation.createTracker({ db })
|
|
82
|
+
await expect(
|
|
83
|
+
tracker.reconcile({ transferId: transfer.id, type: 'funding:transfer:reconcile' }),
|
|
84
|
+
).resolves.toEqual({ type: 'completed' })
|
|
85
|
+
|
|
86
|
+
const completed = await Transfer.transition(db, {
|
|
87
|
+
expectedVersion: 2,
|
|
88
|
+
id: transfer.id,
|
|
89
|
+
status: 'completed',
|
|
90
|
+
})
|
|
91
|
+
expect(completed.type).toBe('stale')
|
|
92
|
+
if (completed.type !== 'stale') return
|
|
93
|
+
expect({
|
|
94
|
+
destinationTransactionHashes: completed.current.snapshot.destinationTransactionHashes,
|
|
95
|
+
status: completed.current.status,
|
|
96
|
+
version: completed.current.version,
|
|
97
|
+
}).toEqual({
|
|
98
|
+
destinationTransactionHashes: [destinationTransactionHash],
|
|
99
|
+
status: 'completed',
|
|
100
|
+
version: 3,
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('tick', () => {
|
|
106
|
+
test('advances through bounded recovery pages', async () => {
|
|
107
|
+
const database = TestApp.database()
|
|
108
|
+
await TestFunding.publish(database)
|
|
109
|
+
for (let index = 0; index < 26; index++) {
|
|
110
|
+
const transfer = await Transfer.create(database, {
|
|
111
|
+
apiKeyId: 'key_test',
|
|
112
|
+
environment: 'production',
|
|
113
|
+
id: Transfer.generateId(new Date(Date.UTC(2026, 0, 1, 0, 0, index))),
|
|
114
|
+
orgId: 'org_test',
|
|
115
|
+
providerState: {},
|
|
116
|
+
snapshot: TestFunding.transferSnapshot({
|
|
117
|
+
provider: { id: 'stargate', name: 'Stargate' },
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
await Transfer.transition(database, {
|
|
121
|
+
expectedVersion: 1,
|
|
122
|
+
id: transfer.id,
|
|
123
|
+
status: 'processing',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type Dispatch = { delaySeconds: number; message: TransferReconciliation.Message }
|
|
128
|
+
const dispatches: Dispatch[] = []
|
|
129
|
+
let available = false
|
|
130
|
+
const tracker = TransferReconciliation.createTracker({
|
|
131
|
+
db: database,
|
|
132
|
+
dispatch: async (message, options) => {
|
|
133
|
+
dispatches.push({ delaySeconds: options.delaySeconds, message })
|
|
134
|
+
if (!available) throw new Error('queue unavailable')
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
await expect(tracker.tick()).rejects.toThrow('queue unavailable')
|
|
138
|
+
available = true
|
|
139
|
+
await expect(tracker.tick()).resolves.toEqual({ checked: 25 })
|
|
140
|
+
await expect(tracker.tick()).resolves.toEqual({ checked: 1 })
|
|
141
|
+
|
|
142
|
+
expect(dispatches).toHaveLength(27)
|
|
143
|
+
expect(dispatches[0]).toEqual(dispatches[1])
|
|
144
|
+
expect(new Set(dispatches.map(({ delaySeconds }) => delaySeconds))).toEqual(new Set([0]))
|
|
145
|
+
expect(new Set(dispatches.map(({ message }) => message.transferId)).size).toBe(26)
|
|
146
|
+
})
|
|
147
|
+
})
|