tapimo 0.9.0 → 0.10.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/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +4292 -3708
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +21 -20
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +66 -23
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +26 -17
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +168 -168
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4280 -84
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +30 -10
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +5799 -4929
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +4 -5
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +19 -4
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +3391 -492
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +372 -29
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +853 -339
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +10 -10
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +145 -83
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/earn.d.ts +455 -1
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +369 -45
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +114 -118
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +35 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +68 -47
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/tokens.d.ts +461 -459
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +313 -305
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +25 -23
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +108 -86
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +618 -105
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +519 -868
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +642 -353
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +4 -2
- package/dist/apps/data/routes/zones.js.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/providers.d.ts +13 -13
- package/dist/apps/funding/routes/quotes.d.ts +89 -89
- package/dist/apps/funding/routes/transfers.d.ts +36 -36
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +11 -309
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +74 -74
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +134 -134
- package/dist/apps/management/routes/invite-links.d.ts +80 -80
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +98 -98
- package/dist/apps/management/routes/projects.d.ts +60 -60
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
- package/dist/apps/management/routes/webhooks.d.ts +203 -618
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +56 -44
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +3 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +3 -0
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +9 -2
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +4 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +75 -15
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +4 -1
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +86 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +139 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +144 -4
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Schema.js +1 -1
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +1 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +1 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +2 -1
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts +2 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +11 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +75 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +299 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +176 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +216 -20
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +4 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +11 -7
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +128 -0
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +6 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +71 -9
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +4 -4
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +61 -5
- package/src/Webhooks.bench.ts +714 -0
- package/src/Webhooks.memory.bench.ts +250 -0
- package/src/Webhooks.test.ts +77 -6
- package/src/Webhooks.ts +85 -29
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +23 -5
- package/src/apps/Zones.ts +31 -10
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +122 -2
- package/src/apps/data/routes/activities.ts +435 -32
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.test.ts +119 -10
- package/src/apps/data/routes/balances.ts +164 -97
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.test.ts +197 -0
- package/src/apps/data/routes/earn.ts +487 -45
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/receipts.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/tokens.test.ts +51 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -0
- package/src/apps/data/routes/transactions.test.ts +42 -0
- package/src/apps/data/routes/transactions.ts +44 -39
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/transfers.test.ts +402 -24
- package/src/apps/data/routes/transfers.ts +708 -101
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +466 -135
- package/src/apps/data/routes/webhooks.ts +820 -429
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +7 -2
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/apps/mpp/Mpp.test.ts +13 -1
- package/src/apps/mpp/Mpp.ts +8 -0
- package/src/apps/mpp/internal/Adapter.ts +10 -2
- package/src/db/Db.test.ts +40 -6
- package/src/db/Db.ts +114 -19
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +131 -4
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +575 -0
- package/src/internal/MetricSink.ts +325 -10
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Schema.ts +1 -1
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +1 -3
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/Viem.test.ts +8 -0
- package/src/internal/Viem.ts +2 -1
- package/src/internal/WebhookDestination.test.ts +20 -0
- package/src/internal/WebhookDestination.ts +10 -0
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +430 -0
- package/src/internal/Webhooks.test.ts +616 -0
- package/src/internal/Webhooks.ts +412 -24
- package/src/internal/funding/Provider.test.ts +48 -8
- package/src/internal/funding/Provider.ts +12 -7
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1240 -0
- package/src/internal/funding/Reconciliation.ts +136 -0
- package/src/internal/funding/SourceObservation.test.ts +1 -0
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +2 -9
- package/src/internal/funding/providers/relay.ts +72 -13
|
@@ -21,6 +21,8 @@ const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as cons
|
|
|
21
21
|
>
|
|
22
22
|
const maximumPages = 100
|
|
23
23
|
const maximumFollowUpAttempts = 6
|
|
24
|
+
const providerFailureStatuses = ['failure'] as const
|
|
25
|
+
const providerRefundStatuses = ['refund', 'refunded'] as const
|
|
24
26
|
|
|
25
27
|
/** Verified token transfer returned by an authoritative chain reader. */
|
|
26
28
|
export type TransferEvidence = {
|
|
@@ -866,6 +868,16 @@ export async function reconcileRequest(
|
|
|
866
868
|
}
|
|
867
869
|
if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
|
|
868
870
|
if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
|
|
871
|
+
const recovery = await reconcileProviderRecovery(db, { ...options, verified })
|
|
872
|
+
if (recovery) {
|
|
873
|
+
for (const id of recovery.updatedIds) updatedIds.add(id)
|
|
874
|
+
return {
|
|
875
|
+
created,
|
|
876
|
+
...(recovery.followUp ? { followUp: recovery.followUp } : {}),
|
|
877
|
+
terminal: recovery.terminal,
|
|
878
|
+
updated: updatedIds.size,
|
|
879
|
+
}
|
|
880
|
+
}
|
|
869
881
|
const delivery = await reconcileDelivery(db, { ...options, verified })
|
|
870
882
|
for (const id of delivery.updatedIds) updatedIds.add(id)
|
|
871
883
|
return {
|
|
@@ -910,6 +922,130 @@ export declare namespace reconcileRequest {
|
|
|
910
922
|
}
|
|
911
923
|
}
|
|
912
924
|
|
|
925
|
+
async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOptions) {
|
|
926
|
+
const failed = (providerFailureStatuses as readonly string[]).includes(
|
|
927
|
+
options.request?.status ?? '',
|
|
928
|
+
)
|
|
929
|
+
const refunded = (providerRefundStatuses as readonly string[]).includes(
|
|
930
|
+
options.request?.status ?? '',
|
|
931
|
+
)
|
|
932
|
+
if (!refunded && !failed) return undefined
|
|
933
|
+
if (failed) {
|
|
934
|
+
const destinationEvidence = await Promise.all(
|
|
935
|
+
unique(options.request?.destinationTransactionHashes ?? []).map((transactionHash) =>
|
|
936
|
+
options.verifyTransfers({
|
|
937
|
+
chain: options.address.snapshot.destinationChain,
|
|
938
|
+
recipient: options.address.recipient,
|
|
939
|
+
token: options.address.providerOutputToken,
|
|
940
|
+
transactionHash,
|
|
941
|
+
}),
|
|
942
|
+
),
|
|
943
|
+
)
|
|
944
|
+
if (destinationEvidence.some((transfers) => transfers.length > 0)) return undefined
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const updatedIds = new Set<string>()
|
|
948
|
+
if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
|
|
949
|
+
return { terminal: true, updatedIds }
|
|
950
|
+
|
|
951
|
+
// Relay auto-refunds may include an internal settlement transaction; only matching source-token transfers become public evidence.
|
|
952
|
+
const providerRefundTransactionHashes = unique(options.request?.refundTransactionHashes ?? [])
|
|
953
|
+
const refundEvidence = await Promise.all(
|
|
954
|
+
providerRefundTransactionHashes.map((transactionHash) =>
|
|
955
|
+
options.verifyTransfers({
|
|
956
|
+
chain: options.address.snapshot.sourceChain,
|
|
957
|
+
recipient: options.address.refundAddress,
|
|
958
|
+
token: options.address.snapshot.sourceToken,
|
|
959
|
+
transactionHash,
|
|
960
|
+
}),
|
|
961
|
+
),
|
|
962
|
+
)
|
|
963
|
+
const refundTransfers = refundEvidence.flat()
|
|
964
|
+
const refundOutput = refundTransfers.reduce(
|
|
965
|
+
(total, transfer) => total + BigInt(transfer.amount),
|
|
966
|
+
0n,
|
|
967
|
+
)
|
|
968
|
+
const refundTransactionHashes = unique(
|
|
969
|
+
refundTransfers.map((transfer) => transfer.transactionHash),
|
|
970
|
+
)
|
|
971
|
+
// Relay can include internal transactions, so its reported amount proves when all matching refund transfers are visible.
|
|
972
|
+
const expectedRefundOutput = options.request?.refundAmountExpected
|
|
973
|
+
? BigInt(options.request.refundAmountExpected)
|
|
974
|
+
: undefined
|
|
975
|
+
const refundEvidenceComplete = (() => {
|
|
976
|
+
if (refundOutput === 0n) return false
|
|
977
|
+
if (expectedRefundOutput !== undefined) return refundOutput === expectedRefundOutput
|
|
978
|
+
return refundEvidence.length > 0 && refundEvidence.every((transfers) => transfers.length > 0)
|
|
979
|
+
})()
|
|
980
|
+
const totalSource = options.verified.reduce((total, entry) => total + entry.sourceAmount, 0n)
|
|
981
|
+
let allocated = 0n
|
|
982
|
+
let cumulativeSource = 0n
|
|
983
|
+
let terminal = refundEvidenceComplete
|
|
984
|
+
|
|
985
|
+
// Include terminal rows in proportional allocation so retries preserve the original split without reallocating their verified refunds.
|
|
986
|
+
for (const entry of options.verified) {
|
|
987
|
+
cumulativeSource += entry.sourceAmount
|
|
988
|
+
const nextAllocated = totalSource === 0n ? 0n : (refundOutput * cumulativeSource) / totalSource
|
|
989
|
+
const amount = nextAllocated - allocated
|
|
990
|
+
allocated = nextAllocated
|
|
991
|
+
if (Deposit.isTerminal(entry.deposit.status)) continue
|
|
992
|
+
let deposit = entry.deposit
|
|
993
|
+
if (deposit.status !== 'refunding') {
|
|
994
|
+
const result = await Deposit.transition(db, {
|
|
995
|
+
expectedVersion: deposit.version,
|
|
996
|
+
id: deposit.id,
|
|
997
|
+
providerTransactionHashes: unique([
|
|
998
|
+
...deposit.providerTransactionHashes,
|
|
999
|
+
...providerRefundTransactionHashes,
|
|
1000
|
+
]),
|
|
1001
|
+
status: 'refunding',
|
|
1002
|
+
})
|
|
1003
|
+
if (result.type === 'applied') {
|
|
1004
|
+
deposit = result.record
|
|
1005
|
+
updatedIds.add(deposit.id)
|
|
1006
|
+
} else if (result.type === 'stale') deposit = result.current
|
|
1007
|
+
}
|
|
1008
|
+
if (!refundEvidenceComplete) continue
|
|
1009
|
+
if (Deposit.isTerminal(deposit.status)) continue
|
|
1010
|
+
if (deposit.status !== 'refunding') {
|
|
1011
|
+
terminal = false
|
|
1012
|
+
continue
|
|
1013
|
+
}
|
|
1014
|
+
const result = await Deposit.transition(db, {
|
|
1015
|
+
expectedVersion: deposit.version,
|
|
1016
|
+
id: deposit.id,
|
|
1017
|
+
snapshot: {
|
|
1018
|
+
...deposit.snapshot,
|
|
1019
|
+
refundAmount: Value.tokenAmount({
|
|
1020
|
+
baseUnits: amount,
|
|
1021
|
+
currency: options.address.snapshot.sourceToken.currency,
|
|
1022
|
+
decimals: options.address.snapshot.sourceToken.decimals,
|
|
1023
|
+
}),
|
|
1024
|
+
refundTransactionHashes,
|
|
1025
|
+
},
|
|
1026
|
+
status: 'refunded',
|
|
1027
|
+
})
|
|
1028
|
+
if (result.type === 'applied') updatedIds.add(deposit.id)
|
|
1029
|
+
else if (result.type !== 'stale' || !Deposit.isTerminal(result.current.status)) terminal = false
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const outcome = (() => {
|
|
1033
|
+
if (refundEvidenceComplete) return 'verified'
|
|
1034
|
+
// Relay failure can be followed by an automatic refund; durable exhaustion owns escalation to action-required.
|
|
1035
|
+
if (failed) return 'provider_failed'
|
|
1036
|
+
return 'pending'
|
|
1037
|
+
})()
|
|
1038
|
+
options.metrics?.count('funding_deposit_refund_count', 1, {
|
|
1039
|
+
outcome,
|
|
1040
|
+
provider: options.address.providerId,
|
|
1041
|
+
})
|
|
1042
|
+
return {
|
|
1043
|
+
...(!refundEvidenceComplete ? { followUp: 'delivery' as const } : {}),
|
|
1044
|
+
terminal,
|
|
1045
|
+
updatedIds,
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
913
1049
|
async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
|
|
914
1050
|
let followUp: reconcileRequest.ReturnType['followUp']
|
|
915
1051
|
let terminal = true
|
|
@@ -188,6 +188,7 @@ describe('observe', () => {
|
|
|
188
188
|
createdAt: '2026-01-01T00:00:02.000Z',
|
|
189
189
|
destinationTransactionHashes: [],
|
|
190
190
|
id: 'relay-source-request',
|
|
191
|
+
refundTransactionHashes: [],
|
|
191
192
|
sourceTransactionHashes: [sourceTransactionHash],
|
|
192
193
|
status: 'pending',
|
|
193
194
|
updatedAt: '2026-01-01T00:00:03.000Z',
|
|
@@ -7,13 +7,15 @@ import * as Squid from './squid.js'
|
|
|
7
7
|
import * as Symbiosis from './symbiosis.js'
|
|
8
8
|
|
|
9
9
|
const now = () => new Date('2026-01-01T00:00:00.000Z')
|
|
10
|
+
const evmUserAddress = TestFunding.relayQuoteUsers['eip155:4217']
|
|
11
|
+
const solanaUserAddress = '7ZwiMrYFNCzZ4TcPkeMx7BTZSZ9jDgGR8H8Nkazhr6L1'
|
|
10
12
|
const providers = [
|
|
11
13
|
Across.across({
|
|
12
14
|
apiKey: 'across-api-key',
|
|
13
15
|
integratorId: '0xdead',
|
|
14
16
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
15
17
|
}),
|
|
16
|
-
Relay.relay({
|
|
18
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
17
19
|
Rhino.rhino(),
|
|
18
20
|
Squid.squid({
|
|
19
21
|
integratorId: 'squid-integrator-id',
|
|
@@ -141,7 +143,7 @@ describe('funding providers', () => {
|
|
|
141
143
|
apiKey: 'relay-api-key',
|
|
142
144
|
fetch,
|
|
143
145
|
now,
|
|
144
|
-
|
|
146
|
+
userAddresses: TestFunding.relayQuoteUsers,
|
|
145
147
|
})
|
|
146
148
|
|
|
147
149
|
const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
|
|
@@ -192,6 +194,74 @@ describe('funding providers', () => {
|
|
|
192
194
|
`)
|
|
193
195
|
})
|
|
194
196
|
|
|
197
|
+
test('requires a Relay quote wallet for the source chain', async () => {
|
|
198
|
+
const requests: RecordedRequest[] = []
|
|
199
|
+
const candidate = quoteCandidate('relay', {
|
|
200
|
+
destinationToken: 'pathUSD',
|
|
201
|
+
sourceChain: 'solana',
|
|
202
|
+
})
|
|
203
|
+
const provider = Relay.relay({
|
|
204
|
+
fetch: recordingFetch(requests, async () => jsonResponse({})),
|
|
205
|
+
now,
|
|
206
|
+
userAddresses: { 'eip155:4217': evmUserAddress },
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
|
|
210
|
+
|
|
211
|
+
expect(requests).toHaveLength(0)
|
|
212
|
+
expect(result).toMatchObject({
|
|
213
|
+
quality: {
|
|
214
|
+
sourceDetail: 'relay:quote-v2:QUOTE_USER_NOT_CONFIGURED',
|
|
215
|
+
tier: 'unavailable',
|
|
216
|
+
},
|
|
217
|
+
status: 'unavailable',
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('uses chain-specific Relay quote wallets', async () => {
|
|
222
|
+
const requests: RecordedRequest[] = []
|
|
223
|
+
const candidate = quoteCandidate('relay', {
|
|
224
|
+
destinationToken: 'pathUSD',
|
|
225
|
+
sourceChain: 'solana',
|
|
226
|
+
})
|
|
227
|
+
const fetch = recordingFetch(requests, async () =>
|
|
228
|
+
jsonResponse({
|
|
229
|
+
details: {
|
|
230
|
+
currencyOut: { amount: '990000', minimumAmount: '980000' },
|
|
231
|
+
timeEstimate: 15,
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
const provider = Relay.relay({
|
|
236
|
+
fetch,
|
|
237
|
+
now,
|
|
238
|
+
userAddresses: {
|
|
239
|
+
'eip155:4217': evmUserAddress,
|
|
240
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': solanaUserAddress,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
|
|
245
|
+
|
|
246
|
+
expect(jsonRequestBody(requests[0])).toMatchObject({
|
|
247
|
+
destinationChainId: 4217,
|
|
248
|
+
originChainId: 792703809,
|
|
249
|
+
recipient: evmUserAddress,
|
|
250
|
+
user: solanaUserAddress,
|
|
251
|
+
})
|
|
252
|
+
expect(result).toMatchObject({ status: 'available' })
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('rejects invalid chain-specific Relay quote wallets', () => {
|
|
256
|
+
expect(() =>
|
|
257
|
+
Relay.relay({
|
|
258
|
+
userAddresses: {
|
|
259
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': '0x1111111111111111111111111111111111111111',
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
).toThrowErrorMatchingInlineSnapshot(`[FundingProvider.ProviderConfigurationError]`)
|
|
263
|
+
})
|
|
264
|
+
|
|
195
265
|
test('maps successful Squid quotes to available quote results', async () => {
|
|
196
266
|
const requests: RecordedRequest[] = []
|
|
197
267
|
const candidate = quoteCandidate('squid')
|
|
@@ -646,7 +716,7 @@ describe('funding providers', () => {
|
|
|
646
716
|
try {
|
|
647
717
|
const provider = Relay.relay({
|
|
648
718
|
now,
|
|
649
|
-
|
|
719
|
+
userAddresses: TestFunding.relayQuoteUsers,
|
|
650
720
|
})
|
|
651
721
|
const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
|
|
652
722
|
|
|
@@ -123,7 +123,6 @@ describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => {
|
|
|
123
123
|
requestBody = JSON.parse(init.body) as Record<string, unknown>
|
|
124
124
|
return globalThis.fetch(input, init)
|
|
125
125
|
},
|
|
126
|
-
userAddress: sender,
|
|
127
126
|
})
|
|
128
127
|
result = await provider.prepareTransfer!(prepareInput(provider), new AbortController().signal)
|
|
129
128
|
})
|
|
@@ -151,11 +150,8 @@ describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => {
|
|
|
151
150
|
expect(result.correlation.requestId).toMatch(/^0x[0-9a-f]{64}$/)
|
|
152
151
|
expect(result.correlation.checkEndpoint).toContain('/intents/status/v3')
|
|
153
152
|
expect(BigInt(result.destinationAmount) > 0n).toBe(true)
|
|
153
|
+
expect(BigInt(result.destinationAmountMin) > 0n).toBe(true)
|
|
154
154
|
expect(BigInt(result.destinationAmountMin) <= BigInt(result.destinationAmount)).toBe(true)
|
|
155
|
-
// The forwarded slippage bound holds on the quoted minimum.
|
|
156
|
-
expect(
|
|
157
|
-
BigInt(result.destinationAmountMin) >= (BigInt(result.destinationAmount) * 9_700n) / 10_000n,
|
|
158
|
-
).toBe(true)
|
|
159
155
|
// Relay returns no quote validity window; the fan-out applies the default.
|
|
160
156
|
expect(result.expiresAt).toBeUndefined()
|
|
161
157
|
expect(Number.isNaN(Date.parse(result.sampledAt))).toBe(false)
|
|
@@ -175,7 +171,6 @@ describe.skipIf(!apiKey)('relay.createDepositAddress (live)', () => {
|
|
|
175
171
|
return globalThis.fetch(input, init)
|
|
176
172
|
},
|
|
177
173
|
subsidies: { maxAmount: '100' },
|
|
178
|
-
userAddress: sender,
|
|
179
174
|
})
|
|
180
175
|
result = await provider.createDepositAddress!(
|
|
181
176
|
{ ...depositAddressInput(provider), subsidize: true },
|
|
@@ -413,9 +408,7 @@ describe('relay.createDepositAddress', () => {
|
|
|
413
408
|
|
|
414
409
|
describe('relay.prepareTransfer', () => {
|
|
415
410
|
test('refuses methods and modes it cannot prepare', async () => {
|
|
416
|
-
const provider = Relay.relay(
|
|
417
|
-
userAddress: sender,
|
|
418
|
-
})
|
|
411
|
+
const provider = Relay.relay()
|
|
419
412
|
await expect(
|
|
420
413
|
provider.prepareTransfer!(
|
|
421
414
|
{ ...prepareInput(provider), method: 'depositAddress' },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbiEvent, AbiFunction, Address, Hash, Hex, RpcTransport } from 'ox'
|
|
1
|
+
import { AbiEvent, AbiFunction, Address, Base58, Bytes, Hash, Hex, RpcTransport } from 'ox'
|
|
2
2
|
import { createClient, custom, fallback } from 'viem'
|
|
3
3
|
import { getBlockNumber, getLogs, getTransaction, getTransactionReceipt } from 'viem/actions'
|
|
4
4
|
import * as z from 'zod/mini'
|
|
@@ -17,6 +17,7 @@ const defaultBaseUrl = 'https://api.relay.link'
|
|
|
17
17
|
const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init)
|
|
18
18
|
const deliveryBackfillBlocks = 256n
|
|
19
19
|
const relayChainsCacheTtlMs = 60_000
|
|
20
|
+
const relayRefundStatuses = ['refund', 'refunded'] as const
|
|
20
21
|
const sourceSweepForwardBlocks = 256n
|
|
21
22
|
const depositErc20FullAllowance = AbiFunction.from(
|
|
22
23
|
'function depositErc20(address depositor,address token,bytes32 id)',
|
|
@@ -46,6 +47,7 @@ const webhookStatuses = [
|
|
|
46
47
|
'fallback',
|
|
47
48
|
'pending',
|
|
48
49
|
'refund',
|
|
50
|
+
'refunded',
|
|
49
51
|
'submitted',
|
|
50
52
|
'success',
|
|
51
53
|
'waiting',
|
|
@@ -196,6 +198,13 @@ namespace schema {
|
|
|
196
198
|
),
|
|
197
199
|
),
|
|
198
200
|
id: z.string().check(z.minLength(1)),
|
|
201
|
+
refundCurrencyData: z.optional(
|
|
202
|
+
z.nullable(
|
|
203
|
+
z.object({
|
|
204
|
+
amount: z.string().check(z.regex(/^\d+$/)),
|
|
205
|
+
}),
|
|
206
|
+
),
|
|
207
|
+
),
|
|
199
208
|
protocol: z.optional(
|
|
200
209
|
z.nullable(
|
|
201
210
|
z.object({
|
|
@@ -244,11 +253,9 @@ export function parseDepositAddressRequests(value: unknown) {
|
|
|
244
253
|
|
|
245
254
|
/** Creates a Relay funding provider backed by `/quote/v2`. */
|
|
246
255
|
export function relay(options: relay.Options = {}): FundingProvider.Provider<'relay'> {
|
|
256
|
+
const userAddresses = parseUserAddresses(options.userAddresses)
|
|
247
257
|
if (
|
|
248
|
-
(
|
|
249
|
-
!Address.validate(options.userAddress, { strict: false })) ||
|
|
250
|
-
(options.recipientAddress !== undefined &&
|
|
251
|
-
!Address.validate(options.recipientAddress, { strict: false }))
|
|
258
|
+
Object.entries(userAddresses).some(([chainId, address]) => invalidUserAddress(chainId, address))
|
|
252
259
|
)
|
|
253
260
|
throw new FundingProvider.ProviderConfigurationError()
|
|
254
261
|
|
|
@@ -402,13 +409,22 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
|
|
|
402
409
|
}
|
|
403
410
|
},
|
|
404
411
|
async getQuote({ candidate, route }, signal) {
|
|
405
|
-
|
|
412
|
+
const userAddress = userAddresses[route.source.chain.id]
|
|
413
|
+
if (userAddress === undefined)
|
|
406
414
|
return FundingProvider.unavailableResult({
|
|
407
415
|
detail: 'relay:quote-v2',
|
|
408
416
|
message: 'QUOTE_USER_NOT_CONFIGURED',
|
|
409
417
|
now: now(),
|
|
410
418
|
source: 'providerQuote',
|
|
411
419
|
})
|
|
420
|
+
const recipientAddress = userAddresses[route.destination.chain.id]
|
|
421
|
+
if (recipientAddress === undefined)
|
|
422
|
+
return FundingProvider.unavailableResult({
|
|
423
|
+
detail: 'relay:quote-v2',
|
|
424
|
+
message: 'QUOTE_RECIPIENT_NOT_CONFIGURED',
|
|
425
|
+
now: now(),
|
|
426
|
+
source: 'providerQuote',
|
|
427
|
+
})
|
|
412
428
|
const destinationChainId = relayChainId(route.destination.chain)
|
|
413
429
|
const sourceChainId = relayChainId(route.source.chain)
|
|
414
430
|
try {
|
|
@@ -419,9 +435,9 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
|
|
|
419
435
|
destinationCurrency: candidate.destinationToken.address,
|
|
420
436
|
originChainId: sourceChainId,
|
|
421
437
|
originCurrency: candidate.sourceToken.address,
|
|
422
|
-
recipient:
|
|
438
|
+
recipient: recipientAddress,
|
|
423
439
|
tradeType: 'EXACT_INPUT',
|
|
424
|
-
user:
|
|
440
|
+
user: userAddress,
|
|
425
441
|
},
|
|
426
442
|
fetch,
|
|
427
443
|
headers: options.apiKey ? { 'x-api-key': options.apiKey } : undefined,
|
|
@@ -481,6 +497,8 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
|
|
|
481
497
|
return {
|
|
482
498
|
...(parsed.continuation ? { continuation: parsed.continuation } : {}),
|
|
483
499
|
requests: parsed.requests.map((request) => {
|
|
500
|
+
const outputTransactionHashes = request.data?.outTxs?.map((tx) => tx.txHash) ?? []
|
|
501
|
+
const refunded = (relayRefundStatuses as readonly string[]).includes(request.status)
|
|
484
502
|
const correlation = (() => {
|
|
485
503
|
const { protocol } = request
|
|
486
504
|
if (!protocol) return undefined
|
|
@@ -499,9 +517,13 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
|
|
|
499
517
|
})()
|
|
500
518
|
return {
|
|
501
519
|
createdAt: new Date(request.createdAt).toISOString(),
|
|
502
|
-
destinationTransactionHashes:
|
|
520
|
+
destinationTransactionHashes: refunded ? [] : outputTransactionHashes,
|
|
503
521
|
id: request.id,
|
|
504
522
|
...(correlation ? { providerState: correlation } : {}),
|
|
523
|
+
...(refunded && request.refundCurrencyData
|
|
524
|
+
? { refundAmountExpected: request.refundCurrencyData.amount }
|
|
525
|
+
: {}),
|
|
526
|
+
refundTransactionHashes: refunded ? outputTransactionHashes : [],
|
|
505
527
|
sourceTransactionHashes: request.depositAddress?.depositTxHash
|
|
506
528
|
? [request.depositAddress.depositTxHash]
|
|
507
529
|
: [],
|
|
@@ -606,12 +628,10 @@ export declare namespace relay {
|
|
|
606
628
|
now?: (() => Date) | undefined
|
|
607
629
|
/** EVM chain evidence used to accelerate authenticated provider delivery. */
|
|
608
630
|
observation?: Observation | undefined
|
|
609
|
-
/** Optional recipient override; defaults to `userAddress`. */
|
|
610
|
-
recipientAddress?: string | undefined
|
|
611
631
|
/** Deposit subsidy policy enabled for this integration. */
|
|
612
632
|
subsidies?: Subsidy.Policy | undefined
|
|
613
|
-
/** Optional
|
|
614
|
-
|
|
633
|
+
/** Optional source and destination quote wallets keyed by CAIP-2 chain id. */
|
|
634
|
+
userAddresses?: Readonly<Partial<Record<FundingChain.Id, string>>> | string | undefined
|
|
615
635
|
/** Tempo infrastructure used to receive authenticated Relay webhooks. */
|
|
616
636
|
webhook?: Webhook | undefined
|
|
617
637
|
}
|
|
@@ -647,6 +667,45 @@ export declare namespace relay {
|
|
|
647
667
|
| 'unknown_address'
|
|
648
668
|
}
|
|
649
669
|
|
|
670
|
+
function parseUserAddresses(value: relay.Options['userAddresses']) {
|
|
671
|
+
if (!value) return {}
|
|
672
|
+
const addresses =
|
|
673
|
+
typeof value === 'string'
|
|
674
|
+
? (() => {
|
|
675
|
+
try {
|
|
676
|
+
return JSON.parse(value) as unknown
|
|
677
|
+
} catch {
|
|
678
|
+
throw new FundingProvider.ProviderConfigurationError()
|
|
679
|
+
}
|
|
680
|
+
})()
|
|
681
|
+
: value
|
|
682
|
+
if (!addresses || typeof addresses !== 'object' || Array.isArray(addresses))
|
|
683
|
+
throw new FundingProvider.ProviderConfigurationError()
|
|
684
|
+
return addresses as Readonly<Partial<Record<FundingChain.Id, string>>>
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function invalidUserAddress(chainId: string, address: unknown) {
|
|
688
|
+
if (typeof address !== 'string') return true
|
|
689
|
+
if (/^eip155:\d+$/.test(chainId)) return !Address.validate(address, { strict: false })
|
|
690
|
+
if (/^solana:[1-9A-HJ-NP-Za-km-z]+$/.test(chainId)) {
|
|
691
|
+
try {
|
|
692
|
+
return Base58.toBytes(address).length !== 32
|
|
693
|
+
} catch {
|
|
694
|
+
return true
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (!/^tron:0x[0-9a-fA-F]+$/.test(chainId)) return true
|
|
698
|
+
try {
|
|
699
|
+
const bytes = Base58.toBytes(address)
|
|
700
|
+
if (bytes.length !== 25 || bytes[0] !== 0x41) return true
|
|
701
|
+
const payload = bytes.slice(0, 21)
|
|
702
|
+
const checksum = Hash.sha256(Hash.sha256(payload), { as: 'Bytes' }).slice(0, 4)
|
|
703
|
+
return !Bytes.isEqual(bytes.slice(21), checksum)
|
|
704
|
+
} catch {
|
|
705
|
+
return true
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
650
709
|
type DepositAddressObserverOptions = relay.Observation & {
|
|
651
710
|
apiKey?: string | undefined
|
|
652
711
|
baseUrl?: string | undefined
|