From 4ac6d573f1c83798816786d84288e6f06d005137 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Tue, 5 May 2026 07:23:08 -0600 Subject: [PATCH] refactor(tangle-cloud): collapse approval form to unified securityCommitments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified `approveService(ApprovalParams)` entrypoint takes a single `securityCommitments[]` array; the operator's slider value for the default-TNT case was being silently dropped because the form sent `stakingPercent` / `tntExposureBps` fields that the hook ignored. - Drop `stakingPercent` and `tntExposureBps` from `ApprovalConfirmationFormFields`. - Render every requirement (custom or default-TNT) through one `commitments.${key}` controller; the operator's slider always reaches the contract via `securityCommitments[]`. - Type-safe `TeeBackend` const for `useServiceApproveTx` (Phala, AwsNitro, GcpConfidential, AzureSkr only — Unset and DirectTdx are rejected on-chain and thus unrepresentable in the type). Net -45 LOC. typecheck + lint + format + vitest pass. --- .../services/useServiceApproveTx.spec.tsx | 4 +- .../src/data/services/useServiceApproveTx.ts | 18 +- .../Instances/PendingInstanceTable.tsx | 12 +- .../ServiceRequestDetailModal.tsx | 274 ++++++++---------- apps/tangle-cloud/src/types/index.ts | 21 +- 5 files changed, 142 insertions(+), 187 deletions(-) diff --git a/apps/tangle-cloud/src/data/services/useServiceApproveTx.spec.tsx b/apps/tangle-cloud/src/data/services/useServiceApproveTx.spec.tsx index e7ba11ea7..915391e96 100644 --- a/apps/tangle-cloud/src/data/services/useServiceApproveTx.spec.tsx +++ b/apps/tangle-cloud/src/data/services/useServiceApproveTx.spec.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useServiceApproveTx } from './useServiceApproveTx'; +import { TeeBackend, useServiceApproveTx } from './useServiceApproveTx'; const { mockUseChainId, mockGetContractsByChainId, mockUseContractWrite } = vi.hoisted(() => ({ @@ -120,7 +120,7 @@ describe('useServiceApproveTx', () => { const blsPubkey = [1n, 2n, 3n, 4n] as const; const blsPopSignature = [5n, 6n] as const; const tee = { - backend: 2, + backend: TeeBackend.AwsNitro, expectedMeasurement: '0x1111111111111111111111111111111111111111111111111111111111111111' as const, nonceBinding: diff --git a/apps/tangle-cloud/src/data/services/useServiceApproveTx.ts b/apps/tangle-cloud/src/data/services/useServiceApproveTx.ts index cd4a8eadb..c7adb70af 100644 --- a/apps/tangle-cloud/src/data/services/useServiceApproveTx.ts +++ b/apps/tangle-cloud/src/data/services/useServiceApproveTx.ts @@ -18,10 +18,24 @@ import type { ContractSecurityCommitment } from '../../types'; export { TxStatus }; +/** + * Vendor-mediated TEE backends accepted by `approveService`. The on-chain enum + * also defines `Unset = 0` (rejected as a misconfiguration sentinel) and + * `DirectTdx = 5` (rejected — vendor-mediated backends only). We omit both + * here so the type system makes invalid backends unrepresentable. + */ +export const TeeBackend = { + Phala: 1, + AwsNitro: 2, + GcpConfidential: 3, + AzureSkr: 4, +} as const; + +export type TeeBackendValue = (typeof TeeBackend)[keyof typeof TeeBackend]; + /** A TEE attestation commitment matching `Types.TeeAttestationCommitment`. */ export interface TeeAttestationCommitment { - /** Backend enum index — Unset=0, Phala=1, AwsNitro=2, GcpConfidential=3, AzureSkr=4, DirectTdx=5 (rejected on-chain). */ - backend: number; + backend: TeeBackendValue; expectedMeasurement: `0x${string}`; /** Must equal `teeNonceFor(requestId)` exposed by the Tangle contract. */ nonceBinding: `0x${string}`; diff --git a/apps/tangle-cloud/src/pages/instances/Instances/PendingInstanceTable.tsx b/apps/tangle-cloud/src/pages/instances/Instances/PendingInstanceTable.tsx index 17a44ff63..f52638cfd 100644 --- a/apps/tangle-cloud/src/pages/instances/Instances/PendingInstanceTable.tsx +++ b/apps/tangle-cloud/src/pages/instances/Instances/PendingInstanceTable.tsx @@ -397,19 +397,9 @@ export const PendingInstanceTable: FC = ({ const onConfirmApprove = useCallback( async (data: ApprovalConfirmationFormFields) => { if (!selectedRequest || !approveServiceRequest) return; - - // The unified `approveService` entrypoint derives the staking percent - // on-chain from `securityCommitments[0].exposureBps` (or defaults to 100% - // when no commitments are supplied). The form's `stakingPercent` / - // `tntExposureBps` inputs are no longer wired to a separate calldata - // field — operators that want to pin a non-default exposure must do so - // through `securityCommitments`. await approveServiceRequest({ requestId: selectedRequest.requestId, - securityCommitments: - data.securityCommitments && data.securityCommitments.length > 0 - ? data.securityCommitments - : undefined, + securityCommitments: data.securityCommitments, }); }, [selectedRequest, approveServiceRequest], diff --git a/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx b/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx index 6dc4a1ed6..5954d48cd 100644 --- a/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx +++ b/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx @@ -46,7 +46,6 @@ type Props = { type FormValues = { requestId: bigint; commitments: Record; - tntExposureBps: number; }; const addCommasToNumber = (value: number) => value.toLocaleString(); @@ -112,18 +111,24 @@ const ServiceRequestDetailModal: FC = ({ const { data: stakeByAsset, isLoading: isLoadingStake } = useOperatorStakeByAsset(operatorAddress, assetsToQuery); + // Seed the commitment record from every requirement on the request — including + // the default-TNT one when present. The contract's unified `approveService` + // entrypoint accepts an explicit per-asset commitment array; sending the + // operator's actual slider value (rather than dropping it and relying on + // on-chain auto-fill at the minimum) is what lets operators commit above + // `minExposureBps` from this UI. const initialCommitments = useMemo(() => { - if (!hasCustomRequirements || !requirements || requirements.length === 0) { - return {}; - } - const commitments: Record = {}; - for (const req of requirements) { - const key = toAssetMapKey(req.asset.token); - commitments[key] = req.minExposureBps; + if (requirements && requirements.length > 0) { + for (const req of requirements) { + commitments[toAssetMapKey(req.asset.token)] = req.minExposureBps; + } + } else if (defaultTntRequirement) { + commitments[toAssetMapKey(defaultTntRequirement.asset.token)] = + defaultTntRequirement.minExposureBps; } return commitments; - }, [hasCustomRequirements, requirements]); + }, [requirements, defaultTntRequirement]); const derivedSimpleStakingPercent = useMemo(() => { if (!contractDetails || !operatorAddress) { @@ -177,7 +182,6 @@ const ServiceRequestDetailModal: FC = ({ defaultValues: { requestId: selectedRequest?.requestId ?? BigInt(0), commitments: {}, - tntExposureBps: 0, }, }); @@ -187,15 +191,6 @@ const ServiceRequestDetailModal: FC = ({ } }, [initialCommitments, setValue]); - // Initialize tntExposureBps from default TNT requirement - useEffect(() => { - if (defaultTntRequirement && !hasCustomRequirements) { - setValue('tntExposureBps', defaultTntRequirement.minExposureBps, { - shouldValidate: true, - }); - } - }, [defaultTntRequirement, hasCustomRequirements, setValue]); - // Close modal on successful transaction useEffect(() => { if ( @@ -216,52 +211,35 @@ const ServiceRequestDetailModal: FC = ({ const buildSecurityCommitments = useCallback( (commitments: Record): ContractSecurityCommitment[] => { - if (!requirements || requirements.length === 0) { - return []; - } - - return requirements.map((req) => { - const key = toAssetMapKey(req.asset.token); - const exposureBps = commitments[key] ?? req.minExposureBps; - - return { - asset: { - kind: req.asset.kind, - token: req.asset.token, - }, - exposureBps, - }; - }); + // Map every active requirement (custom set OR the default-TNT) into a + // per-asset commitment using the operator's slider value, falling back + // to the requirement's `minExposureBps` only if the slider is unset. + const sourceRequirements = + requirements && requirements.length > 0 + ? requirements + : defaultTntRequirement + ? [defaultTntRequirement] + : []; + + return sourceRequirements.map((req) => ({ + asset: { kind: req.asset.kind, token: req.asset.token }, + exposureBps: + commitments[toAssetMapKey(req.asset.token)] ?? req.minExposureBps, + })); }, - [requirements], + [requirements, defaultTntRequirement], ); const handleFormSubmit = useCallback( (data: FormValues) => { - const formattedData: ApprovalConfirmationFormFields = { + const securityCommitments = buildSecurityCommitments(data.commitments); + return onApprove({ requestId: Number(data.requestId), - }; - - if (hasCustomRequirements) { - formattedData.securityCommitments = buildSecurityCommitments( - data.commitments, - ); - } else { - formattedData.stakingPercent = derivedSimpleStakingPercent; - if (defaultTntRequirement && data.tntExposureBps > 0) { - formattedData.tntExposureBps = data.tntExposureBps; - } - } - - return onApprove(formattedData); + securityCommitments: + securityCommitments.length > 0 ? securityCommitments : undefined, + }); }, - [ - buildSecurityCommitments, - defaultTntRequirement, - derivedSimpleStakingPercent, - hasCustomRequirements, - onApprove, - ], + [buildSecurityCommitments, onApprove], ); const handleApproveClick = useCallback(() => { @@ -338,114 +316,88 @@ const ServiceRequestDetailModal: FC = ({ {!isLoadingRequirements && !isLoadingStake && - hasCustomRequirements && - requirements !== undefined && ( -
- - Set your exposure percentage within the allowed bounds for - each asset. - - - {requirements.map((req) => { - const key = toAssetMapKey(req.asset.token); - - return ( - ( - - )} - /> - ); - })} -
- )} - - {!isLoadingRequirements && - !isLoadingStake && - !hasCustomRequirements && ( -
- {defaultTntRequirement ? ( - <> - - Standard approval — set your TNT security commitment. - - - ( - - )} - /> - - ) : ( + (() => { + // Unified rendering: every requirement on the request — custom or + // the default-TNT one — becomes a single per-asset slider bound to + // `commitments.${tokenKey}`. The contract's unified + // `approveService(ApprovalParams)` accepts the resulting array + // directly, so the operator's slider value is no longer dropped + // for the default-TNT case. + const renderable = + requirements && requirements.length > 0 + ? requirements + : defaultTntRequirement + ? [defaultTntRequirement] + : []; + + if (renderable.length === 0) { + return ( - No custom commitments required. + No security commitments required for this request. - )} -
- )} + ); + } + + return ( +
+ + Set your exposure percentage within the allowed bounds for + each asset. + + + {renderable.map((req) => { + const key = toAssetMapKey(req.asset.token); + const isDefaultTnt = + !hasCustomRequirements && + defaultTntRequirement !== null && + toAssetMapKey(defaultTntRequirement.asset.token) === key; + + return ( + ( + + )} + /> + ); + })} +
+ ); + })()} diff --git a/apps/tangle-cloud/src/types/index.ts b/apps/tangle-cloud/src/types/index.ts index 43b5d5a9f..0f014e65b 100644 --- a/apps/tangle-cloud/src/types/index.ts +++ b/apps/tangle-cloud/src/types/index.ts @@ -41,7 +41,7 @@ export const TangleDAppPagePath = { } as const; /** - * Asset structure matching the contract's Asset struct. + * Asset structure matching the contract's `Types.Asset`. * - kind: 0 = Native token, 1 = ERC20 token * - token: The token address (zero address for native) */ @@ -51,8 +51,9 @@ export type ContractAsset = { }; /** - * Security commitment structure matching the contract's AssetSecurityCommitment struct. - * Used when calling approveServiceWithCommitments. + * Security commitment matching the contract's `Types.AssetSecurityCommitment`. + * Threaded into `approveService(ApprovalParams)` as the operator's per-asset + * exposure decision against the request's `AssetSecurityRequirement`s. */ export type ContractSecurityCommitment = { asset: ContractAsset; @@ -61,17 +62,15 @@ export type ContractSecurityCommitment = { /** * Form fields for the approval confirmation modal. - * Supports two approval modes: - * 1. Simple approval: Only stakingPercent is provided - * 2. Approval with commitments: securityCommitments array is provided + * + * Single approval mode under the unified `approveService(ApprovalParams)` + * entrypoint: the operator either supplies explicit `securityCommitments` or + * omits them. When omitted, the contract auto-fills at the requirement's + * `minExposureBps` for the default-TNT-only case and reverts otherwise. */ export type ApprovalConfirmationFormFields = { requestId: number; - /** Simple approval mode: single percentage (0-100) for default TNT requirement */ - stakingPercent?: number; - /** TNT exposure in basis points (0-10000), when set calls 3-arg approveService overload */ - tntExposureBps?: number; - /** Commitments mode: per-asset exposure commitments matching contract format */ + /** Per-asset exposure commitments. Omitted when the operator accepts on-chain auto-fill. */ securityCommitments?: ContractSecurityCommitment[]; };