@provex/proving 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/adapters/extensionEnclave.cjs +160 -0
- package/dist/adapters/extensionEnclave.cjs.map +1 -0
- package/dist/adapters/extensionEnclave.d.cts +5 -0
- package/dist/adapters/extensionEnclave.d.ts +5 -0
- package/dist/adapters/extensionEnclave.js +3 -0
- package/dist/adapters/extensionEnclave.js.map +1 -0
- package/dist/chunk-LFDS3XGK.js +123 -0
- package/dist/chunk-LFDS3XGK.js.map +1 -0
- package/dist/chunk-QLPNUQDA.js +157 -0
- package/dist/chunk-QLPNUQDA.js.map +1 -0
- package/dist/extensionEnclave-BjxXsuaj.d.cts +119 -0
- package/dist/extensionEnclave-DLCjCWxT.d.ts +119 -0
- package/dist/index.cjs +321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/types-ByiBb7AN.d.cts +144 -0
- package/dist/types-ByiBb7AN.d.ts +144 -0
- package/dist/useProving.cjs +125 -0
- package/dist/useProving.cjs.map +1 -0
- package/dist/useProving.d.cts +91 -0
- package/dist/useProving.d.ts +91 -0
- package/dist/useProving.js +3 -0
- package/dist/useProving.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @provex/proving
|
|
2
|
+
|
|
3
|
+
Headless orchestrator for the Provex buy-flow proving pipeline (enclave /
|
|
4
|
+
Trusted Execution Environment path). Exposes the `useProving` hook, the
|
|
5
|
+
`CaptureAdapter` seam, and the enclave attestation client. Depends on
|
|
6
|
+
`@provex/react` for the on-chain `fulfillIntent` rail and indexer sync.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var viem = require('viem');
|
|
4
|
+
var payment = require('@provex/utils/payment');
|
|
5
|
+
var conversionRates = require('@provex/utils/conversionRates');
|
|
6
|
+
|
|
7
|
+
// src/adapters/extensionEnclave.ts
|
|
8
|
+
|
|
9
|
+
// src/types.ts
|
|
10
|
+
var EnclaveCaptureUnavailable = class extends Error {
|
|
11
|
+
platform;
|
|
12
|
+
constructor(platform) {
|
|
13
|
+
super(`No enclave (buyerTee) capture available for platform "${platform}".`);
|
|
14
|
+
this.name = "EnclaveCaptureUnavailable";
|
|
15
|
+
this.platform = platform;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var ManualSelectionRequired = class extends Error {
|
|
19
|
+
candidates;
|
|
20
|
+
constructor(candidates) {
|
|
21
|
+
super("Manual transaction selection is required but no resolver was provided.");
|
|
22
|
+
this.name = "ManualSelectionRequired";
|
|
23
|
+
this.candidates = candidates;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/buyerTeeAttestation.ts
|
|
28
|
+
function asBuyerTeeProof(proof) {
|
|
29
|
+
if (!proof || typeof proof !== "object") return null;
|
|
30
|
+
const candidate = proof;
|
|
31
|
+
if (candidate.proofType !== "buyerTee") return null;
|
|
32
|
+
if (typeof candidate.encryptedSessionMaterial !== "string") return null;
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
async function requestBuyerTeeAttestation(args) {
|
|
36
|
+
const url = `${args.attestationBaseUrl}/api/v0/attestation/buyer-verify/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.actionType)}`;
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
encryptedSessionMaterial: args.encryptedSessionMaterial,
|
|
42
|
+
params: args.params,
|
|
43
|
+
verifyingContract: args.verifyingContract,
|
|
44
|
+
intent: args.intent
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const detail = await response.text().catch(() => "");
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Trusted-execution attestation failed (${response.status}): ${detail || response.statusText}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const json = await response.json();
|
|
54
|
+
const responseObject = json.responseObject ?? json;
|
|
55
|
+
if (!responseObject?.typedDataValue || !responseObject?.signature) {
|
|
56
|
+
throw new Error("Trusted-execution attestation response missing typedDataValue/signature");
|
|
57
|
+
}
|
|
58
|
+
return responseObject;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/adapters/extensionEnclave.ts
|
|
62
|
+
function normalizeAmount(amount, decimals, centIsInt) {
|
|
63
|
+
const raw = String(amount);
|
|
64
|
+
const isFormattedDollars = /[^0-9\-]/.test(raw) && raw.includes(".");
|
|
65
|
+
const cleaned = raw.replace(/[^0-9.]/g, "");
|
|
66
|
+
let value = viem.parseUnits(cleaned, decimals);
|
|
67
|
+
if (centIsInt && !isFormattedDollars) {
|
|
68
|
+
value = value / 100n;
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function autoMatch(candidates, request) {
|
|
73
|
+
const currencyAmount = conversionRates.convertTokenInputToCurrency({
|
|
74
|
+
token: request.token,
|
|
75
|
+
amountOutInt: BigInt(request.intent.amount),
|
|
76
|
+
rate: BigInt(request.intent.conversionRate)
|
|
77
|
+
});
|
|
78
|
+
const configs = payment.providerConfigs(request.chainId);
|
|
79
|
+
const providerConfig = configs[request.provider];
|
|
80
|
+
const centIsInt = providerConfig?.amountInCents ?? false;
|
|
81
|
+
return candidates.find((candidate) => {
|
|
82
|
+
const converted = normalizeAmount(candidate.amount, request.token.decimals, centIsInt);
|
|
83
|
+
return converted === currencyAmount || converted === currencyAmount * -1n;
|
|
84
|
+
}) ?? null;
|
|
85
|
+
}
|
|
86
|
+
function awaitMetadata(peer, actionType, platform, timeoutMs) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let timeoutId;
|
|
89
|
+
const unsub = peer.onMetadataMessage((data) => {
|
|
90
|
+
if (`transfer_${data.platform}` === actionType) {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
unsub();
|
|
93
|
+
resolve(data);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
timeoutId = setTimeout(() => {
|
|
97
|
+
unsub();
|
|
98
|
+
reject(new Error(`Enclave capture timed out for platform "${platform}"`));
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function createExtensionEnclaveAdapter(config) {
|
|
103
|
+
const timeoutMs = config.timeoutMs ?? 6e4;
|
|
104
|
+
return {
|
|
105
|
+
kind: "extension-enclave",
|
|
106
|
+
async authenticateAndCapture(request, options) {
|
|
107
|
+
const peer = config.getPeer();
|
|
108
|
+
if (!peer) {
|
|
109
|
+
throw new EnclaveCaptureUnavailable(request.platform);
|
|
110
|
+
}
|
|
111
|
+
const metadataPromise = awaitMetadata(peer, request.actionType, request.platform, timeoutMs);
|
|
112
|
+
await peer.authenticate({
|
|
113
|
+
actionType: request.actionType,
|
|
114
|
+
platform: request.platform,
|
|
115
|
+
captureMode: "buyerTee"
|
|
116
|
+
});
|
|
117
|
+
const response = await metadataPromise;
|
|
118
|
+
if (!response.buyerTeeCapture) {
|
|
119
|
+
throw new EnclaveCaptureUnavailable(request.platform);
|
|
120
|
+
}
|
|
121
|
+
const matched = autoMatch(response.metadata, request);
|
|
122
|
+
let matchedIndex;
|
|
123
|
+
if (matched) {
|
|
124
|
+
matchedIndex = matched.originalIndex;
|
|
125
|
+
} else if (options?.selectTransaction) {
|
|
126
|
+
matchedIndex = await options.selectTransaction(response.metadata);
|
|
127
|
+
} else {
|
|
128
|
+
throw new ManualSelectionRequired(response.metadata);
|
|
129
|
+
}
|
|
130
|
+
const material = {
|
|
131
|
+
encryptedSessionMaterial: response.buyerTeeCapture.encryptedSessionMaterial,
|
|
132
|
+
matchedIndex
|
|
133
|
+
};
|
|
134
|
+
return { kind: "extension-enclave", material };
|
|
135
|
+
},
|
|
136
|
+
async attest(handle, request) {
|
|
137
|
+
if (handle.kind !== "extension-enclave") {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`ExtensionEnclaveAdapter.attest received a handle of kind "${handle.kind}"; expected "extension-enclave".`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const material = handle.material;
|
|
143
|
+
const attestation = await requestBuyerTeeAttestation({
|
|
144
|
+
attestationBaseUrl: config.attestationBaseUrl,
|
|
145
|
+
platform: request.platform,
|
|
146
|
+
actionType: request.actionType,
|
|
147
|
+
encryptedSessionMaterial: material.encryptedSessionMaterial,
|
|
148
|
+
params: { index: material.matchedIndex },
|
|
149
|
+
verifyingContract: request.verifyingContract,
|
|
150
|
+
intent: request.intent
|
|
151
|
+
});
|
|
152
|
+
return { attestation, verificationData: attestation.signer };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
exports.asBuyerTeeProof = asBuyerTeeProof;
|
|
158
|
+
exports.createExtensionEnclaveAdapter = createExtensionEnclaveAdapter;
|
|
159
|
+
//# sourceMappingURL=extensionEnclave.cjs.map
|
|
160
|
+
//# sourceMappingURL=extensionEnclave.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/buyerTeeAttestation.ts","../../src/adapters/extensionEnclave.ts"],"names":["parseUnits","convertTokenInputToCurrency","providerConfigs"],"mappings":"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","file":"extensionEnclave.cjs","sourcesContent":["/**\n * @fileoverview Seam contract + state types for the proving orchestrator.\n *\n * `CaptureAdapter` is the swappable capture backend (extension+enclave today,\n * TLSNotary tomorrow). `ProvingState` is the single discriminated-union\n * machine that subsumes the live proof-substep machine (ProofPanel) and the\n * attestation lifecycle (submitStepDerivations). Monetary values stay bigint\n * in base units; the only stringified amount is `ProvingIntent.amount`, which\n * mirrors the existing on-the-wire BuyerTeeIntent shape (buyerTeeAttestation).\n */\n\nimport type { Hex } from 'viem'\nimport type { V3AttestationResponseObject } from '@provex/utils'\nimport type { TokenInfo } from '@provex/utils/tokens'\n\n/** Transaction candidate the adapter surfaces for manual selection. Mirrors\n * the `MetadataMessage` fields the live TransactionSelector consumes; kept\n * structural so the package needs no @provex/extension-react import. */\nexport interface CaptureCandidate {\n amount: number | string\n currency?: string\n date?: string | number | Date\n originalIndex: number\n}\n\n/** Identity the attestor binds the attestation to. Mirrors the live\n * attestationIntent built in SubmitStep.tsx:333-342 / BuyerTeeIntent. */\nexport interface ProvingIntent {\n intentHash: Hex\n /** bigint base units, stringified (matches BuyerTeeIntent.amount). */\n amount: string\n timestampMs: string\n paymentMethod: Hex\n fiatCurrency: Hex\n conversionRate: string\n /** payeeDetailsHash */\n payeeDetails: Hex\n timestampBufferMs?: string\n}\n\n/** What the orchestrator hands a capture adapter. */\nexport interface CaptureRequest {\n /** Parent provider key, e.g. 'zelle', 'revolut'. */\n provider: string\n /** Extension platform slug, e.g. 'chase'. */\n platform: string\n /** Action type, e.g. 'transfer_zelle'. */\n actionType: string\n intent: ProvingIntent\n /** UnifiedPaymentVerifier the attestation is re-signed for. */\n verifyingContract: Hex\n chainId: number\n /**\n * Settlement token for this intent (e.g. USDC on the target chain).\n * Used by the adapter to compute the fiat currency amount via\n * convertTokenInputToCurrency so auto-match works correctly for non-1:1 rates.\n */\n token: TokenInfo\n}\n\n/** Opaque per-backend capture state; the orchestrator passes it back unmodified. */\nexport interface CaptureHandle {\n readonly kind: string\n readonly material: unknown\n}\n\n/** Uniform capture output. `verificationData` names the signing witness to\n * the on-chain verifier (enclave signer for buyerTee — SubmitStep.tsx:473-475). */\nexport interface VerifiedAttestation {\n attestation: V3AttestationResponseObject\n verificationData: Hex\n}\n\n/** Resolver the UI provides so the adapter can collect a manual transaction\n * pick when auto-match fails (GenerateProofStep.tsx:391-404). */\nexport type SelectTransaction = (candidates: CaptureCandidate[]) => Promise<number>\n\n/** A pluggable capture backend. One per capture mechanism. */\nexport interface CaptureAdapter {\n /** Stable id for telemetry, e.g. 'extension-enclave'. */\n readonly kind: string\n /** Phase 1: authenticate + capture, returning opaque material to attest. */\n authenticateAndCapture(\n request: CaptureRequest,\n options?: { selectTransaction?: SelectTransaction },\n ): Promise<CaptureHandle>\n /** Phase 2: produce a verified attestation from the captured material. */\n attest(handle: CaptureHandle, request: CaptureRequest): Promise<VerifiedAttestation>\n}\n\n/** Inputs the orchestrator hook needs to drive the machine. */\nexport interface ProvingRequest {\n request: CaptureRequest\n /** Optional manual-select resolver forwarded to the adapter. */\n selectTransaction?: SelectTransaction\n}\n\n/**\n * The proving state machine (spec lines 141-151). `attest` auto-runs on\n * entry; `readyToFulfill` waits for the user's explicit Complete Order\n * action; `error.isSubmissionError` routes retry to Retry Submission\n * (post-fulfilment) vs Retry Verification (pre-fulfilment) — exactly the\n * live SubmitStep.tsx:931-955 routing.\n */\nexport type ProvingState =\n | { status: 'authenticate' }\n | { status: 'capture' }\n | { status: 'attest' }\n | { status: 'readyToFulfill'; attestation: VerifiedAttestation }\n | { status: 'fulfillIntent'; attestation: VerifiedAttestation }\n | { status: 'confirm'; attestation: VerifiedAttestation }\n | { status: 'sync'; attestation: VerifiedAttestation }\n | { status: 'complete'; attestation: VerifiedAttestation }\n | {\n status: 'error'\n message: string\n /** true ⇒ failure occurred during/after on-chain submission. */\n isSubmissionError: boolean\n attestation?: VerifiedAttestation\n }\n\n/**\n * Thrown by a capture adapter when the provider returned no buyerTee capture\n * (a non-migrated / Reclaim-only provider). The caller (UnifiedBuy) catches\n * this to fall back to its retained Reclaim branch — the package itself does\n * not implement Reclaim (decision (a), spec line 256).\n */\nexport class EnclaveCaptureUnavailable extends Error {\n readonly platform: string\n constructor(platform: string) {\n super(`No enclave (buyerTee) capture available for platform \"${platform}\".`)\n this.name = 'EnclaveCaptureUnavailable'\n this.platform = platform\n }\n}\n\n/**\n * Thrown by a capture adapter when auto-match failed and no `selectTransaction`\n * resolver was provided (e.g. a headless wallet). Carries the candidates so a\n * UI caller can render the manual selector and retry (spec lines 263-267).\n */\nexport class ManualSelectionRequired extends Error {\n readonly candidates: CaptureCandidate[]\n constructor(candidates: CaptureCandidate[]) {\n super('Manual transaction selection is required but no resolver was provided.')\n this.name = 'ManualSelectionRequired'\n this.candidates = candidates\n }\n}\n","/**\n * @fileoverview Buyer trusted-execution attestation client.\n *\n * In PeerAuth extension 0.5.2+, capture produces a buyerTee proof —\n * `{ proofType: 'buyerTee', encryptedSessionMaterial, params }` — where the\n * bank session is already encrypted toward the enclave (cookies never reach\n * our page in plaintext). Instead of re-signing a Reclaim proof through our\n * gate, we hand that material to zkp2p's trusted-execution attestation\n * service, which verifies the payment inside an Amazon Web Services Nitro\n * enclave and signs the attestation directly.\n *\n * The response `responseObject` is the same `V3AttestationResponseObject`\n * shape our gate returns, so it flows through the existing\n * `encodeV3PaymentProof` path unchanged.\n *\n * Moved verbatim from `@provex/ui` (packages/ui/src/lib/buyerTeeAttestation.ts)\n * with the base URL changed from a `@provex/ui` config import to an injected\n * `attestationBaseUrl` argument so this package carries no `@provex/ui` dep.\n */\n\nimport type { Hex } from 'viem'\nimport type { V3AttestationResponseObject } from '@provex/utils'\n\n/**\n * Runtime shape of a buyerTee proof emitted by the PeerAuth extension.\n * Narrow with {@link asBuyerTeeProof} before use.\n */\nexport interface BuyerTeeProof {\n proofType: 'buyerTee'\n /** Session material already encrypted toward the enclave (opaque to us). */\n encryptedSessionMaterial: string\n /** Platform-specific params, e.g. Revolut `{ index }`, Chase `{ index }`. */\n params: unknown\n actionType?: string\n actionPlatform?: string\n}\n\n/**\n * Narrow an extension proof to a buyerTee proof, or null when it is the\n * legacy Reclaim shape (or malformed).\n */\nexport function asBuyerTeeProof(proof: unknown): BuyerTeeProof | null {\n if (!proof || typeof proof !== 'object') return null\n const candidate = proof as Record<string, unknown>\n if (candidate.proofType !== 'buyerTee') return null\n if (typeof candidate.encryptedSessionMaterial !== 'string') return null\n return candidate as unknown as BuyerTeeProof\n}\n\n/** Intent details the enclave binds the attestation to. */\nexport interface BuyerTeeIntent {\n intentHash: Hex\n amount: string\n timestampMs: string\n paymentMethod: Hex\n fiatCurrency: Hex\n conversionRate: string\n payeeDetails: Hex\n timestampBufferMs?: string\n}\n\nexport interface RequestBuyerTeeAttestationArgs {\n /** Gate/attestation origin (was `gateForwarderBaseUrl` in @provex/ui). */\n attestationBaseUrl: string\n /** Extension platform / bank key, e.g. `revolut`, `chase`. */\n platform: string\n /** Action type, e.g. `transfer_revolut`, `transfer_zelle`. */\n actionType: string\n /** Encrypted session material from the buyerTee capture. */\n encryptedSessionMaterial: string\n /** Platform params from the capture (e.g. `{ index }`). */\n params: unknown\n /** PulseChain UnifiedPaymentVerifier the gate re-signs the attestation for. */\n verifyingContract: string\n intent: BuyerTeeIntent\n}\n\n/**\n * Verify a buyer payment via the gate. The gate proxies the encrypted capture\n * to zkp2p's enclave, then re-signs the resulting attestation for PulseChain\n * with our witness key — the enclave itself refuses to sign for 369.\n *\n * Throws with the gate's error text on a non-2xx response.\n */\nexport async function requestBuyerTeeAttestation(\n args: RequestBuyerTeeAttestationArgs,\n): Promise<V3AttestationResponseObject> {\n const url = `${args.attestationBaseUrl}/api/v0/attestation/buyer-verify/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.actionType)}`\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n encryptedSessionMaterial: args.encryptedSessionMaterial,\n params: args.params,\n verifyingContract: args.verifyingContract,\n intent: args.intent,\n }),\n })\n\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `Trusted-execution attestation failed (${response.status}): ${detail || response.statusText}`,\n )\n }\n\n const json = (await response.json()) as\n | { responseObject?: V3AttestationResponseObject }\n | V3AttestationResponseObject\n const responseObject =\n (json as { responseObject?: V3AttestationResponseObject }).responseObject ??\n (json as V3AttestationResponseObject)\n\n if (!responseObject?.typedDataValue || !responseObject?.signature) {\n throw new Error('Trusted-execution attestation response missing typedDataValue/signature')\n }\n return responseObject\n}\n","/**\n * @fileoverview Extension + enclave CaptureAdapter (the v1 capture backend).\n *\n * Mirrors the live AuthenticateStep + GenerateProofStep enclave path\n * (AuthenticateStep.tsx:224-281, GenerateProofStep.tsx:237-251) reduced to\n * the buyerTee branch. Talks to an injected `window.peer`-shaped client so\n * @provex/proving stays framework-agnostic and needs no @provex/extension-react\n * import (spec dependency-direction, lines 103-110).\n */\n\nimport { parseUnits } from 'viem'\nimport type { Hex } from 'viem'\nimport { providerConfigs } from '@provex/utils/payment'\nimport type { ChainId } from '@provex/utils/chain'\nimport { convertTokenInputToCurrency } from '@provex/utils/conversionRates'\nimport {\n EnclaveCaptureUnavailable,\n ManualSelectionRequired,\n type CaptureAdapter,\n type CaptureCandidate,\n type CaptureHandle,\n type CaptureRequest,\n type SelectTransaction,\n type VerifiedAttestation,\n} from '../types'\nimport { asBuyerTeeProof, requestBuyerTeeAttestation } from '../buyerTeeAttestation'\n\n/** Minimal shape of the metadata response the extension posts back. */\ninterface MetadataResponse {\n platform: string\n metadata: CaptureCandidate[]\n buyerTeeCapture?: { encryptedSessionMaterial: string }\n}\n\n/** Minimal slice of `window.peer` this adapter depends on. */\nexport interface PeerClient {\n authenticate(inputs: { actionType: string; platform: string; captureMode: 'buyerTee' }): Promise<void>\n onMetadataMessage(fn: (data: MetadataResponse) => void): () => void\n}\n\nexport interface ExtensionEnclaveAdapterConfig {\n /** Resolve the live peer client (e.g. `() => window.peer`). */\n getPeer: () => PeerClient | undefined\n /** Gate/attestation origin handed to requestBuyerTeeAttestation. */\n attestationBaseUrl: string\n /**\n * Milliseconds before awaitMetadata rejects if the extension never posts a\n * matching metadata message. Mirrors the 60-second timeout in\n * AuthenticateStep.tsx:232-236. Defaults to 60000.\n */\n timeoutMs?: number\n}\n\ninterface EnclaveMaterial {\n encryptedSessionMaterial: string\n matchedIndex: number\n}\n\n/**\n * Normalize an amount from the extension into a bigint in token base units.\n *\n * Reproduces findMetadataMessage's normalizeAmount (useZKTLS.ts:630-639).\n * The extension sends amounts in different formats per provider:\n * - Venmo: `\"- $1.00\"` (formatted dollar string — isFormattedDollars=true)\n * - Others: `100` (integer cents) or `1.00` (decimal dollars)\n *\n * When `centIsInt` is true and the raw value has no currency symbol or decimal\n * point (i.e. it arrived as integer cents), we divide by 100 to convert to\n * the same base unit scale that parseUnits applied.\n */\nfunction normalizeAmount(amount: number | string, decimals: number, centIsInt: boolean): bigint {\n const raw = String(amount)\n const isFormattedDollars = /[^0-9\\-]/.test(raw) && raw.includes('.')\n const cleaned = raw.replace(/[^0-9.]/g, '')\n let value = parseUnits(cleaned, decimals)\n if (centIsInt && !isFormattedDollars) {\n value = value / 100n\n }\n return value\n}\n\n/**\n * Find the candidate whose amount equals the intent's FIAT currency amount,\n * matching positive or negative sign as findMetadataMessage does.\n *\n * Uses the same pipeline as the live AuthenticateStep.tsx:147-163:\n * 1. Convert token amount → fiat via convertTokenInputToCurrency.\n * 2. Normalize each candidate with the provider's centIsInt flag.\n * 3. Match equality (or negated equality for debit/refund signs).\n *\n * `centIsInt` is sourced from `providerConfigs(chainId)[provider]?.amountInCents`\n * which mirrors how the live caller derives it:\n * `targetProvider?.amountInCents ?? false`\n * where `targetProvider` is resolved via `useProvider` from the same\n * providerConfigs call. Sub-providers (e.g. zelle-chase) do NOT currently\n * set amountInCents, so the parent-provider lookup is sufficient.\n */\nfunction autoMatch(\n candidates: CaptureCandidate[],\n request: CaptureRequest,\n): CaptureCandidate | null {\n const currencyAmount = convertTokenInputToCurrency({\n token: request.token,\n amountOutInt: BigInt(request.intent.amount),\n rate: BigInt(request.intent.conversionRate),\n })\n\n // Resolve centIsInt from the provider config — same source as the live UI.\n // The cast is safe: CaptureRequest.chainId must be a supported chain ID.\n const configs = providerConfigs(request.chainId as ChainId)\n const providerConfig = configs[request.provider as keyof typeof configs]\n const centIsInt = (providerConfig as { amountInCents?: boolean } | undefined)?.amountInCents ?? false\n\n return (\n candidates.find((candidate) => {\n const converted = normalizeAmount(candidate.amount, request.token.decimals, centIsInt)\n return converted === currencyAmount || converted === currencyAmount * -1n\n }) ?? null\n )\n}\n\n/**\n * Wait for the metadata message whose platform matches the action type,\n * mirroring the live listener resolve guard (AuthenticateStep.tsx:238-244).\n *\n * Races a configurable timeout (default 60s) against the metadata event,\n * mirroring AuthenticateStep.tsx:232-236. The unsubscribe function is always\n * called on resolution, timeout, or error — no listener leaks.\n */\nfunction awaitMetadata(\n peer: PeerClient,\n actionType: string,\n platform: string,\n timeoutMs: number,\n): Promise<MetadataResponse> {\n return new Promise<MetadataResponse>((resolve, reject) => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n\n const unsub = peer.onMetadataMessage((data) => {\n if (`transfer_${data.platform}` === actionType) {\n clearTimeout(timeoutId)\n unsub()\n resolve(data)\n }\n })\n\n timeoutId = setTimeout(() => {\n unsub()\n reject(new Error(`Enclave capture timed out for platform \"${platform}\"`))\n }, timeoutMs)\n })\n}\n\nexport function createExtensionEnclaveAdapter(config: ExtensionEnclaveAdapterConfig): CaptureAdapter {\n const timeoutMs = config.timeoutMs ?? 60000\n\n return {\n kind: 'extension-enclave',\n\n async authenticateAndCapture(\n request: CaptureRequest,\n options?: { selectTransaction?: SelectTransaction },\n ): Promise<CaptureHandle> {\n const peer = config.getPeer()\n if (!peer) {\n throw new EnclaveCaptureUnavailable(request.platform)\n }\n\n // Register the listener BEFORE authenticate fires to avoid a race where\n // the extension posts metadata before the listener is attached.\n const metadataPromise = awaitMetadata(peer, request.actionType, request.platform, timeoutMs)\n await peer.authenticate({\n actionType: request.actionType,\n platform: request.platform,\n captureMode: 'buyerTee',\n })\n const response = await metadataPromise\n\n // No buyerTee capture ⇒ a non-migrated / Reclaim-only provider. The\n // caller falls back to UnifiedBuy's retained Reclaim branch.\n if (!response.buyerTeeCapture) {\n throw new EnclaveCaptureUnavailable(request.platform)\n }\n\n const matched = autoMatch(response.metadata, request)\n let matchedIndex: number\n if (matched) {\n matchedIndex = matched.originalIndex\n } else if (options?.selectTransaction) {\n matchedIndex = await options.selectTransaction(response.metadata)\n } else {\n throw new ManualSelectionRequired(response.metadata)\n }\n\n const material: EnclaveMaterial = {\n encryptedSessionMaterial: response.buyerTeeCapture.encryptedSessionMaterial,\n matchedIndex,\n }\n return { kind: 'extension-enclave', material }\n },\n\n async attest(handle: CaptureHandle, request: CaptureRequest): Promise<VerifiedAttestation> {\n if (handle.kind !== 'extension-enclave') {\n throw new Error(\n `ExtensionEnclaveAdapter.attest received a handle of kind \"${handle.kind}\"; expected \"extension-enclave\".`,\n )\n }\n const material = handle.material as EnclaveMaterial\n const attestation = await requestBuyerTeeAttestation({\n attestationBaseUrl: config.attestationBaseUrl,\n platform: request.platform,\n actionType: request.actionType,\n encryptedSessionMaterial: material.encryptedSessionMaterial,\n params: { index: material.matchedIndex },\n verifyingContract: request.verifyingContract,\n intent: request.intent,\n })\n // verificationData = enclave signer (SubmitStep.tsx:473-474).\n return { attestation, verificationData: attestation.signer as Hex }\n },\n }\n}\n\n// Re-narrow export so consumers can validate proof shapes if they bypass the\n// adapter (kept for parity with the live SubmitStep check, SubmitStep.tsx:206).\nexport { asBuyerTeeProof }\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"extensionEnclave.js"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { extractNullifier } from '@provex/utils';
|
|
3
|
+
import { sleep } from '@provex/utils/time';
|
|
4
|
+
|
|
5
|
+
// src/useProving.ts
|
|
6
|
+
async function withMinimumVisible(floorMs, started) {
|
|
7
|
+
const elapsed = Date.now() - started;
|
|
8
|
+
if (elapsed < floorMs) await sleep(floorMs - elapsed);
|
|
9
|
+
}
|
|
10
|
+
function isWalletRejection(error) {
|
|
11
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "WALLET_REJECTED";
|
|
12
|
+
}
|
|
13
|
+
function useProving(options) {
|
|
14
|
+
const {
|
|
15
|
+
client,
|
|
16
|
+
captureAdapter,
|
|
17
|
+
request,
|
|
18
|
+
paymentMethodId,
|
|
19
|
+
selectTransaction,
|
|
20
|
+
onAwaitingSignature,
|
|
21
|
+
onTransactionHash,
|
|
22
|
+
onComplete
|
|
23
|
+
} = options;
|
|
24
|
+
const [state, setState] = useState({ status: "authenticate" });
|
|
25
|
+
const inFlightRef = useRef(false);
|
|
26
|
+
const submitInFlightRef = useRef(false);
|
|
27
|
+
const runCaptureAndAttest = useCallback(async () => {
|
|
28
|
+
if (inFlightRef.current) return;
|
|
29
|
+
inFlightRef.current = true;
|
|
30
|
+
try {
|
|
31
|
+
setState({ status: "capture" });
|
|
32
|
+
const handle = await captureAdapter.authenticateAndCapture(request, { selectTransaction });
|
|
33
|
+
const attestStarted = Date.now();
|
|
34
|
+
setState({ status: "attest" });
|
|
35
|
+
const verified = await captureAdapter.attest(handle, request);
|
|
36
|
+
await withMinimumVisible(1e3, attestStarted);
|
|
37
|
+
const nullifier = extractNullifier(verified.attestation);
|
|
38
|
+
if (nullifier) {
|
|
39
|
+
const isUsed = await client.checkNullifierUsed({ paymentMethodId, nullifier });
|
|
40
|
+
if (isUsed === true) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"This payment has already been verified. The nullifier has already been used onchain."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
setState({ status: "readyToFulfill", attestation: verified });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
setState({
|
|
49
|
+
status: "error",
|
|
50
|
+
message: error instanceof Error ? error.message : "Verification failed",
|
|
51
|
+
isSubmissionError: false
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
inFlightRef.current = false;
|
|
55
|
+
}
|
|
56
|
+
}, [captureAdapter, client, request, paymentMethodId, selectTransaction]);
|
|
57
|
+
const submit = useCallback(
|
|
58
|
+
async (verified) => {
|
|
59
|
+
if (submitInFlightRef.current) return;
|
|
60
|
+
submitInFlightRef.current = true;
|
|
61
|
+
const submitStarted = Date.now();
|
|
62
|
+
setState({ status: "fulfillIntent", attestation: verified });
|
|
63
|
+
try {
|
|
64
|
+
await client.fulfillIntent({
|
|
65
|
+
intentHash: request.intent.intentHash,
|
|
66
|
+
attestation: verified.attestation,
|
|
67
|
+
verificationData: verified.verificationData,
|
|
68
|
+
// Wrap consumer callbacks to match the real buildWritableMethod
|
|
69
|
+
// call shapes (ProveXClient.ts ~890, ~903).
|
|
70
|
+
onAwaitingSignature: onAwaitingSignature ? (_info) => {
|
|
71
|
+
onAwaitingSignature();
|
|
72
|
+
} : void 0,
|
|
73
|
+
// buildWritableMethod fires onTransactionHash with { ...ctx, hash }
|
|
74
|
+
// (ProveXClient.ts ~903). Extract hash and forward to the consumer's
|
|
75
|
+
// simpler (hash: Hex) => void API.
|
|
76
|
+
onTransactionHash: onTransactionHash ? (info) => {
|
|
77
|
+
setState({ status: "confirm", attestation: verified });
|
|
78
|
+
onTransactionHash(info.hash);
|
|
79
|
+
} : (_info) => {
|
|
80
|
+
setState({ status: "confirm", attestation: verified });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
await withMinimumVisible(500, submitStarted);
|
|
84
|
+
setState({ status: "sync", attestation: verified });
|
|
85
|
+
await sleep(600);
|
|
86
|
+
setState({ status: "complete", attestation: verified });
|
|
87
|
+
onComplete?.(verified.attestation);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (isWalletRejection(error)) {
|
|
90
|
+
setState({ status: "readyToFulfill", attestation: verified });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setState({
|
|
94
|
+
status: "error",
|
|
95
|
+
message: error instanceof Error ? error.message : "Submission failed",
|
|
96
|
+
isSubmissionError: true,
|
|
97
|
+
attestation: verified
|
|
98
|
+
});
|
|
99
|
+
} finally {
|
|
100
|
+
submitInFlightRef.current = false;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[client, request.intent.intentHash, onAwaitingSignature, onTransactionHash, onComplete]
|
|
104
|
+
);
|
|
105
|
+
const authenticate = useCallback(() => runCaptureAndAttest(), [runCaptureAndAttest]);
|
|
106
|
+
const fulfill = useCallback(async () => {
|
|
107
|
+
if (state.status !== "readyToFulfill") return;
|
|
108
|
+
await submit(state.attestation);
|
|
109
|
+
}, [state, submit]);
|
|
110
|
+
const retryVerification = useCallback(async () => {
|
|
111
|
+
setState({ status: "authenticate" });
|
|
112
|
+
await runCaptureAndAttest();
|
|
113
|
+
}, [runCaptureAndAttest]);
|
|
114
|
+
const retrySubmission = useCallback(async () => {
|
|
115
|
+
if (state.status !== "error" || !state.attestation) return;
|
|
116
|
+
await submit(state.attestation);
|
|
117
|
+
}, [state, submit]);
|
|
118
|
+
return { state, authenticate, fulfill, retryVerification, retrySubmission };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { useProving };
|
|
122
|
+
//# sourceMappingURL=chunk-LFDS3XGK.js.map
|
|
123
|
+
//# sourceMappingURL=chunk-LFDS3XGK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useProving.ts"],"names":[],"mappings":"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","file":"chunk-LFDS3XGK.js","sourcesContent":["/**\n * @fileoverview useProving — headless orchestrator for the enclave proving\n * pipeline. Replicates the live AuthenticateStep/GenerateProofStep/SubmitStep\n * state machine (ProofPanel) with no presentation. Renders nothing; returns\n * state + actions for a UI (UnifiedBuy ProofPanel or a wallet) to bind to.\n *\n * Owns: the ProvingState machine, the nullifier double-spend check\n * (SubmitStep.tsx:419-426), the attest→fulfil sequencing, retry routing\n * (SubmitStep.tsx:931-955), and the minimum-visible-state timings\n * (SubmitStep.tsx:404-417). Delegates capture to the injected CaptureAdapter,\n * the on-chain write (+ receipt + indexer sync) to ProveXClient.fulfillIntent.\n */\n\nimport { useCallback, useRef, useState } from 'react'\nimport type { Hex } from 'viem'\nimport { extractNullifier, type V3AttestationResponseObject } from '@provex/utils'\nimport { sleep } from '@provex/utils/time'\nimport type {\n CaptureAdapter,\n CaptureRequest,\n SelectTransaction,\n ProvingState,\n VerifiedAttestation,\n} from './types'\n\n/**\n * Minimal context object that buildWritableMethod passes to the per-call hooks.\n * Kept structural so @provex/proving does not depend on @valve-tech/wallet-adapter.\n * Only `hash` is consumed by this hook — the rest is opaque context.\n */\ninterface TxCallbackContext {\n chainId: number\n request: unknown\n}\n\n/** Structural params for the fulfillIntent call. Mirrors FulfillIntentParams +\n * WriteHookParams from @provex/react but declared here to keep @provex/proving\n * from depending on the React package's built dist (which the test environment\n * doesn't require). The real ProveXClient.fulfillIntent satisfies this shape.\n *\n * IMPORTANT: onAwaitingSignature and onTransactionHash match the exact call\n * shapes in buildWritableMethod (ProveXClient.ts ~890, ~903):\n * params.onAwaitingSignature?.(ctx) → (info: TxCallbackContext) => void\n * params.onTransactionHash?.({ ...ctx, hash }) → (info: TxCallbackContext & { hash: Hex }) => void\n */\ninterface FulfillParams {\n intentHash: Hex\n attestation: V3AttestationResponseObject\n verificationData: Hex\n postIntentHookData?: Hex\n onAwaitingSignature?: (info: TxCallbackContext) => void\n onTransactionHash?: (info: TxCallbackContext & { hash: Hex }) => void\n}\n\n/** The structural slice of ProveXClient the orchestrator depends on. Keeping\n * it structural lets tests pass a light fake and avoids importing the class. */\nexport interface ProvingClient {\n fulfillIntent: (params: FulfillParams) => Promise<{ hash: Hex; receipt: unknown }>\n checkNullifierUsed: (params: { paymentMethodId: Hex; nullifier: Hex }) => Promise<boolean | null>\n}\n\nexport interface UseProvingOptions {\n client: ProvingClient\n captureAdapter: CaptureAdapter\n request: CaptureRequest\n /** Payment method id for the nullifier registry lookup. */\n paymentMethodId: Hex\n /** Manual-select resolver forwarded to the adapter. */\n selectTransaction?: SelectTransaction\n /**\n * Forwarded to ProveXClient.fulfillIntent for the consumer's tx-flight strip.\n * Receives the raw TxContext — the hook does not inspect the argument.\n */\n onAwaitingSignature?: () => void\n /** Receives the raw tx hash extracted from the TxContext+hash payload. */\n onTransactionHash?: (hash: Hex) => void\n /** Called once when the flow reaches `complete`. */\n onComplete?: (attestation: V3AttestationResponseObject) => void\n}\n\nexport interface UseProvingReturn {\n state: ProvingState\n /** Phase 1+2: authenticate, capture, attest, nullifier-check → readyToFulfill. */\n authenticate: () => Promise<void>\n /** Phase 3: submit fulfillIntent → confirm → sync → complete. */\n fulfill: () => Promise<void>\n /** Re-run attestation after a pre-fulfilment error. */\n retryVerification: () => Promise<void>\n /** Re-run submission after a post-fulfilment error. */\n retrySubmission: () => Promise<void>\n}\n\n/** Minimum visible-state pacing from SubmitStep.tsx:404-417 — keep a state\n * on screen at least `floorMs` so the user perceives the step. */\nasync function withMinimumVisible(floorMs: number, started: number): Promise<void> {\n const elapsed = Date.now() - started\n if (elapsed < floorMs) await sleep(floorMs - elapsed)\n}\n\n/**\n * Detect a wallet-rejection error structurally (without importing ProveXError).\n * ProveXClient always re-wraps user rejections as ProveXError with\n * code === 'WALLET_REJECTED' before the error escapes fulfillIntent.\n */\nfunction isWalletRejection(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n (error as { code: unknown }).code === 'WALLET_REJECTED'\n )\n}\n\nexport function useProving(options: UseProvingOptions): UseProvingReturn {\n const {\n client,\n captureAdapter,\n request,\n paymentMethodId,\n selectTransaction,\n onAwaitingSignature,\n onTransactionHash,\n onComplete,\n } = options\n\n const [state, setState] = useState<ProvingState>({ status: 'authenticate' })\n // Re-entry guard mirroring isRequestingAttestationRef (SubmitStep.tsx:199).\n const inFlightRef = useRef(false)\n // Separate in-flight guard for submit — prevents double on-chain spend from\n // two rapid fulfill() calls before a re-render.\n const submitInFlightRef = useRef(false)\n\n const runCaptureAndAttest = useCallback(async (): Promise<void> => {\n if (inFlightRef.current) return\n inFlightRef.current = true\n try {\n setState({ status: 'capture' })\n const handle = await captureAdapter.authenticateAndCapture(request, { selectTransaction })\n\n const attestStarted = Date.now()\n setState({ status: 'attest' })\n const verified = await captureAdapter.attest(handle, request)\n // SubmitStep.tsx:414-417: verifying state must be visible for ≥1000ms.\n await withMinimumVisible(1000, attestStarted)\n\n // Nullifier double-spend check (SubmitStep.tsx:419-426).\n const nullifier = extractNullifier(verified.attestation)\n if (nullifier) {\n const isUsed = await client.checkNullifierUsed({ paymentMethodId, nullifier })\n if (isUsed === true) {\n throw new Error(\n 'This payment has already been verified. The nullifier has already been used onchain.',\n )\n }\n }\n\n setState({ status: 'readyToFulfill', attestation: verified })\n } catch (error) {\n setState({\n status: 'error',\n message: error instanceof Error ? error.message : 'Verification failed',\n isSubmissionError: false,\n })\n } finally {\n inFlightRef.current = false\n }\n }, [captureAdapter, client, request, paymentMethodId, selectTransaction])\n\n const submit = useCallback(\n async (verified: VerifiedAttestation): Promise<void> => {\n // Re-entry guard: prevent two rapid submit calls from both calling\n // fulfillIntent (double on-chain spend). Mirrors inFlightRef pattern.\n if (submitInFlightRef.current) return\n submitInFlightRef.current = true\n\n const submitStarted = Date.now()\n setState({ status: 'fulfillIntent', attestation: verified })\n try {\n await client.fulfillIntent({\n intentHash: request.intent.intentHash,\n attestation: verified.attestation,\n verificationData: verified.verificationData,\n // Wrap consumer callbacks to match the real buildWritableMethod\n // call shapes (ProveXClient.ts ~890, ~903).\n onAwaitingSignature: onAwaitingSignature\n ? (_info: TxCallbackContext) => { onAwaitingSignature() }\n : undefined,\n // buildWritableMethod fires onTransactionHash with { ...ctx, hash }\n // (ProveXClient.ts ~903). Extract hash and forward to the consumer's\n // simpler (hash: Hex) => void API.\n onTransactionHash: onTransactionHash\n ? (info: TxCallbackContext & { hash: Hex }) => {\n // After the tx is broadcast (a separate async tick before the\n // receipt arrives), surface the confirm state so it is\n // observable by consumers.\n setState({ status: 'confirm', attestation: verified })\n onTransactionHash(info.hash)\n }\n : ((_info: TxCallbackContext & { hash: Hex }) => {\n setState({ status: 'confirm', attestation: verified })\n }),\n })\n await withMinimumVisible(500, submitStarted)\n\n // fulfillIntent already awaited receipt + indexer sync internally\n // (ProveXClient.ts:906-915). Surface sync as a real visible state\n // (confirm was set in onTransactionHash above); then await a\n // minimum-visible window before complete so sync is not batched away.\n setState({ status: 'sync', attestation: verified })\n await sleep(600)\n setState({ status: 'complete', attestation: verified })\n onComplete?.(verified.attestation)\n } catch (error) {\n // Wallet rejection: return to readyToFulfill so the user can retry\n // without losing the attestation (mirrors SubmitStep.tsx:571-573).\n if (isWalletRejection(error)) {\n setState({ status: 'readyToFulfill', attestation: verified })\n return\n }\n setState({\n status: 'error',\n message: error instanceof Error ? error.message : 'Submission failed',\n isSubmissionError: true,\n attestation: verified,\n })\n } finally {\n submitInFlightRef.current = false\n }\n },\n [client, request.intent.intentHash, onAwaitingSignature, onTransactionHash, onComplete],\n )\n\n const authenticate = useCallback(() => runCaptureAndAttest(), [runCaptureAndAttest])\n\n const fulfill = useCallback(async () => {\n if (state.status !== 'readyToFulfill') return\n await submit(state.attestation)\n }, [state, submit])\n\n const retryVerification = useCallback(async () => {\n setState({ status: 'authenticate' })\n await runCaptureAndAttest()\n }, [runCaptureAndAttest])\n\n const retrySubmission = useCallback(async () => {\n if (state.status !== 'error' || !state.attestation) return\n await submit(state.attestation)\n }, [state, submit])\n\n return { state, authenticate, fulfill, retryVerification, retrySubmission }\n}\n"]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { parseUnits } from 'viem';
|
|
2
|
+
import { providerConfigs } from '@provex/utils/payment';
|
|
3
|
+
import { convertTokenInputToCurrency } from '@provex/utils/conversionRates';
|
|
4
|
+
|
|
5
|
+
// src/adapters/extensionEnclave.ts
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var EnclaveCaptureUnavailable = class extends Error {
|
|
9
|
+
platform;
|
|
10
|
+
constructor(platform) {
|
|
11
|
+
super(`No enclave (buyerTee) capture available for platform "${platform}".`);
|
|
12
|
+
this.name = "EnclaveCaptureUnavailable";
|
|
13
|
+
this.platform = platform;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ManualSelectionRequired = class extends Error {
|
|
17
|
+
candidates;
|
|
18
|
+
constructor(candidates) {
|
|
19
|
+
super("Manual transaction selection is required but no resolver was provided.");
|
|
20
|
+
this.name = "ManualSelectionRequired";
|
|
21
|
+
this.candidates = candidates;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/buyerTeeAttestation.ts
|
|
26
|
+
function asBuyerTeeProof(proof) {
|
|
27
|
+
if (!proof || typeof proof !== "object") return null;
|
|
28
|
+
const candidate = proof;
|
|
29
|
+
if (candidate.proofType !== "buyerTee") return null;
|
|
30
|
+
if (typeof candidate.encryptedSessionMaterial !== "string") return null;
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
async function requestBuyerTeeAttestation(args) {
|
|
34
|
+
const url = `${args.attestationBaseUrl}/api/v0/attestation/buyer-verify/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.actionType)}`;
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "content-type": "application/json" },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
encryptedSessionMaterial: args.encryptedSessionMaterial,
|
|
40
|
+
params: args.params,
|
|
41
|
+
verifyingContract: args.verifyingContract,
|
|
42
|
+
intent: args.intent
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const detail = await response.text().catch(() => "");
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Trusted-execution attestation failed (${response.status}): ${detail || response.statusText}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const json = await response.json();
|
|
52
|
+
const responseObject = json.responseObject ?? json;
|
|
53
|
+
if (!responseObject?.typedDataValue || !responseObject?.signature) {
|
|
54
|
+
throw new Error("Trusted-execution attestation response missing typedDataValue/signature");
|
|
55
|
+
}
|
|
56
|
+
return responseObject;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/adapters/extensionEnclave.ts
|
|
60
|
+
function normalizeAmount(amount, decimals, centIsInt) {
|
|
61
|
+
const raw = String(amount);
|
|
62
|
+
const isFormattedDollars = /[^0-9\-]/.test(raw) && raw.includes(".");
|
|
63
|
+
const cleaned = raw.replace(/[^0-9.]/g, "");
|
|
64
|
+
let value = parseUnits(cleaned, decimals);
|
|
65
|
+
if (centIsInt && !isFormattedDollars) {
|
|
66
|
+
value = value / 100n;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function autoMatch(candidates, request) {
|
|
71
|
+
const currencyAmount = convertTokenInputToCurrency({
|
|
72
|
+
token: request.token,
|
|
73
|
+
amountOutInt: BigInt(request.intent.amount),
|
|
74
|
+
rate: BigInt(request.intent.conversionRate)
|
|
75
|
+
});
|
|
76
|
+
const configs = providerConfigs(request.chainId);
|
|
77
|
+
const providerConfig = configs[request.provider];
|
|
78
|
+
const centIsInt = providerConfig?.amountInCents ?? false;
|
|
79
|
+
return candidates.find((candidate) => {
|
|
80
|
+
const converted = normalizeAmount(candidate.amount, request.token.decimals, centIsInt);
|
|
81
|
+
return converted === currencyAmount || converted === currencyAmount * -1n;
|
|
82
|
+
}) ?? null;
|
|
83
|
+
}
|
|
84
|
+
function awaitMetadata(peer, actionType, platform, timeoutMs) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let timeoutId;
|
|
87
|
+
const unsub = peer.onMetadataMessage((data) => {
|
|
88
|
+
if (`transfer_${data.platform}` === actionType) {
|
|
89
|
+
clearTimeout(timeoutId);
|
|
90
|
+
unsub();
|
|
91
|
+
resolve(data);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
timeoutId = setTimeout(() => {
|
|
95
|
+
unsub();
|
|
96
|
+
reject(new Error(`Enclave capture timed out for platform "${platform}"`));
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function createExtensionEnclaveAdapter(config) {
|
|
101
|
+
const timeoutMs = config.timeoutMs ?? 6e4;
|
|
102
|
+
return {
|
|
103
|
+
kind: "extension-enclave",
|
|
104
|
+
async authenticateAndCapture(request, options) {
|
|
105
|
+
const peer = config.getPeer();
|
|
106
|
+
if (!peer) {
|
|
107
|
+
throw new EnclaveCaptureUnavailable(request.platform);
|
|
108
|
+
}
|
|
109
|
+
const metadataPromise = awaitMetadata(peer, request.actionType, request.platform, timeoutMs);
|
|
110
|
+
await peer.authenticate({
|
|
111
|
+
actionType: request.actionType,
|
|
112
|
+
platform: request.platform,
|
|
113
|
+
captureMode: "buyerTee"
|
|
114
|
+
});
|
|
115
|
+
const response = await metadataPromise;
|
|
116
|
+
if (!response.buyerTeeCapture) {
|
|
117
|
+
throw new EnclaveCaptureUnavailable(request.platform);
|
|
118
|
+
}
|
|
119
|
+
const matched = autoMatch(response.metadata, request);
|
|
120
|
+
let matchedIndex;
|
|
121
|
+
if (matched) {
|
|
122
|
+
matchedIndex = matched.originalIndex;
|
|
123
|
+
} else if (options?.selectTransaction) {
|
|
124
|
+
matchedIndex = await options.selectTransaction(response.metadata);
|
|
125
|
+
} else {
|
|
126
|
+
throw new ManualSelectionRequired(response.metadata);
|
|
127
|
+
}
|
|
128
|
+
const material = {
|
|
129
|
+
encryptedSessionMaterial: response.buyerTeeCapture.encryptedSessionMaterial,
|
|
130
|
+
matchedIndex
|
|
131
|
+
};
|
|
132
|
+
return { kind: "extension-enclave", material };
|
|
133
|
+
},
|
|
134
|
+
async attest(handle, request) {
|
|
135
|
+
if (handle.kind !== "extension-enclave") {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`ExtensionEnclaveAdapter.attest received a handle of kind "${handle.kind}"; expected "extension-enclave".`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const material = handle.material;
|
|
141
|
+
const attestation = await requestBuyerTeeAttestation({
|
|
142
|
+
attestationBaseUrl: config.attestationBaseUrl,
|
|
143
|
+
platform: request.platform,
|
|
144
|
+
actionType: request.actionType,
|
|
145
|
+
encryptedSessionMaterial: material.encryptedSessionMaterial,
|
|
146
|
+
params: { index: material.matchedIndex },
|
|
147
|
+
verifyingContract: request.verifyingContract,
|
|
148
|
+
intent: request.intent
|
|
149
|
+
});
|
|
150
|
+
return { attestation, verificationData: attestation.signer };
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { EnclaveCaptureUnavailable, ManualSelectionRequired, asBuyerTeeProof, createExtensionEnclaveAdapter, requestBuyerTeeAttestation };
|
|
156
|
+
//# sourceMappingURL=chunk-QLPNUQDA.js.map
|
|
157
|
+
//# sourceMappingURL=chunk-QLPNUQDA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/buyerTeeAttestation.ts","../src/adapters/extensionEnclave.ts"],"names":[],"mappings":"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","file":"chunk-QLPNUQDA.js","sourcesContent":["/**\n * @fileoverview Seam contract + state types for the proving orchestrator.\n *\n * `CaptureAdapter` is the swappable capture backend (extension+enclave today,\n * TLSNotary tomorrow). `ProvingState` is the single discriminated-union\n * machine that subsumes the live proof-substep machine (ProofPanel) and the\n * attestation lifecycle (submitStepDerivations). Monetary values stay bigint\n * in base units; the only stringified amount is `ProvingIntent.amount`, which\n * mirrors the existing on-the-wire BuyerTeeIntent shape (buyerTeeAttestation).\n */\n\nimport type { Hex } from 'viem'\nimport type { V3AttestationResponseObject } from '@provex/utils'\nimport type { TokenInfo } from '@provex/utils/tokens'\n\n/** Transaction candidate the adapter surfaces for manual selection. Mirrors\n * the `MetadataMessage` fields the live TransactionSelector consumes; kept\n * structural so the package needs no @provex/extension-react import. */\nexport interface CaptureCandidate {\n amount: number | string\n currency?: string\n date?: string | number | Date\n originalIndex: number\n}\n\n/** Identity the attestor binds the attestation to. Mirrors the live\n * attestationIntent built in SubmitStep.tsx:333-342 / BuyerTeeIntent. */\nexport interface ProvingIntent {\n intentHash: Hex\n /** bigint base units, stringified (matches BuyerTeeIntent.amount). */\n amount: string\n timestampMs: string\n paymentMethod: Hex\n fiatCurrency: Hex\n conversionRate: string\n /** payeeDetailsHash */\n payeeDetails: Hex\n timestampBufferMs?: string\n}\n\n/** What the orchestrator hands a capture adapter. */\nexport interface CaptureRequest {\n /** Parent provider key, e.g. 'zelle', 'revolut'. */\n provider: string\n /** Extension platform slug, e.g. 'chase'. */\n platform: string\n /** Action type, e.g. 'transfer_zelle'. */\n actionType: string\n intent: ProvingIntent\n /** UnifiedPaymentVerifier the attestation is re-signed for. */\n verifyingContract: Hex\n chainId: number\n /**\n * Settlement token for this intent (e.g. USDC on the target chain).\n * Used by the adapter to compute the fiat currency amount via\n * convertTokenInputToCurrency so auto-match works correctly for non-1:1 rates.\n */\n token: TokenInfo\n}\n\n/** Opaque per-backend capture state; the orchestrator passes it back unmodified. */\nexport interface CaptureHandle {\n readonly kind: string\n readonly material: unknown\n}\n\n/** Uniform capture output. `verificationData` names the signing witness to\n * the on-chain verifier (enclave signer for buyerTee — SubmitStep.tsx:473-475). */\nexport interface VerifiedAttestation {\n attestation: V3AttestationResponseObject\n verificationData: Hex\n}\n\n/** Resolver the UI provides so the adapter can collect a manual transaction\n * pick when auto-match fails (GenerateProofStep.tsx:391-404). */\nexport type SelectTransaction = (candidates: CaptureCandidate[]) => Promise<number>\n\n/** A pluggable capture backend. One per capture mechanism. */\nexport interface CaptureAdapter {\n /** Stable id for telemetry, e.g. 'extension-enclave'. */\n readonly kind: string\n /** Phase 1: authenticate + capture, returning opaque material to attest. */\n authenticateAndCapture(\n request: CaptureRequest,\n options?: { selectTransaction?: SelectTransaction },\n ): Promise<CaptureHandle>\n /** Phase 2: produce a verified attestation from the captured material. */\n attest(handle: CaptureHandle, request: CaptureRequest): Promise<VerifiedAttestation>\n}\n\n/** Inputs the orchestrator hook needs to drive the machine. */\nexport interface ProvingRequest {\n request: CaptureRequest\n /** Optional manual-select resolver forwarded to the adapter. */\n selectTransaction?: SelectTransaction\n}\n\n/**\n * The proving state machine (spec lines 141-151). `attest` auto-runs on\n * entry; `readyToFulfill` waits for the user's explicit Complete Order\n * action; `error.isSubmissionError` routes retry to Retry Submission\n * (post-fulfilment) vs Retry Verification (pre-fulfilment) — exactly the\n * live SubmitStep.tsx:931-955 routing.\n */\nexport type ProvingState =\n | { status: 'authenticate' }\n | { status: 'capture' }\n | { status: 'attest' }\n | { status: 'readyToFulfill'; attestation: VerifiedAttestation }\n | { status: 'fulfillIntent'; attestation: VerifiedAttestation }\n | { status: 'confirm'; attestation: VerifiedAttestation }\n | { status: 'sync'; attestation: VerifiedAttestation }\n | { status: 'complete'; attestation: VerifiedAttestation }\n | {\n status: 'error'\n message: string\n /** true ⇒ failure occurred during/after on-chain submission. */\n isSubmissionError: boolean\n attestation?: VerifiedAttestation\n }\n\n/**\n * Thrown by a capture adapter when the provider returned no buyerTee capture\n * (a non-migrated / Reclaim-only provider). The caller (UnifiedBuy) catches\n * this to fall back to its retained Reclaim branch — the package itself does\n * not implement Reclaim (decision (a), spec line 256).\n */\nexport class EnclaveCaptureUnavailable extends Error {\n readonly platform: string\n constructor(platform: string) {\n super(`No enclave (buyerTee) capture available for platform \"${platform}\".`)\n this.name = 'EnclaveCaptureUnavailable'\n this.platform = platform\n }\n}\n\n/**\n * Thrown by a capture adapter when auto-match failed and no `selectTransaction`\n * resolver was provided (e.g. a headless wallet). Carries the candidates so a\n * UI caller can render the manual selector and retry (spec lines 263-267).\n */\nexport class ManualSelectionRequired extends Error {\n readonly candidates: CaptureCandidate[]\n constructor(candidates: CaptureCandidate[]) {\n super('Manual transaction selection is required but no resolver was provided.')\n this.name = 'ManualSelectionRequired'\n this.candidates = candidates\n }\n}\n","/**\n * @fileoverview Buyer trusted-execution attestation client.\n *\n * In PeerAuth extension 0.5.2+, capture produces a buyerTee proof —\n * `{ proofType: 'buyerTee', encryptedSessionMaterial, params }` — where the\n * bank session is already encrypted toward the enclave (cookies never reach\n * our page in plaintext). Instead of re-signing a Reclaim proof through our\n * gate, we hand that material to zkp2p's trusted-execution attestation\n * service, which verifies the payment inside an Amazon Web Services Nitro\n * enclave and signs the attestation directly.\n *\n * The response `responseObject` is the same `V3AttestationResponseObject`\n * shape our gate returns, so it flows through the existing\n * `encodeV3PaymentProof` path unchanged.\n *\n * Moved verbatim from `@provex/ui` (packages/ui/src/lib/buyerTeeAttestation.ts)\n * with the base URL changed from a `@provex/ui` config import to an injected\n * `attestationBaseUrl` argument so this package carries no `@provex/ui` dep.\n */\n\nimport type { Hex } from 'viem'\nimport type { V3AttestationResponseObject } from '@provex/utils'\n\n/**\n * Runtime shape of a buyerTee proof emitted by the PeerAuth extension.\n * Narrow with {@link asBuyerTeeProof} before use.\n */\nexport interface BuyerTeeProof {\n proofType: 'buyerTee'\n /** Session material already encrypted toward the enclave (opaque to us). */\n encryptedSessionMaterial: string\n /** Platform-specific params, e.g. Revolut `{ index }`, Chase `{ index }`. */\n params: unknown\n actionType?: string\n actionPlatform?: string\n}\n\n/**\n * Narrow an extension proof to a buyerTee proof, or null when it is the\n * legacy Reclaim shape (or malformed).\n */\nexport function asBuyerTeeProof(proof: unknown): BuyerTeeProof | null {\n if (!proof || typeof proof !== 'object') return null\n const candidate = proof as Record<string, unknown>\n if (candidate.proofType !== 'buyerTee') return null\n if (typeof candidate.encryptedSessionMaterial !== 'string') return null\n return candidate as unknown as BuyerTeeProof\n}\n\n/** Intent details the enclave binds the attestation to. */\nexport interface BuyerTeeIntent {\n intentHash: Hex\n amount: string\n timestampMs: string\n paymentMethod: Hex\n fiatCurrency: Hex\n conversionRate: string\n payeeDetails: Hex\n timestampBufferMs?: string\n}\n\nexport interface RequestBuyerTeeAttestationArgs {\n /** Gate/attestation origin (was `gateForwarderBaseUrl` in @provex/ui). */\n attestationBaseUrl: string\n /** Extension platform / bank key, e.g. `revolut`, `chase`. */\n platform: string\n /** Action type, e.g. `transfer_revolut`, `transfer_zelle`. */\n actionType: string\n /** Encrypted session material from the buyerTee capture. */\n encryptedSessionMaterial: string\n /** Platform params from the capture (e.g. `{ index }`). */\n params: unknown\n /** PulseChain UnifiedPaymentVerifier the gate re-signs the attestation for. */\n verifyingContract: string\n intent: BuyerTeeIntent\n}\n\n/**\n * Verify a buyer payment via the gate. The gate proxies the encrypted capture\n * to zkp2p's enclave, then re-signs the resulting attestation for PulseChain\n * with our witness key — the enclave itself refuses to sign for 369.\n *\n * Throws with the gate's error text on a non-2xx response.\n */\nexport async function requestBuyerTeeAttestation(\n args: RequestBuyerTeeAttestationArgs,\n): Promise<V3AttestationResponseObject> {\n const url = `${args.attestationBaseUrl}/api/v0/attestation/buyer-verify/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.actionType)}`\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n encryptedSessionMaterial: args.encryptedSessionMaterial,\n params: args.params,\n verifyingContract: args.verifyingContract,\n intent: args.intent,\n }),\n })\n\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `Trusted-execution attestation failed (${response.status}): ${detail || response.statusText}`,\n )\n }\n\n const json = (await response.json()) as\n | { responseObject?: V3AttestationResponseObject }\n | V3AttestationResponseObject\n const responseObject =\n (json as { responseObject?: V3AttestationResponseObject }).responseObject ??\n (json as V3AttestationResponseObject)\n\n if (!responseObject?.typedDataValue || !responseObject?.signature) {\n throw new Error('Trusted-execution attestation response missing typedDataValue/signature')\n }\n return responseObject\n}\n","/**\n * @fileoverview Extension + enclave CaptureAdapter (the v1 capture backend).\n *\n * Mirrors the live AuthenticateStep + GenerateProofStep enclave path\n * (AuthenticateStep.tsx:224-281, GenerateProofStep.tsx:237-251) reduced to\n * the buyerTee branch. Talks to an injected `window.peer`-shaped client so\n * @provex/proving stays framework-agnostic and needs no @provex/extension-react\n * import (spec dependency-direction, lines 103-110).\n */\n\nimport { parseUnits } from 'viem'\nimport type { Hex } from 'viem'\nimport { providerConfigs } from '@provex/utils/payment'\nimport type { ChainId } from '@provex/utils/chain'\nimport { convertTokenInputToCurrency } from '@provex/utils/conversionRates'\nimport {\n EnclaveCaptureUnavailable,\n ManualSelectionRequired,\n type CaptureAdapter,\n type CaptureCandidate,\n type CaptureHandle,\n type CaptureRequest,\n type SelectTransaction,\n type VerifiedAttestation,\n} from '../types'\nimport { asBuyerTeeProof, requestBuyerTeeAttestation } from '../buyerTeeAttestation'\n\n/** Minimal shape of the metadata response the extension posts back. */\ninterface MetadataResponse {\n platform: string\n metadata: CaptureCandidate[]\n buyerTeeCapture?: { encryptedSessionMaterial: string }\n}\n\n/** Minimal slice of `window.peer` this adapter depends on. */\nexport interface PeerClient {\n authenticate(inputs: { actionType: string; platform: string; captureMode: 'buyerTee' }): Promise<void>\n onMetadataMessage(fn: (data: MetadataResponse) => void): () => void\n}\n\nexport interface ExtensionEnclaveAdapterConfig {\n /** Resolve the live peer client (e.g. `() => window.peer`). */\n getPeer: () => PeerClient | undefined\n /** Gate/attestation origin handed to requestBuyerTeeAttestation. */\n attestationBaseUrl: string\n /**\n * Milliseconds before awaitMetadata rejects if the extension never posts a\n * matching metadata message. Mirrors the 60-second timeout in\n * AuthenticateStep.tsx:232-236. Defaults to 60000.\n */\n timeoutMs?: number\n}\n\ninterface EnclaveMaterial {\n encryptedSessionMaterial: string\n matchedIndex: number\n}\n\n/**\n * Normalize an amount from the extension into a bigint in token base units.\n *\n * Reproduces findMetadataMessage's normalizeAmount (useZKTLS.ts:630-639).\n * The extension sends amounts in different formats per provider:\n * - Venmo: `\"- $1.00\"` (formatted dollar string — isFormattedDollars=true)\n * - Others: `100` (integer cents) or `1.00` (decimal dollars)\n *\n * When `centIsInt` is true and the raw value has no currency symbol or decimal\n * point (i.e. it arrived as integer cents), we divide by 100 to convert to\n * the same base unit scale that parseUnits applied.\n */\nfunction normalizeAmount(amount: number | string, decimals: number, centIsInt: boolean): bigint {\n const raw = String(amount)\n const isFormattedDollars = /[^0-9\\-]/.test(raw) && raw.includes('.')\n const cleaned = raw.replace(/[^0-9.]/g, '')\n let value = parseUnits(cleaned, decimals)\n if (centIsInt && !isFormattedDollars) {\n value = value / 100n\n }\n return value\n}\n\n/**\n * Find the candidate whose amount equals the intent's FIAT currency amount,\n * matching positive or negative sign as findMetadataMessage does.\n *\n * Uses the same pipeline as the live AuthenticateStep.tsx:147-163:\n * 1. Convert token amount → fiat via convertTokenInputToCurrency.\n * 2. Normalize each candidate with the provider's centIsInt flag.\n * 3. Match equality (or negated equality for debit/refund signs).\n *\n * `centIsInt` is sourced from `providerConfigs(chainId)[provider]?.amountInCents`\n * which mirrors how the live caller derives it:\n * `targetProvider?.amountInCents ?? false`\n * where `targetProvider` is resolved via `useProvider` from the same\n * providerConfigs call. Sub-providers (e.g. zelle-chase) do NOT currently\n * set amountInCents, so the parent-provider lookup is sufficient.\n */\nfunction autoMatch(\n candidates: CaptureCandidate[],\n request: CaptureRequest,\n): CaptureCandidate | null {\n const currencyAmount = convertTokenInputToCurrency({\n token: request.token,\n amountOutInt: BigInt(request.intent.amount),\n rate: BigInt(request.intent.conversionRate),\n })\n\n // Resolve centIsInt from the provider config — same source as the live UI.\n // The cast is safe: CaptureRequest.chainId must be a supported chain ID.\n const configs = providerConfigs(request.chainId as ChainId)\n const providerConfig = configs[request.provider as keyof typeof configs]\n const centIsInt = (providerConfig as { amountInCents?: boolean } | undefined)?.amountInCents ?? false\n\n return (\n candidates.find((candidate) => {\n const converted = normalizeAmount(candidate.amount, request.token.decimals, centIsInt)\n return converted === currencyAmount || converted === currencyAmount * -1n\n }) ?? null\n )\n}\n\n/**\n * Wait for the metadata message whose platform matches the action type,\n * mirroring the live listener resolve guard (AuthenticateStep.tsx:238-244).\n *\n * Races a configurable timeout (default 60s) against the metadata event,\n * mirroring AuthenticateStep.tsx:232-236. The unsubscribe function is always\n * called on resolution, timeout, or error — no listener leaks.\n */\nfunction awaitMetadata(\n peer: PeerClient,\n actionType: string,\n platform: string,\n timeoutMs: number,\n): Promise<MetadataResponse> {\n return new Promise<MetadataResponse>((resolve, reject) => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n\n const unsub = peer.onMetadataMessage((data) => {\n if (`transfer_${data.platform}` === actionType) {\n clearTimeout(timeoutId)\n unsub()\n resolve(data)\n }\n })\n\n timeoutId = setTimeout(() => {\n unsub()\n reject(new Error(`Enclave capture timed out for platform \"${platform}\"`))\n }, timeoutMs)\n })\n}\n\nexport function createExtensionEnclaveAdapter(config: ExtensionEnclaveAdapterConfig): CaptureAdapter {\n const timeoutMs = config.timeoutMs ?? 60000\n\n return {\n kind: 'extension-enclave',\n\n async authenticateAndCapture(\n request: CaptureRequest,\n options?: { selectTransaction?: SelectTransaction },\n ): Promise<CaptureHandle> {\n const peer = config.getPeer()\n if (!peer) {\n throw new EnclaveCaptureUnavailable(request.platform)\n }\n\n // Register the listener BEFORE authenticate fires to avoid a race where\n // the extension posts metadata before the listener is attached.\n const metadataPromise = awaitMetadata(peer, request.actionType, request.platform, timeoutMs)\n await peer.authenticate({\n actionType: request.actionType,\n platform: request.platform,\n captureMode: 'buyerTee',\n })\n const response = await metadataPromise\n\n // No buyerTee capture ⇒ a non-migrated / Reclaim-only provider. The\n // caller falls back to UnifiedBuy's retained Reclaim branch.\n if (!response.buyerTeeCapture) {\n throw new EnclaveCaptureUnavailable(request.platform)\n }\n\n const matched = autoMatch(response.metadata, request)\n let matchedIndex: number\n if (matched) {\n matchedIndex = matched.originalIndex\n } else if (options?.selectTransaction) {\n matchedIndex = await options.selectTransaction(response.metadata)\n } else {\n throw new ManualSelectionRequired(response.metadata)\n }\n\n const material: EnclaveMaterial = {\n encryptedSessionMaterial: response.buyerTeeCapture.encryptedSessionMaterial,\n matchedIndex,\n }\n return { kind: 'extension-enclave', material }\n },\n\n async attest(handle: CaptureHandle, request: CaptureRequest): Promise<VerifiedAttestation> {\n if (handle.kind !== 'extension-enclave') {\n throw new Error(\n `ExtensionEnclaveAdapter.attest received a handle of kind \"${handle.kind}\"; expected \"extension-enclave\".`,\n )\n }\n const material = handle.material as EnclaveMaterial\n const attestation = await requestBuyerTeeAttestation({\n attestationBaseUrl: config.attestationBaseUrl,\n platform: request.platform,\n actionType: request.actionType,\n encryptedSessionMaterial: material.encryptedSessionMaterial,\n params: { index: material.matchedIndex },\n verifyingContract: request.verifyingContract,\n intent: request.intent,\n })\n // verificationData = enclave signer (SubmitStep.tsx:473-474).\n return { attestation, verificationData: attestation.signer as Hex }\n },\n }\n}\n\n// Re-narrow export so consumers can validate proof shapes if they bypass the\n// adapter (kept for parity with the live SubmitStep check, SubmitStep.tsx:206).\nexport { asBuyerTeeProof }\n"]}
|