import { formatDate } from "@helpers"; // import { DSPContractStorage } from "@helpers/dsp/DSPContractStorage"; import { localized, msg } from "@lit/localize"; import type { TemplateResultOrSymbol } from "@src/component"; import { DSPContractStorage, offerKindActionHandler, offerKindHandler, rdf, TemsObjectHandler, } from "@startinblox/solid-tems-shared"; import ModalStyle from "@styles/modal/tems-modal.scss?inline"; import { css, html, nothing, type TemplateResult, unsafeCSS } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; export type TemsModalProps = rdf.ValidM18Object; @customElement("tems-modal") @localized() export class TemsModal extends TemsObjectHandler { static styles = css` ${unsafeCSS(ModalStyle)} `; @property({ attribute: false, type: Object }) object: TemsModalProps["object"] = { "@id": "" }; @property({ attribute: false }) dspStore?: any; @property({ attribute: false }) participantId?: string; @property({ attribute: false }) participantConnectorUri?: string; @property({ attribute: false, type: Boolean }) displayServiceTest = true; @state() negotiationStatus: | "idle" | "negotiating" | "pending" | "granted" | "failed" | "transferring" = "idle"; @state() negotiationError?: string; @state() contractId?: string; @state() negotiationId?: string; @state() currentState?: string; @state() attempt?: number; @state() maxAttempts?: number; @state() gettingToken = false; @state() testingService = false; @state() testResult?: any; @state() existingAgreementChecked = false; @state() transferId?: string; @state() edrToken?: string; @state() edrEndpoint?: string; @state() showPolicySelection = false; @state() selectedPolicyIndex?: number; @state() availablePolicies?: any[]; @state() transferError?: string; @state() gettingEDR = false; @state() accessingData = false; @state() dataAccessAttempt?: number; @state() dataAccessMaxAttempts?: number; @state() countdown?: number; @state() dataResult?: any; @state() dataAccessError?: string; /** * Check for existing agreement when component connects */ connectedCallback() { super.connectedCallback(); this._checkExistingAgreement(); } /** * Get localStorage key for this asset * Uses combination of provider ID and dataset ID for uniqueness across providers */ private _getStorageKey(): string { const obj = this.object as any; const datasetId = obj.datasetId || obj.assetId; // Include provider ID to differentiate assets with same ID from different providers const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; // DEBUG: Log what provider info we're seeing if (!datasetId) return ""; // Create composite key: provider-assetId const key = providerId ? `dsp-agreement-${providerId}-${datasetId}` : `dsp-agreement-${datasetId}`; return key; } /** * Save agreement info to localStorage */ private _saveAgreementInfo( contractId: string, negotiationId: string, timestamp: number, ) { const key = this._getStorageKey(); if (key) { const obj = this.object as any; const agreementInfo = { contractId, negotiationId, timestamp, assetId: obj.datasetId || obj.assetId, providerId: obj.counterPartyId || obj._providerParticipantId || obj._provider || "", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", }; localStorage.setItem(key, JSON.stringify(agreementInfo)); } } /** * Load agreement info from localStorage */ private _loadAgreementInfo(): { contractId: string; negotiationId: string; timestamp: number; assetId: string; } | null { const key = this._getStorageKey(); if (!key) return null; try { const stored = localStorage.getItem(key); if (stored) { const info = JSON.parse(stored); return info; } } catch (error) { console.error("Failed to load agreement info:", error); } return null; } /** * Save initial contract state when negotiation starts */ private _saveInitialContractState(negotiationId: string) { try { const obj = this.object as any; // Check if contract already exists for this asset from this provider const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId, ); if (!existingContract) { // Debug: log asset object to see available properties // Extract index endpoint URL from asset (dcat:endpointUrl) const indexEndpointUrl = obj["dcat:endpointUrl"] || obj.endpointUrl || obj["endpointUrl"]; const assetName = obj.name || obj.assetId || "Unknown Asset"; // Detect if this is an index asset const isIndexAsset = assetName.toLowerCase().includes("index"); // Create new contract in REQUESTED state DSPContractStorage.create({ assetId: obj.assetId || obj.datasetId, datasetId: obj.datasetId || obj.assetId, assetName, assetDescription: obj.description, providerName: obj._provider || obj.provider?.name || "Unknown Provider", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", providerParticipantId: obj.counterPartyId || obj._providerParticipantId || "", providerColor: obj._providerColor, policy: obj.policy, state: "REQUESTED", contractId: negotiationId, // Index-specific fields isIndexAsset, indexEndpointUrl, }); } } catch (error) { console.error( "[DSP Contract Catalog] Failed to save initial contract state:", error, ); } } /** * Save contract to DSP Contract Catalog for history tracking */ private _saveToContractCatalog(contractId: string, negotiationId: string) { try { const obj = this.object as any; // Debug: log asset object to see available properties for endpoint URL // Check if contract already exists - search by contractId (negotiationId) first, // then by agreementId as fallback. Filter by provider to avoid cross-provider confusion. const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId || c.agreementId === contractId, ); // Extract index endpoint URL from asset (dcat:endpointUrl) // Try multiple possible property names - the mapping config adds 'indexEndpointUrl' const indexEndpointUrl = obj.indexEndpointUrl || obj["dcat:endpointUrl"] || obj["dcat:endpointURL"] || obj.endpointUrl || obj["endpointUrl"] || obj.endpointURL; const assetName = obj.name || obj.assetId || "Unknown Asset"; // Detect if this is an index asset const isIndexAsset = assetName.toLowerCase().includes("index"); if (existingContract) { // Update existing contract with index metadata DSPContractStorage.updateState(existingContract.id, "FINALIZED", { agreementId: contractId, contractId: negotiationId, isIndexAsset, indexEndpointUrl, }); } else { // Create new contract entry DSPContractStorage.create({ assetId: obj.assetId || obj.datasetId, datasetId: obj.datasetId || obj.assetId, assetName, assetDescription: obj.description, providerName: obj._provider || obj.provider?.name || "Unknown Provider", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", providerParticipantId: obj.counterPartyId || obj._providerParticipantId || "", providerColor: obj._providerColor, policy: obj.policy, state: "FINALIZED", contractId: negotiationId, agreementId: contractId, // Index-specific fields isIndexAsset, indexEndpointUrl, }); } } catch (error) { console.error("[DSP Contract Catalog] Failed to save contract:", error); } } /** * Update contract state in catalog (for failures) */ private _updateContractState( negotiationId: string, state: "FAILED" | "TERMINATED", error?: string, ) { try { const obj = this.object as any; const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId, ); if (existingContract) { DSPContractStorage.updateState(existingContract.id, state, { error }); } } catch (error) { console.error( "[DSP Contract Catalog] Failed to update contract state:", error, ); } } /** * Check if there's an existing agreement for this asset */ private async _checkExistingAgreement() { if (this.existingAgreementChecked) return; this.existingAgreementChecked = true; // Try to load from localStorage const storedInfo = this._loadAgreementInfo(); if (storedInfo) { this.contractId = storedInfo.contractId; this.negotiationId = storedInfo.negotiationId; this.negotiationStatus = "granted"; this.requestUpdate(); } // Also check if DSP store has the agreement try { if (this.dspStore && storedInfo?.negotiationId) { // Verify the agreement still exists in the store try { const obj = this.object as any; const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; await this.dspStore.getContractAgreement( storedInfo.negotiationId, providerId, ); } catch (error) { console.warn("Could not verify agreement in store:", error); } } } catch (error) { console.warn("Error checking DSP store for existing agreement:", error); } } /** * Clear existing agreement and allow renegotiation */ private _renewContract() { if ( !confirm( msg( "This will delete the current contract and start a fresh negotiation. Continue?", ), ) ) { return; } const key = this._getStorageKey(); if (key) { localStorage.removeItem(key); } // Also delete from DSPContractStorage - only for this provider's contract const obj = this.object as any; const assetId = obj?.assetId || obj?.datasetId; const providerId = obj?.counterPartyId || obj?._providerParticipantId || ""; if (assetId) { const existingContracts = DSPContractStorage.getByAssetAndProvider( assetId, providerId, ); for (const contract of existingContracts) { DSPContractStorage.delete(contract.id); } } // Reset state to idle this.negotiationStatus = "idle"; this.contractId = undefined; this.negotiationId = undefined; this.negotiationError = undefined; this.gettingToken = false; this.testingService = false; this.testResult = undefined; this.existingAgreementChecked = false; this.requestUpdate(); } _selectPolicy(index: number) { this.selectedPolicyIndex = index; this.showPolicySelection = false; this.requestUpdate(); // Automatically proceed with negotiation after selection this._negotiateAccess(); } _cancelPolicySelection() { this.showPolicySelection = false; this.selectedPolicyIndex = undefined; this.availablePolicies = undefined; this.requestUpdate(); } _formatPolicyDetails(policy: any): string { if (!policy) return "No policy details available"; const parts: string[] = []; // Policy ID if (policy["@id"]) { parts.push( `
curl -X POST '<consumer-connector>/v3/transferprocesses' \\
-H 'Content-Type: application/json' \\
-H 'X-Api-Key: <your-api-key>' \\
-d '{
"@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" },
"@type": "TransferRequest",
"assetId": "${assetId}",
"protocol": "dataspace-protocol-http",
"counterPartyAddress": "${connectorUri}",
"contractId": "${agreementId}",
"transferType": "HttpData-PULL",
"dataDestination": { "type": "HttpProxy" }
}'
{ "@id": "<transfer-id>", ... }
curl -X GET '<consumer-connector>/v3/edrs/<transfer-id>/dataaddress' \\
-H 'X-Api-Key: <your-api-key>'
{ "endpoint": "https://...", "authorization": "eyJ..." }
curl -X GET '<endpoint-from-step-2>' \\
-H 'Authorization: <authorization-token-from-step-2>'
${this.contractId}`
: html`⚠️ ${msg("You need to complete contract negotiation first to get an agreement ID.")}`
}
${JSON.stringify(this.dataResult, null, 2)}
${asset.format ? asset.format.name : nothing}
${asset.name}
` : nothing} ${asset.size ? html`${asset.size}
` : nothing}${msg( "Multiple policies are available for this dataset. Please select one to proceed with the negotiation.", )}
${policy["@id"]}`
: nothing}