Trustless Work
English
English
  • Welcome
    • Why Escrows Matter
    • Who Should Use This
    • Links
  • ⚒️Core Concepts
    • Smart Escrow Design
      • What does a Smart Escrow "look like"?
    • Roles in Trustless Work
    • Escrow Lifecycle
      • Initiation Phase
      • Funding Phase
      • Complete phase
      • Approval phase
      • Release phase
      • Dispute Resolution
    • USDC: The Stablecoin Powering Trustless Work
    • Stellar & Soroban: The Backbone of Trustless Work
  • 💻Developer Guide
    • 🚀Developer Quickstart
    • Authentication
      • Request Api Key
    • Schema
    • Architecture
    • Stellar Wallets
      • Freighter Wallet
      • Albedo Wallet
      • xBull Wallet
      • Rabet Wallet
      • Lobstr Wallet
      • Hana Wallet
      • Additional Resources
      • Troubleshooting & FAQs
    • How to Get Testnet Tokens
  • API Reference
    • Introduction
    • Types
      • Basic
      • Entities
      • Payloads
      • Responses
      • Errors
    • Single Release Escrow
      • Deploy
      • Fund Escrow
      • Approve Milestone
      • Change Milestone Status
      • Release Funds
      • Dispute Escrow
      • Resolve Dispute
      • Get Escrow
      • Update Escrow
    • Multi Release Escrow
      • Deploy
      • Fund Escrow
      • Approve Milestone
      • Change Milestone Status
      • Release Milestone Funds
      • Dispute Milestone
      • Resolve Milestone Dispute
      • Update Escrow
      • Get Escrow
    • Helpers
      • Set Trustline
      • Send Transaction
      • Get Multiple Escrow Balance
    • 🚀Smart Escrow API
  • React Library
    • Getting Started
    • useSendTransaction
    • useGetEscrow
    • useGetMultipleEscrowBalances
    • useInitializeEscrow
    • useUpdateEscrow
    • useFundEscrow
    • useChangeMilestoneApprovedFlag
    • useChangeMilestoneStatus
    • useReleaseFunds
    • useStartDispute
    • useResolveDispute
  • Templates & Components
    • 🤖Using the dApp
      • Step 1: Accessing the Trustless Work Dapp and Logging In
      • Step 2: Creating a Profile and Requesting an API Key
      • Step 3: Creating an Escrow
      • Step 4: Funding an Escrow
      • Step 5: Marking a Milestone as Done ✅
      • Step 6: Approving the Milestone
      • Step 7: Releasing the Payment
      • Resolving Disputes
    • Integration Demo Project
      • Getting Started
      • Configuration
      • Context API
      • Wallet Setup
      • Trustlines
      • Services
      • Helpers
      • Error Handling
      • Forms
        • Initialize Escrow
        • Fund Escrow
        • Get Escrow
        • Get Multiple Escrow Balances
        • Resolve Dispute
        • Change Milestone Status
        • Change Milestone Flag
        • Change Dispute Flag
        • Release Funds
        • Update Escrow
      • UI Components
        • Pages
        • Cards
        • Tabs
        • Utils
  • Tools & Utilities
  • 🌍Use Cases by Industry
    • Marketplaces & E-commerce
    • Grants, Bounties, and Hackathons
    • P2P Exchanges and OTC Desks
    • Security Deposits
    • Milestone-based Freelance & Contract Work
    • Crowdfunding & Pre-orders
    • DAO Treasury & Working Group Budgets
    • Education & Online Courses
    • Subscription + Performance-based Retainers
  • 🏴‍☠️Community & Contribution
    • 🌎Spotlight: Kindfi
    • 🛣️Roadmap: The Journey Ahead
    • Contributor's Guide
    • 📂Open-Source Templates & Developer Tools
  • 📢Appendix & Archive
    • Contact and Support
    • Notion for Startups
    • Meru
    • ✒️Background & Theory
      • 📘Core Concepts & Escrow Glossary
      • 🎒Historical Context
        • 📜Escrow History:
          • 🗿Ancient to Colonial Era
          • 🏦Banking Era (19th - 20th Century)
          • 💻The Digital Era and Online Escrow
        • ⛓️Blockchain Era: Smart Escrows
Powered by GitBook
LogoLogo

Links

  • Website
On this page
  • Schema
  • Custom Hook
  • Form

Was this helpful?

Export as PDF
  1. Templates & Components
  2. Integration Demo Project
  3. Forms

Initialize Escrow

The ideal form for this endpoint.

Schema

This validates an escrow form using Zod, including amounts, wallet addresses, trustline, and a list of milestones.

import { isValidWallet } from "@/helpers/valid-data.helper";
import { z } from "zod";

export const formSchema = z.object({
  engagementId: z.string().min(1, {
    message: "Engagement is required.",
  }),
  title: z.string().min(1, {
    message: "Title is required.",
  }),
  description: z.string().min(10, {
    message: "Description must be at least 10 characters long.",
  }),
  amount: z.string().min(1, {
    message: "Amount is required.",
  }),
  platformFee: z.string().min(1, {
    message: "Platform fee is required.",
  }),
  receiverMemo: z.number().min(0, {
    message: "Receiver memo must be a non-negative number.",
  }),
  roles: z.object({
    approver: z
      .string()
      .min(1, {
        message: "Approver is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Approver must be a valid wallet.",
      }),
    serviceProvider: z
      .string()
      .min(1, {
        message: "Service provider is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Service provider must be a valid wallet.",
      }),
    platformAddress: z
      .string()
      .min(1, {
        message: "Platform address is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Platform address must be a valid wallet.",
      }),
    releaseSigner: z
      .string()
      .min(1, {
        message: "Release signer is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Release signer must be a valid wallet.",
      }),
    disputeResolver: z
      .string()
      .min(1, {
        message: "Dispute resolver is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Dispute resolver must be a valid wallet.",
      }),
    receiver: z
      .string()
      .min(1, {
        message: "Receiver address is required.",
      })
      .refine((value) => isValidWallet(value), {
        message: "Receiver address must be a valid wallet.",
      }),
  }),
  trustline: z.object({
    address: z.string().min(1, {
      message: "Trustline address is required.",
    }),
    decimals: z.number().default(10000000),
  }),
  milestones: z
    .array(
      z.object({
        description: z.string().min(1, {
          message: "Milestone description is required.",
        }),
        status: z.string().default("pending"),
        evidence: z.string().default(""),
        approvedFlag: z.boolean().default(false),
      })
    )
    .min(1, { message: "At least one milestone is required." }),
});

Custom Hook

This contains all the form logic, including schema validation, onSubmit function and others states and functionalities.

import { InitializeEscrowPayload } from "@/@types/escrows/escrow-payload.entity";
import { useWalletContext } from "@/providers/wallet.provider";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { formSchema } from "../schemas/initialize-escrow-form.schema";
import { toast } from "sonner";
import { useEscrowContext } from "@/providers/escrow.provider";
import { InitializeEscrowResponse } from "@/@types/escrows/escrow-response.entity";
import { useTabsContext } from "@/providers/tabs.provider";
import { escrowService } from "../services/escrow.service";
import { trustlines } from "../constants/trustline.constant";
import { Trustline } from "@/@types/trustline.entity";
import { z } from "zod";
import { Resolver } from "react-hook-form";
import { steps } from "../constants/initialize-steps.constant";
import { buildEscrowFromResponse } from "../../../../helpers/build-escrow-from-response.helper";

type FormValues = z.infer<typeof formSchema>;

export const useInitializeEscrow = () => {
  const [currentStep, setCurrentStep] = useState(0);
  const [loading, setLoading] = useState(false);
  const [response, setResponse] = useState<InitializeEscrowResponse | null>(
    null
  );
  const { walletAddress } = useWalletContext();
  const { setEscrow } = useEscrowContext();
  const { setActiveTab } = useTabsContext();

  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema) as Resolver<FormValues>,
    defaultValues: {
      engagementId: "",
      title: "",
      description: "",
      amount: "",
      platformFee: "",
      receiverMemo: 0,
      roles: {
        approver: "",
        serviceProvider: "",
        platformAddress: "",
        releaseSigner: "",
        disputeResolver: "",
        receiver: "",
      },
      trustline: {
        address: "",
        decimals: 10000000,
      },
      milestones: [
        {
          description: "",
          status: "pending",
          evidence: "",
          approvedFlag: false,
        },
      ],
    },
    mode: "onChange",
  });

  const trustlinesOptions = trustlines.map((trustline: Trustline) => ({
    value: trustline.address,
    label: trustline.name,
  }));

  const addMilestone = () => {
    const currentMilestones = form.getValues("milestones");
    form.setValue("milestones", [
      ...currentMilestones,
      { description: "", status: "pending", evidence: "", approvedFlag: false },
    ]);
  };

  const removeMilestone = (index: number) => {
    const currentMilestones = form.getValues("milestones");
    if (currentMilestones.length > 1) {
      form.setValue(
        "milestones",
        currentMilestones.filter((_, i) => i !== index)
      );
    }
  };

  const loadTemplate = () => {
    form.setValue("title", "Sample TW Escrow");
    form.setValue(
      "description",
      "This is a sample TW escrow for testing purposes"
    );
    form.setValue("engagementId", "ENG12345");
    form.setValue("amount", "50");
    form.setValue("platformFee", "5");
    form.setValue("roles.approver", walletAddress || "");
    form.setValue("roles.serviceProvider", walletAddress || "");
    form.setValue("roles.platformAddress", walletAddress || "");
    form.setValue("roles.releaseSigner", walletAddress || "");
    form.setValue("roles.disputeResolver", walletAddress || "");
    form.setValue("roles.receiver", walletAddress || "");
    form.setValue("receiverMemo", 90909090);
    form.setValue(
      "trustline.address",
      trustlines.find((t) => t.name === "USDC")?.address || ""
    );
    form.setValue("milestones", [
      {
        description: "Initial milestone",
        status: "pending",
        evidence: "",
        approvedFlag: false,
      },
      {
        description: "Second milestone",
        status: "pending",
        evidence: "",
        approvedFlag: false,
      },
      {
        description: "Final milestone",
        status: "pending",
        evidence: "",
        approvedFlag: false,
      },
    ]);
  };

  const onSubmit = async (payload: InitializeEscrowPayload) => {
    setLoading(true);
    setResponse(null);

    try {
      // This is the final payload that will be sent to the API
      const finalPayload: InitializeEscrowPayload = {
        ...payload,
        receiverMemo: payload.receiverMemo ?? 0,
        signer: walletAddress || "",
      };

      /**
       * API call by using the escrow service
       * @Note:
       * - We need to specify the endpoint and the method
       * - We need to specify that the returnEscrowDataIsRequired is false
       * - The result will be an InitializeEscrowResponse
       */
      const result = (await escrowService.execute({
        payload: finalPayload,
        endpoint: "/deployer/invoke-deployer-contract",
        method: "post",
      })) as InitializeEscrowResponse;

      /**
       * @Responses:
       * result.status === "SUCCESS"
       * - Escrow created successfully
       * - Set the escrow in the context
       * - Set the active tab to "escrow"
       * - Show a success toast
       *
       * result.status !== "SUCCESS"
       * - Show an error toast
       */
      if (result.status === "SUCCESS") {
        const escrow = buildEscrowFromResponse(result, walletAddress || "");
        setEscrow(escrow);
        setActiveTab("escrow");
        toast.info("Escrow Created");
      }
    } catch (err) {
      toast.error(
        err instanceof Error ? err.message : "An unknown error occurred"
      );
    } finally {
      setLoading(false);
    }
  };

  const nextStep = async () => {
    const fields = getStepFields(currentStep);
    const isValid = await form.trigger(fields);

    if (isValid) {
      setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
    }
  };

  const prevStep = () => {
    setCurrentStep((prev) => Math.max(prev - 1, 0));
  };

  const getStepFields = (
    step: number
  ): (keyof z.infer<typeof formSchema>)[] => {
    switch (step) {
      case 0:
        return ["title", "engagementId", "description"];
      case 1:
        return ["amount", "platformFee", "trustline", "receiverMemo"];
      case 2:
        return ["roles"];
      case 3:
        return ["milestones"];
      default:
        return [];
    }
  };

  return {
    form,
    loading,
    response,
    trustlinesOptions,
    currentStep,
    addMilestone,
    removeMilestone,
    loadTemplate,
    onSubmit,
    nextStep,
    prevStep,
  };
};

Form

This form is built with react hook form. We use the custom hook and zod schema mentioned before.

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Plus, Trash, ChevronLeft, ChevronRight } from "lucide-react";
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormMessage,
  FormDescription,
} from "@/components/ui/form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { UseFormReturn } from "react-hook-form";
import { z } from "zod";
import { formSchema } from "../../schemas/initialize-escrow-form.schema";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { steps } from "../../constants/initialize-steps.constant";
import { InitializeEscrowResponse } from "@/@types/escrows/escrow-response.entity";
import { ResponseDisplay } from "@/components/utils/response-display";

interface InitializeEscrowFormProps {
  form: UseFormReturn<z.infer<typeof formSchema>>;
  loading?: boolean;
  response: InitializeEscrowResponse | null;
  trustlinesOptions: { value: string; label: string }[];
  currentStep: number;
  onSubmit: (data: z.infer<typeof formSchema>) => Promise<void>;
  addMilestone: () => void;
  removeMilestone: (index: number) => void;
  nextStep: () => void;
  prevStep: () => void;
}

export const InitializeEscrowForm = ({
  form,
  loading,
  response,
  trustlinesOptions,
  currentStep,
  onSubmit,
  addMilestone,
  removeMilestone,
  nextStep,
  prevStep,
}: InitializeEscrowFormProps) => {
  const renderStep = () => {
    const currentStepData = steps[currentStep];

    return (
      <Card className="w-full md:w-3/4">
        <CardHeader>
          <CardTitle className="text-2xl font-bold">
            {currentStepData.title}
          </CardTitle>
          <FormDescription>{currentStepData.description}</FormDescription>
        </CardHeader>
        <CardContent>
          <div className="space-y-4">
            {currentStep === 0 && (
              <>
                <FormField
                  control={form.control}
                  name="title"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Title</FormLabel>
                      <FormControl>
                        <Input placeholder="Escrow Title" {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="engagementId"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Engagement ID</FormLabel>
                      <FormControl>
                        <Input placeholder="ENG12345" {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="description"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Description</FormLabel>
                      <FormControl>
                        <Textarea placeholder="Escrow description" {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
              </>
            )}

            {currentStep === 1 && (
              <>
                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                  <FormField
                    control={form.control}
                    name="amount"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Amount</FormLabel>
                        <FormControl>
                          <Input placeholder="1000" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />

                  <FormField
                    control={form.control}
                    name="platformFee"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Platform Fee (%)</FormLabel>
                        <FormControl>
                          <Input placeholder="5" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />

                  <FormField
                    control={form.control}
                    name="trustline.address"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Trustline</FormLabel>
                        <FormControl>
                          <Select
                            onValueChange={(value) => {
                              const selectedOption = trustlinesOptions.find(
                                (opt) => opt.value === value
                              );
                              if (selectedOption) {
                                field.onChange(selectedOption.value);
                              }
                            }}
                            value={field.value || ""}
                          >
                            <SelectTrigger className="w-full">
                              <SelectValue placeholder="Select a trustline" />
                            </SelectTrigger>
                            <SelectContent>
                              {trustlinesOptions.map((option) => (
                                <SelectItem
                                  key={option.value}
                                  value={option.value}
                                >
                                  {option.label}
                                </SelectItem>
                              ))}
                            </SelectContent>
                          </Select>
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />

                  <FormField
                    control={form.control}
                    name="receiverMemo"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Receiver Memo</FormLabel>
                        <FormControl>
                          <Input
                            type="number"
                            min="0"
                            placeholder="0"
                            {...field}
                            value={field.value}
                            onChange={(e) =>
                              field.onChange(Number(e.target.value))
                            }
                          />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                </div>
              </>
            )}

            {currentStep === 2 && (
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <FormField
                  control={form.control}
                  name="roles.approver"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Approver Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="roles.serviceProvider"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Service Provider Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="roles.platformAddress"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Platform Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="roles.releaseSigner"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Release Signer Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="roles.disputeResolver"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Dispute Resolver Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="roles.receiver"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Receiver Address</FormLabel>
                      <FormControl>
                        <Input placeholder="GCU2QK..." {...field} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
              </div>
            )}

            {currentStep === 3 && (
              <div className="space-y-4">
                <div className="flex items-center justify-between">
                  <FormLabel>Milestones</FormLabel>
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    onClick={addMilestone}
                  >
                    <Plus className="h-4 w-4 mr-2" /> Add Milestone
                  </Button>
                </div>

                {form.watch("milestones").map((_, index) => (
                  <Card key={index}>
                    <CardContent className="p-4">
                      <div className="flex items-start gap-2">
                        <div className="flex-1">
                          <FormField
                            control={form.control}
                            name={`milestones.${index}.description`}
                            render={({ field }) => (
                              <FormItem>
                                <FormLabel>
                                  Milestone {index + 1} - Description
                                </FormLabel>
                                <FormControl>
                                  <Textarea
                                    placeholder="Milestone description"
                                    {...field}
                                  />
                                </FormControl>
                                <FormMessage />
                              </FormItem>
                            )}
                          />
                        </div>
                        {form.watch("milestones").length > 1 && (
                          <Button
                            type="button"
                            variant="ghost"
                            size="icon"
                            onClick={() => removeMilestone(index)}
                            className="mt-8"
                          >
                            <Trash className="h-4 w-4" />
                          </Button>
                        )}
                      </div>
                    </CardContent>
                  </Card>
                ))}
              </div>
            )}
          </div>
        </CardContent>
      </Card>
    );
  };

  return (
    <div className="space-y-6 w-full">
      <div className="flex items-center justify-between mb-8 w-full">
        {steps.map((step, index) => (
          <div
            key={step.id}
            className={cn(
              "flex items-center",
              index !== steps.length - 1 ? "flex-1" : ""
            )}
          >
            <div
              className={cn(
                "flex items-center justify-center w-8 h-8 rounded-full transition-colors",
                index <= currentStep
                  ? "bg-primary text-primary-foreground"
                  : "bg-muted"
              )}
            >
              {index + 1}
            </div>
            {index !== steps.length - 1 && (
              <div
                className={cn(
                  "flex-1 h-1 mx-2 transition-colors",
                  index < currentStep ? "bg-primary" : "bg-muted"
                )}
              />
            )}
          </div>
        ))}
      </div>

      <div className="flex flex-col items-center justify-center w-full">
        <Form {...form}>
          <form
            onSubmit={form.handleSubmit(onSubmit)}
            className="space-y-6 w-full flex flex-col items-center justify-center"
          >
            {renderStep()}
          </form>
        </Form>

        <div className="flex w-3/4 justify-between pt-4">
          <Button
            type="button"
            variant="outline"
            onClick={prevStep}
            disabled={currentStep === 0}
          >
            <ChevronLeft className="h-4 w-4 mr-2" />
            Previous
          </Button>

          {currentStep === steps.length - 1 ? (
            <Button
              type="button"
              onClick={() => form.handleSubmit(onSubmit)()}
              disabled={loading}
            >
              {loading ? "Initializing..." : "Initialize Escrow"}
            </Button>
          ) : (
            <Button type="button" onClick={nextStep}>
              Next
              <ChevronRight className="h-4 w-4 ml-2" />
            </Button>
          )}
        </div>
      </div>

      <ResponseDisplay response={response} />
    </div>
  );
};

PreviousFormsNextFund Escrow

Last updated 20 days ago

Was this helpful?