Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/grid_client/src/clients/tf-grid/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ExtrinsicResult,
GetDedicatedNodePriceOptions,
NodeContractUsedResources,
OptOutV3BillingOptions,
SetDedicatedNodeExtraFeesOptions,
} from "@threefold/tfchain_client";
import { GridClientError } from "@threefold/types";
Expand Down Expand Up @@ -707,6 +708,12 @@ class TFContracts extends Contracts {
extraFee: feeUSD,
});
}

async optOutV3Billing(options: OptOutV3BillingOptions) {
return await super.optOutV3Billing({
nodeId: options.nodeId,
});
}
}

export { TFContracts };
30 changes: 30 additions & 0 deletions packages/grid_client/src/modules/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ import {
GetActiveContractsModel,
GetDedicatedNodePriceModel,
GetServiceContractModel,
IsNodeOptedOutOfV3BillingModel,
NameContractCreateModel,
NameContractGetModel,
NodeContractCreateModel,
NodeContractUpdateModel,
OptOutV3BillingModel,
RentContractCreateModel,
RentContractGetModel,
ServiceContractApproveModel,
Expand Down Expand Up @@ -215,6 +217,13 @@ class Contracts {
return await this.client.contracts.getDedicatedNodeExtraFee(options);
}

/** Returns whether the node has opted out of v3 billing. */
@expose
@validateInput
async isNodeOptedOutOfV3Billing(options: IsNodeOptedOutOfV3BillingModel): Promise<boolean> {
return await this.client.contracts.isNodeOptedOutOfV3Billing(options);
}

/**
* Retrieves active `contract IDs` based on the provided node ID.
*
Expand Down Expand Up @@ -526,6 +535,27 @@ class Contracts {
async setDedicatedNodeExtraFee(options: SetDedicatedNodeExtraFeesModel) {
return (await this.client.contracts.setDedicatedNodeExtraFee(options)).apply();
}

/**
* Opts out of billing for a v3 node.
*
* This method allows farmers to stop billing flows for a specific v3 node.
* After opting out, only Threefold admins will be able to create contracts on that node.
*
* @param {OptOutV3BillingModel} options - The options object containing the nodeId to opt out of billing.
* @returns {Promise<number>} A promise that resolves to the node ID for opting out of billing.
* @decorators
* - `@expose`: Exposes the method for external use.
* - `@validateInput`: Validates the input options.
* - `@checkBalance`: Checks the balance before proceeding.
*/
@expose
@validateInput
@checkBalance
async optOutV3Billing(options: OptOutV3BillingModel) {
return (await this.client.contracts.optOutV3Billing(options)).apply();
}

/**
* Get contract discount package
* @param {ContractDiscountPackage} options
Expand Down
10 changes: 10 additions & 0 deletions packages/grid_client/src/modules/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,10 +861,18 @@ class SetDedicatedNodeExtraFeesModel {
@Expose() @IsNumber() @IsNotEmpty() @Min(0) extraFee: number;
}

class OptOutV3BillingModel {
@Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number;
}

class GetDedicatedNodePriceModel {
@Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number;
}

class IsNodeOptedOutOfV3BillingModel {
@Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number;
}

class SwapToStellarModel {
@Expose() @IsNotEmpty() @IsString() target: string;
@Expose() @IsNotEmpty() @Min(1) amount: number;
Expand Down Expand Up @@ -1067,7 +1075,9 @@ export {
NetworkGetModel,
NodeGetModel,
SetDedicatedNodeExtraFeesModel,
OptOutV3BillingModel,
GetDedicatedNodePriceModel,
IsNodeOptedOutOfV3BillingModel,
SwapToStellarModel,
ListenToMintCompletedModel,
AddFarmIPModel,
Expand Down
45 changes: 45 additions & 0 deletions packages/playground/src/dashboard/components/node_actions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<span>
<PublicConfig
class="me-2"
:node-id="nodeId"
:farm-id="farmId"
@remove-config="$emit('remove-config', $event)"
@add-config="$emit('add-config', $event)"
/>
<SetExtraFee class="me-2" :node-id="nodeId" :is-opted-out="isOptedOut" />
<OptOutV3Billing class="me-2" :node-id="nodeId" :is-opted-out="isOptedOut" @opted-out="markOptedOut" />
</span>
</template>

<script lang="ts">
import { ref, watch } from "vue";
import { useGrid } from "../../stores";
import OptOutV3Billing from "./opt_out_v3_billing.vue";
import PublicConfig from "./public_config.vue";
import SetExtraFee from "./set_extra_fee.vue";

export default {
name: "NodeActions",
components: { OptOutV3Billing, PublicConfig, SetExtraFee },
props: { nodeId: { type: Number, required: true }, farmId: { type: Number, required: true } },
emits: ["add-config", "remove-config"],
setup(props) {
const gridStore = useGrid();
const isOptedOut = ref(false);
async function fetchOptedOut() {
if (!gridStore.grid) return;
try {
isOptedOut.value = await gridStore.grid.contracts.isNodeOptedOutOfV3Billing({ nodeId: props.nodeId });
} catch {
isOptedOut.value = false;
}
}
function markOptedOut() {
isOptedOut.value = true;
}
watch(() => props.nodeId, fetchOptedOut, { immediate: true });
return { isOptedOut, markOptedOut };
},
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<span>
<v-tooltip :text="tooltipText">
<template #activator="{ props: tooltipProps }">
<span class="d-inline-block mx-1" v-bind="tooltipProps">
<v-icon
size="large"
:disabled="loading || isOptedOut"
:loading="loading"
@click.stop="!isOptedOut && (showDialog = true)"
>
mdi-cash-off
</v-icon>
</span>
</template>
</v-tooltip>

<v-dialog v-model="showDialog" max-width="600" attach="#modals">
<v-card>
<v-card-title class="bg-primary">Opt Out of V3 Billing</v-card-title>
<v-card-text>
<v-alert type="warning" class="mb-4"> <strong>Warning:</strong> This action cannot be undone. </v-alert>
<p>
By opting out of billing for node {{ nodeId }}, you are giving up control of the node to Threefold admins.
Billing flows will be stopped, and only Threefold admins will be able to create contracts on this node.
</p>
</v-card-text>
<v-card-actions class="justify-end my-1 mr-2">
<v-btn color="anchor" :disabled="loading" @click="showDialog = false">Cancel</v-btn>
<v-btn color="error" :loading="loading" :disabled="loading" @click="handleOptOut">Opt Out</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</span>
</template>

<script lang="ts">
import { computed, ref } from "vue";

import { useGrid } from "../../stores";
import { createCustomToast, ToastType } from "../../utils/custom_toast";

export default {
name: "OptOutV3Billing",
props: {
nodeId: { type: Number, required: true },
isOptedOut: { type: Boolean, default: false },
},
emits: ["opted-out"],
setup(props, { emit }) {
const showDialog = ref(false);
const loading = ref(false);
const gridStore = useGrid();
const tooltipText = computed(() =>
props.isOptedOut ? "Opt Out of V3 Billing (already opted out)" : "Opt Out of V3 Billing",
);

async function handleOptOut() {
try {
loading.value = true;
await gridStore.grid.contracts.optOutV3Billing({ nodeId: props.nodeId });
createCustomToast("Successfully opted out of billing for this node.", ToastType.success);
showDialog.value = false;
emit("opted-out", props.nodeId);
} catch (error) {
console.error("Opt out billing error:", error);
createCustomToast("Failed to opt out of billing.", ToastType.danger);
} finally {
loading.value = false;
}
}

return { showDialog, loading, tooltipText, handleOptOut };
},
};
</script>
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<span>
<v-tooltip location="bottom" text="Add a public config">
<template #activator="{ props }">
<template #activator="{ props: tooltipProps }">
<v-icon
class="mx-1"
v-bind="props"
v-bind="tooltipProps"
size="large"
:disabled="isAdding"
:loading="isAdding"
@click="showDialogue = true"
@click.stop="showDialogue = true"
>
mdi-earth
</v-icon>
Expand Down
29 changes: 17 additions & 12 deletions packages/playground/src/dashboard/components/set_extra_fee.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<template>
<span>
<v-tooltip text="Set Additional Fees">
<template #activator="{ props }">
<v-icon
class="mx-1"
v-bind="props"
size="large"
:disabled="isAdding"
:loading="isAdding"
@click="setupDialog()"
>
mdi-currency-usd
</v-icon>
<v-tooltip :text="tooltipText">
<template #activator="{ props: tooltipProps }">
<span class="d-inline-block mx-1" v-bind="tooltipProps">
<v-icon
size="large"
:disabled="isAdding || isOptedOut"
:loading="isAdding"
@click.stop="!isOptedOut && setupDialog()"
>
mdi-currency-usd
</v-icon>
</span>
</template>
</v-tooltip>

Expand Down Expand Up @@ -83,12 +83,16 @@ export default {
type: Number,
required: true,
},
isOptedOut: { type: Boolean, default: false },
},
setup(props) {
const showDialogue = ref(false);
const isAdding = ref(false);
const gridStore = useGrid();
const valid = ref(false);
const tooltipText = computed(() =>
props.isOptedOut ? "Set Additional Fees (unavailable - node opted out of v3 billing)" : "Set Additional Fees",
);
const isSetting = ref(false);
const inputFee = ref(0);
const isDisabled = computed(() => {
Expand Down Expand Up @@ -143,6 +147,7 @@ export default {
inputFee,
isSetting,
isDisabled,
tooltipText,
setExtraFee,
setupDialog,
loading,
Expand Down
24 changes: 7 additions & 17 deletions packages/playground/src/dashboard/components/user_nodes.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<template v-if="nodes">
<div class="my-6">
<v-card color="primary rounded-0">
<v-card-title class="py-1 text-subtitle-1 text-center">
Your Nodes
</v-card-title>
<v-card-title class="py-1 text-subtitle-1 text-center"> Your Nodes </v-card-title>
</v-card>
<v-data-table-server
v-model:page="page"
Expand Down Expand Up @@ -46,9 +44,7 @@

<v-card class="mt-4">
<v-alert class="pa-5" style="height: 20px">
<h4 class="text-center font-weight-medium">
Resource Units Reserved
</h4>
<h4 class="text-center font-weight-medium">Resource Units Reserved</h4>
</v-alert>
<v-card-text class="pb-8">
<NodeResources :node="item" />
Expand All @@ -57,9 +53,7 @@

<v-card v-if="network == 'main'" class="mt-4" focusable single model-value>
<v-alert class="pa-5" style="height: 20px">
<h4 class="text-center font-weight-medium">
Node Statistics
</h4>
<h4 class="text-center font-weight-medium">Node Statistics</h4>
</v-alert>
<v-card-item>
<NodeMintingDetails :node="item" />
Expand All @@ -76,14 +70,12 @@
</template>

<template #[`item.actions`]="{ item }">
<PublicConfig
class="me-2"
<NodeActions
:node-id="item.nodeId"
:farm-id="item.farmId"
@remove-config="config => toggleConfig(item, config)"
@add-config="config => toggleConfig(item, config)"
/>
<SetExtraFee class="me-2" :node-id="item.nodeId" />
</template>

<template #[`item.country`]="{ item }">
Expand Down Expand Up @@ -112,16 +104,14 @@ import { calculateUptime, getNodeAvailability, getNodeMintingFixupReceipts, type

import NodeResources from "../../components/node_resources.vue";
import NodeMintingDetails from "./NodeMintingDetails.vue";
import PublicConfig from "./public_config.vue";
import SetExtraFee from "./set_extra_fee.vue";
import NodeActions from "./node_actions.vue";

export default {
name: "UserNodes",
components: {
NodeMintingDetails,
PublicConfig,
SetExtraFee,
CardDetails,
NodeActions,
NodeMintingDetails,
NodeResources,
},
setup() {
Expand Down
Loading