diff --git a/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx b/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx
index 899c481..b73aa2a 100644
--- a/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx
+++ b/apps/backoffice-tokenization/src/components/shared/campaign-card.tsx
@@ -6,7 +6,7 @@ import { Badge } from "@tokenization/ui/badge";
import { Button } from "@tokenization/ui/button";
import { CampaignCard as SharedCampaignCard } from "@tokenization/ui/campaign-card";
import { cn } from "@tokenization/shared/lib/utils";
-import { Landmark } from "lucide-react";
+import { Banknote, CheckCircle, Circle, Landmark } from "lucide-react";
import { useGetEscrowFromIndexerByContractIds } from "@trustless-work/escrow";
import type { MultiReleaseMilestone } from "@trustless-work/escrow/types";
import type { Campaign } from "@/features/campaigns/types/campaign.types";
@@ -39,9 +39,12 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
staleTime: 1000 * 60 * 5,
});
- const milestones = (escrowData?.milestones ?? []) as MultiReleaseMilestone[];
- const assigned = milestones.reduce((sum, m) => sum + fromStroops(m.amount ?? 0), 0);
- const progressValue = campaign.poolSize > 0 ? Math.min(100, (assigned / campaign.poolSize) * 100) : 0;
+ const allMilestones = (escrowData?.milestones ?? []) as MultiReleaseMilestone[];
+ const visibleMilestones = allMilestones.slice(1);
+ const assigned = allMilestones.reduce((sum, m) => sum + fromStroops(m.amount ?? 0), 0);
+ const loansCompleted = visibleMilestones.filter((m) => m.status === "Approved").length;
+ const totalLoans = visibleMilestones.length;
+ const progressValue = totalLoans > 0 ? Math.min(100, (loansCompleted / totalLoans) * 100) : 0;
return (
- USDC {formatCurrency(assigned)} / USDC {formatCurrency(campaign.poolSize)}
+ Pool Size: USDC {formatCurrency(assigned)} / USDC {formatCurrency(campaign.poolSize)}
- {campaign.vaultId ? (
-
- Vault: {campaign.vaultId}
-
- ) : null}
}
- progress={{ label: "Dinero recaudado", value: progressValue }}
- />
+ progress={{ label: "Loans Completed", value: progressValue }}
+ >
+ {visibleMilestones.length > 0 ? (
+ <>
+
+ Loans
+
+
+ {visibleMilestones.map((m, i) => (
+ -
+ {m.flags?.approved ? (
+
+ ) : m.flags?.released ? (
+
+ ) : (
+
+ )}
+ {m.description || `Loan ${i + 1}`}
+ {m.amount} USDC
+
+ ))}
+
+ >
+ ) : (
+ No loans available.
+ )}
+
);
}
diff --git a/apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx b/apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx
index 2a6d87e..a4ff64b 100644
--- a/apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx
+++ b/apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx
@@ -245,18 +245,17 @@ export function ManageLoansView({ contractId }: ManageLoansViewProps) {
milestones.map((milestone, index) => {
const isApproved = milestone.flags?.approved === true;
const isReleased = milestone.flags?.released === true;
- const milestoneAmount = fromStroops(milestone.amount || 0);
+ const milestoneAmount = milestone.amount
const insufficientFunds = escrowBalance < milestoneAmount;
return (
{
clearFlowState();
router.push("/campaigns");
@@ -298,7 +302,7 @@ export function useCreateCampaign() {
setPhaseStatus(currentPhase, "error", message);
setDeployFailedAt(currentPhase);
}
- }, [walletAddress, router]);
+ }, [walletAddress, router, queryClient]);
const retryDeploy = useCallback(() => {
if (deployFailedAt === null) return;
diff --git a/apps/backoffice-tokenization/src/features/campaigns/hooks/useToggleVault.ts b/apps/backoffice-tokenization/src/features/campaigns/hooks/useToggleVault.ts
index afd5fce..97110fd 100644
--- a/apps/backoffice-tokenization/src/features/campaigns/hooks/useToggleVault.ts
+++ b/apps/backoffice-tokenization/src/features/campaigns/hooks/useToggleVault.ts
@@ -1,10 +1,14 @@
"use client";
import { useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
import { signTransaction } from "@tokenization/tw-blocks-shared/src/wallet-kit/wallet-kit";
import { submitAndExtractAddress } from "@/features/campaigns/services/soroban.service";
-import { enableVault } from "@/features/campaigns/services/campaigns.api";
+import {
+ enableVault,
+ updateCampaignStatusByVaultId,
+} from "@/features/campaigns/services/campaigns.api";
interface UseToggleVaultParams {
onSuccess?: () => void;
@@ -12,6 +16,7 @@ interface UseToggleVaultParams {
export function useToggleVault({ onSuccess }: UseToggleVaultParams = {}) {
const { walletAddress } = useWalletContext();
+ const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
@@ -39,6 +44,16 @@ export function useToggleVault({ onSuccess }: UseToggleVaultParams = {}) {
await submitAndExtractAddress(signedXdr);
+ try {
+ await updateCampaignStatusByVaultId(
+ vaultContractId,
+ enabled ? "CLAIMABLE" : "FUNDRAISING",
+ );
+ await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
+ } catch {
+ // Campaign may not exist or vaultId not linked; status update is best-effort
+ }
+
onSuccess?.();
} catch (e) {
const message = e instanceof Error ? e.message : "Unexpected error";
diff --git a/apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts b/apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts
index 02b1142..fbc9e5f 100644
--- a/apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts
+++ b/apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts
@@ -1,39 +1,14 @@
+import { httpClient } from "@/lib/httpClient";
import type { Campaign } from "@/features/campaigns/types/campaign.types";
-const CORE_API = "/core-api";
-
-const API_KEY = process.env.NEXT_PUBLIC_CORE_API_KEY ?? "";
-
-async function post(path: string, body: unknown): Promise {
- const res = await fetch(`${CORE_API}${path}`, {
- method: "POST",
- headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
- body: JSON.stringify(body),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- throw new Error(
- (err as { message?: string }).message ??
- `Error ${res.status} en ${path}`,
- );
- }
- return res.json() as Promise;
-}
-
export async function getCampaigns(): Promise {
- const res = await fetch(`${CORE_API}/campaigns`, {
- headers: { "x-api-key": API_KEY },
- });
- if (!res.ok) throw new Error("No se pudieron cargar las campañas.");
- return res.json();
+ const { data } = await httpClient.get("/campaigns");
+ return data;
}
export async function getCampaignById(id: string): Promise {
- const res = await fetch(`${CORE_API}/campaigns/${id}`, {
- headers: { "x-api-key": API_KEY },
- });
- if (!res.ok) throw new Error("No se pudo cargar la campaña.");
- return res.json();
+ const { data } = await httpClient.get(`/campaigns/${id}`);
+ return data;
}
export async function deployAll(params: {
@@ -46,25 +21,32 @@ export async function deployAll(params: {
maxPerInvestor: number;
callerPublicKey: string;
}): Promise<{ unsignedXdr: string }> {
- return post("/deploy/all", params);
+ const { data } = await httpClient.post<{ unsignedXdr: string }>(
+ "/deploy/all",
+ params,
+ );
+ return data;
}
export async function updateCampaignStatus(
id: string,
status: string,
): Promise {
- const res = await fetch(`${CORE_API}/campaigns/${id}/status`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
- body: JSON.stringify({ status }),
+ const { data } = await httpClient.patch(`/campaigns/${id}/status`, {
+ status,
});
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- throw new Error(
- (err as { message?: string }).message ?? `Error ${res.status}`,
- );
- }
- return res.json();
+ return data;
+}
+
+export async function updateCampaignStatusByVaultId(
+ vaultId: string,
+ status: string,
+): Promise {
+ const { data } = await httpClient.patch(
+ `/campaigns/by-vault/${vaultId}/status`,
+ { status },
+ );
+ return data;
}
export async function createCampaign(params: {
@@ -80,35 +62,11 @@ export async function createCampaign(params: {
tokenSaleId: string;
vaultId?: string;
}): Promise<{ id: string }> {
- return post("/campaigns", params);
-}
-
-async function get(path: string): Promise {
- const res = await fetch(`${CORE_API}${path}`, {
- headers: { "x-api-key": API_KEY },
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- throw new Error(
- (err as { message?: string }).message ?? `Error ${res.status} on ${path}`,
- );
- }
- return res.json() as Promise;
-}
-
-async function patch(path: string, body: unknown): Promise {
- const res = await fetch(`${CORE_API}${path}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
- body: JSON.stringify(body),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- throw new Error(
- (err as { message?: string }).message ?? `Error ${res.status} on ${path}`,
- );
- }
- return res.json() as Promise;
+ const { data } = await httpClient.post<{ id: string }>(
+ "/campaigns",
+ params,
+ );
+ return data;
}
export async function enableVault(params: {
@@ -117,21 +75,29 @@ export async function enableVault(params: {
enabled: boolean;
callerPublicKey: string;
}): Promise<{ unsignedXdr: string }> {
- return post("/vault/availability-for-exchange", params);
+ const { data } = await httpClient.post<{ unsignedXdr: string }>(
+ "/vault/availability-for-exchange",
+ params,
+ );
+ return data;
}
export async function getVaultIsEnabled(
contractId: string,
callerPublicKey: string,
): Promise<{ enabled: boolean }> {
- return get(
+ const { data } = await httpClient.get<{ enabled: boolean }>(
`/vault/is-enabled?contractId=${contractId}&callerPublicKey=${callerPublicKey}`,
);
+ return data;
}
export async function updateCampaignVaultId(
campaignId: string,
vaultId: string,
): Promise {
- return patch(`/campaigns/${campaignId}`, { vaultId });
+ const { data } = await httpClient.patch(`/campaigns/${campaignId}`, {
+ vaultId,
+ });
+ return data;
}
diff --git a/apps/backoffice-tokenization/src/features/home/HeroSection.tsx b/apps/backoffice-tokenization/src/features/home/HeroSection.tsx
index 05f9e66..8753db0 100644
--- a/apps/backoffice-tokenization/src/features/home/HeroSection.tsx
+++ b/apps/backoffice-tokenization/src/features/home/HeroSection.tsx
@@ -20,7 +20,7 @@ export const HeroSection = () => {
project, from contract deployment to milestone execution.
-
+
Open App
diff --git a/apps/backoffice-tokenization/src/features/tokens/services/token.service.ts b/apps/backoffice-tokenization/src/features/tokens/services/token.service.ts
index ec257d4..7bf2643 100644
--- a/apps/backoffice-tokenization/src/features/tokens/services/token.service.ts
+++ b/apps/backoffice-tokenization/src/features/tokens/services/token.service.ts
@@ -1,4 +1,4 @@
-import axios, { AxiosInstance } from "axios";
+import { httpClient } from "@/lib/httpClient";
export type DeployTokenResponse = {
success: boolean;
@@ -13,22 +13,8 @@ export type DeployTokenParams = {
};
export class TokenService {
- private readonly apiUrl: string;
- private readonly axios: AxiosInstance;
-
- constructor() {
- // If NEXT_PUBLIC_API_URL is set, use it. Otherwise, use relative path /api
- // This allows the service to work both with external APIs and Next.js route handlers
- const envApiUrl = process.env.NEXT_PUBLIC_API_URL;
- this.apiUrl = envApiUrl && envApiUrl.trim() !== "" ? envApiUrl : "/api";
-
- this.axios = axios.create({
- baseURL: this.apiUrl,
- });
- }
-
async deployToken(params: DeployTokenParams): Promise
{
- const response = await this.axios.post("/deploy", {
+ const response = await httpClient.post("/deploy", {
escrowContractId: params.escrowContractId,
tokenName: params.tokenName,
tokenSymbol: params.tokenSymbol,
diff --git a/apps/backoffice-tokenization/src/features/vaults/deploy/dialog/useEnableVault.ts b/apps/backoffice-tokenization/src/features/vaults/deploy/dialog/useEnableVault.ts
index 96bdaa3..88c64be 100644
--- a/apps/backoffice-tokenization/src/features/vaults/deploy/dialog/useEnableVault.ts
+++ b/apps/backoffice-tokenization/src/features/vaults/deploy/dialog/useEnableVault.ts
@@ -8,6 +8,8 @@ import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/
import { signTransaction } from "@tokenization/tw-blocks-shared/src/wallet-kit/wallet-kit";
import { SendTransactionService } from "@/lib/sendTransactionService";
import { toastSuccessWithTx } from "@/lib/toastWithTx";
+import { updateCampaignStatusByVaultId } from "@/features/campaigns/services/campaigns.api";
+import { useQueryClient } from "@tanstack/react-query";
export type EnableVaultFormValues = {
vaultContractAddress: string;
@@ -19,6 +21,7 @@ type UseEnableVaultParams = {
export function useEnableVault(params?: UseEnableVaultParams) {
const { walletAddress } = useWalletContext();
+ const queryClient = useQueryClient();
const form = useForm({
defaultValues: {
@@ -67,6 +70,16 @@ export function useEnableVault(params?: UseEnableVaultParams) {
toastSuccessWithTx("Vault enabled successfully", submitResponse.hash);
+ try {
+ await updateCampaignStatusByVaultId(
+ values.vaultContractAddress,
+ "CLAIMABLE",
+ );
+ await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
+ } catch {
+ // Campaign may not exist or vaultId not linked; status update is best-effort
+ }
+
setResponse(enableResponse);
if (enableResponse?.success) {
diff --git a/apps/backoffice-tokenization/src/features/vaults/services/vault.service.ts b/apps/backoffice-tokenization/src/features/vaults/services/vault.service.ts
index 5d4bf1a..db0162a 100644
--- a/apps/backoffice-tokenization/src/features/vaults/services/vault.service.ts
+++ b/apps/backoffice-tokenization/src/features/vaults/services/vault.service.ts
@@ -1,4 +1,4 @@
-import axios, { AxiosInstance } from "axios";
+import { httpClient } from "@/lib/httpClient";
export type DeployVaultResponse = {
success: boolean;
@@ -17,20 +17,6 @@ export type EnableVaultPayload = {
};
export class VaultService {
- private readonly apiUrl: string;
- private readonly axios: AxiosInstance;
-
- constructor() {
- // If NEXT_PUBLIC_API_URL is set, use it. Otherwise, use relative path /api
- // This allows the service to work both with external APIs and Next.js route handlers
- const envApiUrl = process.env.NEXT_PUBLIC_API_URL;
- this.apiUrl = envApiUrl && envApiUrl.trim() !== "" ? envApiUrl : "/api";
-
- this.axios = axios.create({
- baseURL: this.apiUrl,
- });
- }
-
async deployVault({
admin,
enabled,
@@ -44,7 +30,7 @@ export class VaultService {
token: string;
usdc: string;
}): Promise {
- const response = await this.axios.post(
+ const response = await httpClient.post(
"/deploy/vault-contract",
{
admin,
@@ -62,7 +48,7 @@ export class VaultService {
vaultContractId,
adminAddress,
}: EnableVaultPayload): Promise {
- const response = await this.axios.post(
+ const response = await httpClient.post(
"/vault-contract/availability-for-exchange",
{
vaultContractId,
diff --git a/apps/backoffice-tokenization/src/lib/httpClient.ts b/apps/backoffice-tokenization/src/lib/httpClient.ts
index 65474d9..6ff496d 100644
--- a/apps/backoffice-tokenization/src/lib/httpClient.ts
+++ b/apps/backoffice-tokenization/src/lib/httpClient.ts
@@ -1,6 +1,6 @@
import { createHttpClient } from "@tokenization/shared/lib/httpClient";
export const httpClient = createHttpClient({
- baseURL: "/core-api",
+ baseURL: process.env.NEXT_PUBLIC_CORE_API_URL ?? "http://localhost:4000",
apiKey: process.env.NEXT_PUBLIC_BACKOFFICE_API_KEY ?? "",
});
diff --git a/apps/core/src/app.module.ts b/apps/core/src/app.module.ts
index d5874b2..a935a73 100644
--- a/apps/core/src/app.module.ts
+++ b/apps/core/src/app.module.ts
@@ -10,6 +10,7 @@ import { LoansModule } from './loans/loans.module';
import { ParticipationTokenModule } from './participation-token/participation-token.module';
import { VaultModule } from './vault/vault.module';
import { TokenSaleModule } from './token-sale/token-sale.module';
+import { HelperModule } from './helper/helper.module';
@Module({
imports: [
@@ -22,6 +23,7 @@ import { TokenSaleModule } from './token-sale/token-sale.module';
ParticipationTokenModule,
VaultModule,
TokenSaleModule,
+HelperModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/apps/core/src/campaigns/campaigns.controller.ts b/apps/core/src/campaigns/campaigns.controller.ts
index ca845dd..0991bc0 100644
--- a/apps/core/src/campaigns/campaigns.controller.ts
+++ b/apps/core/src/campaigns/campaigns.controller.ts
@@ -31,6 +31,14 @@ export class CampaignsController {
return this.campaignsService.create(dto);
}
+ @Patch('by-vault/:vaultId/status')
+ updateStatusByVaultId(
+ @Param('vaultId') vaultId: string,
+ @Body() dto: UpdateCampaignStatusDto,
+ ) {
+ return this.campaignsService.updateStatusByVaultId(vaultId, dto);
+ }
+
@Patch(':id/status')
updateStatus(@Param('id') id: string, @Body() dto: UpdateCampaignStatusDto) {
return this.campaignsService.updateStatus(id, dto);
diff --git a/apps/core/src/campaigns/campaigns.service.ts b/apps/core/src/campaigns/campaigns.service.ts
index b2a1b0c..2694abb 100644
--- a/apps/core/src/campaigns/campaigns.service.ts
+++ b/apps/core/src/campaigns/campaigns.service.ts
@@ -14,7 +14,11 @@ const ALLOWED_TRANSITIONS: Record = {
[CampaignStatus.FUNDRAISING]: [CampaignStatus.ACTIVE, CampaignStatus.PAUSED],
[CampaignStatus.ACTIVE]: [CampaignStatus.REPAYMENT, CampaignStatus.PAUSED],
[CampaignStatus.REPAYMENT]: [CampaignStatus.CLAIMABLE, CampaignStatus.PAUSED],
- [CampaignStatus.CLAIMABLE]: [CampaignStatus.CLOSED, CampaignStatus.PAUSED],
+ [CampaignStatus.CLAIMABLE]: [
+ CampaignStatus.CLOSED,
+ CampaignStatus.PAUSED,
+ CampaignStatus.FUNDRAISING,
+ ],
[CampaignStatus.CLOSED]: [],
[CampaignStatus.PAUSED]: [],
};
@@ -66,6 +70,10 @@ export class CampaignsService {
const currentStatus = campaign.status;
const newStatus = dto.status;
+ if (currentStatus === newStatus) {
+ return campaign;
+ }
+
this.validateStatusTransition(
currentStatus,
newStatus,
@@ -92,6 +100,35 @@ export class CampaignsService {
});
}
+ async findOneByVaultId(vaultId: string) {
+ const campaign = await this.prisma.campaign.findFirst({
+ where: { vaultId },
+ include: { investments: true },
+ });
+ if (!campaign) {
+ throw new NotFoundException(
+ `Campaign with vaultId ${vaultId} not found`,
+ );
+ }
+ return campaign;
+ }
+
+ async updateStatusByVaultId(
+ vaultId: string,
+ dto: UpdateCampaignStatusDto,
+ ) {
+ const campaign = await this.findOneByVaultId(vaultId);
+
+ if (campaign.status === dto.status) {
+ return campaign;
+ }
+
+ return this.prisma.campaign.update({
+ where: { id: campaign.id },
+ data: { status: dto.status },
+ });
+ }
+
private validateStatusTransition(
current: CampaignStatus,
next: CampaignStatus,
diff --git a/apps/core/src/helper/dto/send-transaction.dto.ts b/apps/core/src/helper/dto/send-transaction.dto.ts
new file mode 100644
index 0000000..8252d74
--- /dev/null
+++ b/apps/core/src/helper/dto/send-transaction.dto.ts
@@ -0,0 +1,7 @@
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class SendTransactionDto {
+ @IsString()
+ @IsNotEmpty()
+ signedXdr: string;
+}
diff --git a/apps/core/src/helper/helper.controller.ts b/apps/core/src/helper/helper.controller.ts
new file mode 100644
index 0000000..512f8f5
--- /dev/null
+++ b/apps/core/src/helper/helper.controller.ts
@@ -0,0 +1,25 @@
+import { Controller, Post, Body } from '@nestjs/common';
+import { HelperService } from './helper.service';
+import { SendTransactionDto } from './dto/send-transaction.dto';
+import { rpc } from '@stellar/stellar-sdk';
+
+@Controller('helper')
+export class HelperController {
+ constructor(private readonly helperService: HelperService) {}
+
+ @Post('send-transaction')
+ async sendTransaction(@Body() dto: SendTransactionDto) {
+ try {
+ return await this.helperService.submitTransaction(dto.signedXdr);
+ } catch (error) {
+ const message =
+ error instanceof Error ? error.message : String(error);
+ return {
+ status: rpc.Api.GetTransactionStatus.FAILED,
+ message:
+ message ||
+ 'An unknown error occurred while submitting the transaction.',
+ };
+ }
+ }
+}
diff --git a/apps/core/src/helper/helper.module.ts b/apps/core/src/helper/helper.module.ts
new file mode 100644
index 0000000..596d70d
--- /dev/null
+++ b/apps/core/src/helper/helper.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { HelperController } from './helper.controller';
+import { HelperService } from './helper.service';
+
+@Module({
+ controllers: [HelperController],
+ providers: [HelperService],
+})
+export class HelperModule {}
diff --git a/apps/core/src/helper/helper.service.ts b/apps/core/src/helper/helper.service.ts
new file mode 100644
index 0000000..3f205c6
--- /dev/null
+++ b/apps/core/src/helper/helper.service.ts
@@ -0,0 +1,48 @@
+import { Injectable } from '@nestjs/common';
+import {
+ TransactionBuilder,
+ Networks,
+ Horizon,
+ rpc,
+} from '@stellar/stellar-sdk';
+
+const HORIZON_TESTNET = 'https://horizon-testnet.stellar.org';
+
+@Injectable()
+export class HelperService {
+ private readonly horizonUrl: string;
+
+ constructor() {
+ this.horizonUrl = process.env.HORIZON_URL ?? HORIZON_TESTNET;
+ }
+
+ async submitTransaction(signedXdr: string): Promise<{
+ status: string;
+ message: string;
+ hash?: string;
+ }> {
+ const server = new Horizon.Server(this.horizonUrl, { allowHttp: true });
+
+ const transaction = TransactionBuilder.fromXDR(
+ signedXdr,
+ Networks.TESTNET,
+ );
+
+ const response = await server.submitTransaction(transaction);
+
+ if (!response.successful) {
+ return {
+ status: rpc.Api.GetTransactionStatus.FAILED,
+ message:
+ 'The transaction could not be sent to the Stellar network for some unknown reason. Please try again.',
+ };
+ }
+
+ return {
+ status: rpc.Api.GetTransactionStatus.SUCCESS,
+ message:
+ 'The transaction has been successfully sent to the Stellar network.',
+ hash: response.hash,
+ };
+ }
+}
diff --git a/apps/core/src/token-sale/token-sale.service.ts b/apps/core/src/token-sale/token-sale.service.ts
index 6090c63..a3e0068 100644
--- a/apps/core/src/token-sale/token-sale.service.ts
+++ b/apps/core/src/token-sale/token-sale.service.ts
@@ -8,7 +8,7 @@ import { SetAdminDto } from './dto/set-admin.dto';
@Injectable()
export class TokenSaleService {
- constructor(private readonly soroban: SorobanService) {}
+ constructor(private readonly soroban: SorobanService) { }
// ── Writes ──
@@ -23,7 +23,6 @@ export class TokenSaleService {
amount: toMicroUSDC(dto.amount),
},
dto.callerPublicKey,
- 'token-sale',
);
}
@@ -36,7 +35,6 @@ export class TokenSaleService {
new_max_per_investor: toMicroUSDC(dto.newMaxPerInvestor),
},
dto.callerPublicKey,
- 'token-sale',
);
}
@@ -46,7 +44,6 @@ export class TokenSaleService {
'set_token',
{ new_token: dto.newToken },
dto.callerPublicKey,
- 'token-sale',
);
}
@@ -56,13 +53,12 @@ export class TokenSaleService {
'set_admin',
{ new_admin: dto.newAdmin },
dto.callerPublicKey,
- 'token-sale',
);
}
// ── Reads ──
getAdmin(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_admin', {}, callerPublicKey, 'token-sale');
+ return this.soroban.readContractState(contractId, 'get_admin', {}, callerPublicKey);
}
}
diff --git a/apps/core/src/vault/vault.service.ts b/apps/core/src/vault/vault.service.ts
index be32a73..7ba1625 100644
--- a/apps/core/src/vault/vault.service.ts
+++ b/apps/core/src/vault/vault.service.ts
@@ -10,7 +10,7 @@ export class VaultService {
constructor(
private readonly soroban: SorobanService,
private readonly prisma: PrismaService,
- ) {}
+ ) { }
async availabilityForExchange(dto: AvailabilityForExchangeDto): Promise {
const unsignedXdr = await this.soroban.buildContractCallTransaction(
@@ -18,7 +18,6 @@ export class VaultService {
'availability_for_exchange',
{ enabled: dto.enabled },
dto.callerPublicKey,
- 'vault',
);
if (dto.enabled && dto.campaignId) {
@@ -37,43 +36,42 @@ export class VaultService {
'claim',
{ beneficiary: dto.beneficiary },
dto.callerPublicKey,
- 'vault',
);
}
getOverview(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_vault_overview', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_vault_overview', {}, callerPublicKey);
}
previewClaim(contractId: string, beneficiary: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'preview_claim', { beneficiary }, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'preview_claim', { beneficiary }, callerPublicKey);
}
isEnabled(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'is_enabled', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'is_enabled', {}, callerPublicKey);
}
getUsdcBalance(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_vault_usdc_balance', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_vault_usdc_balance', {}, callerPublicKey);
}
getTotalTokensRedeemed(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_total_tokens_redeemed', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_total_tokens_redeemed', {}, callerPublicKey);
}
getAdmin(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_admin', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_admin', {}, callerPublicKey);
}
getRoiPercentage(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_roi_percentage', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_roi_percentage', {}, callerPublicKey);
}
getTokenAddress(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_token_address', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_token_address', {}, callerPublicKey);
}
getUsdcAddress(contractId: string, callerPublicKey: string): Promise {
- return this.soroban.readContractState(contractId, 'get_usdc_address', {}, callerPublicKey, 'vault');
+ return this.soroban.readContractState(contractId, 'get_usdc_address', {}, callerPublicKey);
}
}
diff --git a/apps/investor-tokenization/services/wasm/soroban_token_contract.wasm b/apps/investor-tokenization/services/wasm/soroban_token_contract.wasm
deleted file mode 100644
index 733c647..0000000
Binary files a/apps/investor-tokenization/services/wasm/soroban_token_contract.wasm and /dev/null differ
diff --git a/apps/investor-tokenization/services/wasm/token_sale.wasm b/apps/investor-tokenization/services/wasm/token_sale.wasm
deleted file mode 100644
index c543cd8..0000000
Binary files a/apps/investor-tokenization/services/wasm/token_sale.wasm and /dev/null differ
diff --git a/apps/investor-tokenization/services/wasm/vault_contract.wasm b/apps/investor-tokenization/services/wasm/vault_contract.wasm
deleted file mode 100644
index 0497db3..0000000
Binary files a/apps/investor-tokenization/services/wasm/vault_contract.wasm and /dev/null differ
diff --git a/apps/investor-tokenization/src/app/api/deploy/route.ts b/apps/investor-tokenization/src/app/api/deploy/route.ts
deleted file mode 100644
index 6e45bbf..0000000
--- a/apps/investor-tokenization/src/app/api/deploy/route.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { NextResponse } from "next/server";
-import { SorobanClient } from "../../../lib/sorobanClient";
-import { deployTokenContracts } from "../../../lib/tokenDeploymentService";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-const SOURCE_SECRET = process.env.SOURCE_SECRET || "";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { escrowContractId, tokenName, tokenSymbol } = data ?? {};
-
- if (!escrowContractId || !tokenName || !tokenSymbol) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details: "escrowContractId, tokenName, and tokenSymbol are required",
- }),
- { status: 400 },
- );
- }
-
- try {
- const sorobanClient = new SorobanClient({
- rpcUrl: RPC_URL,
- sourceSecret: SOURCE_SECRET,
- });
-
- const { tokenFactoryAddress, tokenSaleAddress } =
- await deployTokenContracts(sorobanClient, {
- escrowContractId,
- tokenName,
- tokenSymbol,
- });
-
- return NextResponse.json({
- success: true,
- tokenFactoryAddress,
- tokenSaleAddress,
- });
- } catch (error) {
- console.error("Deployment error:", error);
- return new Response(
- JSON.stringify({
- error: "Internal Server Error",
- details: error instanceof Error ? error.message : String(error),
- }),
- { status: 500 },
- );
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/deploy/vault-contract/route.ts b/apps/investor-tokenization/src/app/api/deploy/vault-contract/route.ts
deleted file mode 100644
index 0273b4b..0000000
--- a/apps/investor-tokenization/src/app/api/deploy/vault-contract/route.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { NextResponse } from "next/server";
-import { SorobanClient } from "../../../../lib/sorobanClient";
-import { deployVaultContract } from "../../../../lib/vaultDeploymentService";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-const SOURCE_SECRET = process.env.SOURCE_SECRET || "";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { admin, enabled, price, token, usdc } = data ?? {};
-
- if (!admin || typeof enabled !== "boolean" || !price || !token || !usdc) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details:
- "admin, enabled (boolean), price, token, and usdc are required",
- }),
- { status: 400 },
- );
- }
-
- if (typeof price !== "number" && typeof price !== "string") {
- return new Response(
- JSON.stringify({
- error: "Invalid price",
- details: "price must be a number or string",
- }),
- { status: 400 },
- );
- }
-
- try {
- const sorobanClient = new SorobanClient({
- rpcUrl: RPC_URL,
- sourceSecret: SOURCE_SECRET,
- });
-
- const { vaultContractAddress } = await deployVaultContract(sorobanClient, {
- admin,
- enabled,
- price,
- token,
- usdc,
- });
-
- return NextResponse.json({
- success: true,
- vaultContractAddress,
- });
- } catch (error) {
- console.error("Vault deployment error:", error);
- return new Response(
- JSON.stringify({
- error: "Internal Server Error",
- details: error instanceof Error ? error.message : String(error),
- }),
- { status: 500 },
- );
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/helper/send-transaction/route.ts b/apps/investor-tokenization/src/app/api/helper/send-transaction/route.ts
deleted file mode 100644
index 24824b7..0000000
--- a/apps/investor-tokenization/src/app/api/helper/send-transaction/route.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-
-export async function POST(request: Request) {
- const { signedXdr } = await request.json();
- const server = new StellarSDK.Horizon.Server(
- "https://horizon-testnet.stellar.org",
- {
- allowHttp: true,
- },
- );
-
- try {
- const transaction = StellarSDK.TransactionBuilder.fromXDR(
- signedXdr,
- StellarSDK.Networks.TESTNET,
- );
-
- const response = await server.submitTransaction(transaction);
- if (!response.successful) {
- return NextResponse.json({
- status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
- message:
- "The transaction could not be sent to the Stellar network for some unknown reason. Please try again.",
- });
- }
- return NextResponse.json({
- status: StellarSDK.rpc.Api.GetTransactionStatus.SUCCESS,
- message:
- "The transaction has been successfully sent to the Stellar network.",
- hash: response.hash,
- });
- } catch (error) {
- console.error("Transaction submission error:", error);
- return NextResponse.json({
- status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
- message:
- error instanceof Error
- ? error.message
- : "An unknown error occurred while submitting the transaction.",
- });
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/token-balance/route.ts b/apps/investor-tokenization/src/app/api/token-balance/route.ts
deleted file mode 100644
index 9747091..0000000
--- a/apps/investor-tokenization/src/app/api/token-balance/route.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { tokenFactoryAddress, address } = data ?? {};
-
- if (!tokenFactoryAddress || !address) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details: "tokenFactoryAddress and address are required",
- }),
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
-
- // Try to get balance using contract function call first (works for Stellar Asset Contracts)
- // This is the preferred method as it works for both custom contracts and SAC
- try {
- const sourceAccount = await server.getAccount(address);
- const contract = new StellarSDK.Contract(tokenFactoryAddress);
-
- const transaction = new StellarSDK.TransactionBuilder(sourceAccount, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(
- contract.call(
- "balance",
- StellarSDK.nativeToScVal(new StellarSDK.Address(address), {
- type: "address",
- }),
- ),
- )
- .setTimeout(30)
- .build();
-
- const simulation = await server.simulateTransaction(transaction);
-
- // Check if simulation was successful and has results
- if (
- "results" in simulation &&
- Array.isArray(simulation.results) &&
- simulation.results.length > 0
- ) {
- const result = simulation.results[0];
- if (result && "retval" in result && result.retval) {
- const balanceVal = StellarSDK.scValToNative(result.retval);
- const balance =
- typeof balanceVal === "bigint"
- ? Number(balanceVal)
- : Number(balanceVal);
-
- return NextResponse.json({
- success: true,
- balance: balance.toString(),
- });
- }
- }
- } catch (functionCallError) {
- // If function call fails, fall back to reading from storage
- console.log(
- "Balance function call failed, trying storage read:",
- functionCallError,
- );
- }
-
- // Fallback: Read balance directly from contract storage
- // The balance is stored in persistent storage with key DataKey::Balance(address)
- const contractAddress = StellarSDK.Address.fromString(tokenFactoryAddress);
- const userAddress = StellarSDK.Address.fromString(address);
-
- // Create the storage key: DataKey::Balance(userAddress)
- // In Soroban, enum variants are encoded as vectors: [variant_index, ...data]
- // Balance is variant index 1 (0=Allowance, 1=Balance, 2=State, 3=Admin)
- // We need to create a vector ScVal: [1, userAddress]
- const vecElements: StellarSDK.xdr.ScVal[] = [
- StellarSDK.xdr.ScVal.scvU32(1), // Balance variant index
- userAddress.toScVal(), // The address parameter
- ];
- const balanceKey = StellarSDK.xdr.ScVal.scvVec(vecElements);
-
- const ledgerKey = StellarSDK.xdr.LedgerKey.contractData(
- new StellarSDK.xdr.LedgerKeyContractData({
- contract: contractAddress.toScAddress(),
- key: balanceKey,
- durability: StellarSDK.xdr.ContractDataDurability.persistent(),
- }),
- );
-
- // Get the ledger entry
- const ledgerEntries = await server.getLedgerEntries(ledgerKey);
-
- // !IMPORTANT: Not working
- if (
- !ledgerEntries ||
- !ledgerEntries.entries ||
- ledgerEntries.entries.length === 0
- ) {
- // No balance entry found means balance is 0
- return NextResponse.json({
- success: true,
- balance: "0",
- });
- }
-
- // Parse the storage entry to get the balance
- const entry = ledgerEntries.entries[0];
- if (!entry.val || !entry.val.contractData()) {
- return NextResponse.json(
- {
- success: false,
- balance: "0",
- error: "Invalid contract data format",
- },
- { status: 200 },
- );
- }
-
- const contractData = entry.val.contractData();
- const storageValue = contractData.val();
-
- // The value should be an i128 (the balance)
- // Parse it from ScVal
- let balance: number;
- try {
- const balanceVal = StellarSDK.scValToNative(storageValue);
- balance =
- typeof balanceVal === "bigint"
- ? Number(balanceVal)
- : Number(balanceVal);
- } catch (parseError) {
- return NextResponse.json(
- {
- success: false,
- balance: "0",
- error: `Failed to parse balance: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
- },
- { status: 200 },
- );
- }
-
- return NextResponse.json({
- success: true,
- balance: balance.toString(),
- });
- } catch (error) {
- console.error("Token balance fetch error:", error);
- return NextResponse.json(
- {
- success: false,
- balance: "0",
- error: error instanceof Error ? error.message : String(error),
- },
- { status: 200 },
- );
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/token-metadata/route.ts b/apps/investor-tokenization/src/app/api/token-metadata/route.ts
deleted file mode 100644
index 03e9f54..0000000
--- a/apps/investor-tokenization/src/app/api/token-metadata/route.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { tokenFactoryAddress } = data ?? {};
-
- if (!tokenFactoryAddress) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details: "tokenFactoryAddress is required",
- }),
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
-
- // Get a dummy account for simulation
- const dummyAccount = await server.getAccount("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF").catch(() => null);
-
- const account = dummyAccount || {
- accountId: () => "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
- sequenceNumber: () => "0",
- } as StellarSDK.Account;
-
- const contract = new StellarSDK.Contract(tokenFactoryAddress);
-
- // Fetch name, symbol, and decimals in parallel
- const [nameTx, symbolTx, decimalsTx] = await Promise.all([
- new StellarSDK.TransactionBuilder(account, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(contract.call("name"))
- .setTimeout(300)
- .build(),
- new StellarSDK.TransactionBuilder(account, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(contract.call("symbol"))
- .setTimeout(300)
- .build(),
- new StellarSDK.TransactionBuilder(account, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(contract.call("decimals"))
- .setTimeout(300)
- .build(),
- ]);
-
- const [nameSim, symbolSim, decimalsSim] = await Promise.all([
- server.simulateTransaction(nameTx),
- server.simulateTransaction(symbolTx),
- server.simulateTransaction(decimalsTx),
- ]);
-
- // Parse name
- let name = "Unknown Token";
- if (!("error" in nameSim) && nameSim.result?.retval) {
- try {
- const nameVal = StellarSDK.scValToNative(nameSim.result.retval);
- name = typeof nameVal === "string" ? nameVal : String(nameVal);
- } catch (e) {
- console.warn("Failed to parse name:", e);
- }
- }
-
- // Parse symbol
- let symbol = "TOKEN";
- if (!("error" in symbolSim) && symbolSim.result?.retval) {
- try {
- const symbolVal = StellarSDK.scValToNative(symbolSim.result.retval);
- symbol = typeof symbolVal === "string" ? symbolVal : String(symbolVal);
- } catch (e) {
- console.warn("Failed to parse symbol:", e);
- }
- }
-
- // Parse decimals
- let decimals = 7; // Default for Stellar
- if (!("error" in decimalsSim) && decimalsSim.result?.retval) {
- try {
- const decimalsVal = StellarSDK.scValToNative(decimalsSim.result.retval);
- decimals = typeof decimalsVal === "number" ? decimalsVal : Number(decimalsVal);
- } catch (e) {
- console.warn("Failed to parse decimals:", e);
- }
- }
-
- return NextResponse.json({
- success: true,
- name,
- symbol,
- decimals,
- });
- } catch (error) {
- console.error("Token metadata fetch error:", error);
- return NextResponse.json({
- success: false,
- name: "Unknown Token",
- symbol: "TOKEN",
- decimals: 7,
- error: error instanceof Error ? error.message : String(error),
- }, { status: 200 });
- }
-}
-
diff --git a/apps/investor-tokenization/src/app/api/token-sale/buy/route.ts b/apps/investor-tokenization/src/app/api/token-sale/buy/route.ts
deleted file mode 100644
index 3826e42..0000000
--- a/apps/investor-tokenization/src/app/api/token-sale/buy/route.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { adjustPricesToMicroUSDC } from "@/utils/adjustedAmounts";
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-import { extractContractError } from "@/lib/contractErrorHandler";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const {
- tokenSaleContractId,
- usdcAddress,
- payerAddress,
- beneficiaryAddress,
- amount,
- } = data ?? {};
-
- if (
- !tokenSaleContractId ||
- !usdcAddress ||
- !payerAddress ||
- !beneficiaryAddress
- ) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details:
- "tokenSaleContractId, usdcAddress, payerAddress, and beneficiaryAddress are required",
- }),
- { status: 400 },
- );
- }
-
- if (typeof amount !== "number" && typeof amount !== "string") {
- return new Response(
- JSON.stringify({
- error: "Invalid amount",
- details: "amount must be a number or string",
- }),
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
- const sourceAccount = await server.getAccount(payerAddress);
-
- const contract = new StellarSDK.Contract(tokenSaleContractId);
- const adjustedAmount = adjustPricesToMicroUSDC(Number(amount));
- const transaction = new StellarSDK.TransactionBuilder(sourceAccount, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(
- contract.call(
- "buy",
- StellarSDK.nativeToScVal(new StellarSDK.Address(usdcAddress), {
- type: "address",
- }),
- StellarSDK.nativeToScVal(new StellarSDK.Address(payerAddress), {
- type: "address",
- }),
- StellarSDK.nativeToScVal(new StellarSDK.Address(beneficiaryAddress), {
- type: "address",
- }),
- StellarSDK.nativeToScVal(adjustedAmount, { type: "i128" }),
- ),
- )
- .setTimeout(300)
- .build();
-
- const preparedTransaction = await server.prepareTransaction(transaction);
- const xdr = preparedTransaction.toXDR();
-
- return NextResponse.json({
- success: true,
- xdr,
- message:
- "Transaction built successfully. Sign with wallet and submit to network.",
- });
- } catch (error) {
- console.error("Buy transaction build error:", error);
- const { message, details } = extractContractError(error, "token-sale");
- return new Response(
- JSON.stringify({
- error: message,
- details: details,
- }),
- { status: 500 },
- );
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/trustline/add/route.ts b/apps/investor-tokenization/src/app/api/trustline/add/route.ts
deleted file mode 100644
index 216dce4..0000000
--- a/apps/investor-tokenization/src/app/api/trustline/add/route.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { address } = data ?? {};
-
- if (!address) {
- return NextResponse.json(
- { error: "Missing required field: address" },
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
- const sourceAccount = await server.getAccount(address);
-
- const asset = new StellarSDK.Asset("USDC", USDC_ISSUER);
-
- const transaction = new StellarSDK.TransactionBuilder(sourceAccount, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(StellarSDK.Operation.changeTrust({ asset }))
- .setTimeout(300)
- .build();
-
- return NextResponse.json({
- success: true,
- xdr: transaction.toXDR(),
- message: "Trustline transaction built. Sign with wallet and submit.",
- });
- } catch (error) {
- console.error("Trustline transaction build error:", error);
- return NextResponse.json(
- {
- error: "Failed to build trustline transaction",
- details: error instanceof Error ? error.message : String(error),
- },
- { status: 500 },
- );
- }
-}
diff --git a/apps/investor-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts b/apps/investor-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts
deleted file mode 100644
index 95f41f7..0000000
--- a/apps/investor-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-import { extractContractError } from "@/lib/contractErrorHandler";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { vaultContractId, adminAddress, enabled } = data ?? {};
-
- if (!vaultContractId || !adminAddress || typeof enabled !== "boolean") {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details:
- "vaultContractId, adminAddress, and enabled (boolean) are required",
- }),
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
- const sourceAccount = await server.getAccount(adminAddress);
-
- const contract = new StellarSDK.Contract(vaultContractId);
-
- const transaction = new StellarSDK.TransactionBuilder(sourceAccount, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(
- contract.call(
- "availability_for_exchange",
- StellarSDK.nativeToScVal(new StellarSDK.Address(adminAddress), {
- type: "address",
- }),
- StellarSDK.nativeToScVal(enabled, { type: "bool" }),
- ),
- )
- .setTimeout(300)
- .build();
-
- const preparedTransaction = await server.prepareTransaction(transaction);
- const xdr = preparedTransaction.toXDR();
-
- return NextResponse.json({
- success: true,
- xdr,
- message:
- "Transaction built successfully. Sign with wallet and submit to network.",
- });
- } catch (error) {
- console.error("Availability for exchange transaction build error:", error);
- const { message, details } = extractContractError(error, "vault");
- return new Response(
- JSON.stringify({
- error: message,
- details: details,
- }),
- { status: 500 },
- );
- }
-}
\ No newline at end of file
diff --git a/apps/investor-tokenization/src/app/api/vault-contract/claim/route.ts b/apps/investor-tokenization/src/app/api/vault-contract/claim/route.ts
deleted file mode 100644
index 81ff783..0000000
--- a/apps/investor-tokenization/src/app/api/vault-contract/claim/route.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import * as StellarSDK from "@stellar/stellar-sdk";
-import { NextResponse } from "next/server";
-import { extractContractError } from "@/lib/contractErrorHandler";
-
-const RPC_URL = "https://soroban-testnet.stellar.org";
-
-export async function POST(request: Request) {
- const data = await request.json();
- const { vaultContractId, beneficiaryAddress } = data ?? {};
-
- if (!vaultContractId || !beneficiaryAddress) {
- return new Response(
- JSON.stringify({
- error: "Missing required fields",
- details: "vaultContractId and beneficiaryAddress are required",
- }),
- { status: 400 },
- );
- }
-
- try {
- const server = new StellarSDK.rpc.Server(RPC_URL);
- const sourceAccount = await server.getAccount(beneficiaryAddress);
-
- const contract = new StellarSDK.Contract(vaultContractId);
-
- const transaction = new StellarSDK.TransactionBuilder(sourceAccount, {
- fee: StellarSDK.BASE_FEE,
- networkPassphrase: StellarSDK.Networks.TESTNET,
- })
- .addOperation(
- contract.call(
- "claim",
- StellarSDK.nativeToScVal(new StellarSDK.Address(beneficiaryAddress), {
- type: "address",
- }),
- ),
- )
- .setTimeout(300)
- .build();
-
- const preparedTransaction = await server.prepareTransaction(transaction);
- const xdr = preparedTransaction.toXDR();
-
- return NextResponse.json({
- success: true,
- xdr,
- message:
- "Transaction built successfully. Sign with wallet and submit to network.",
- });
- } catch (error) {
- console.error("Claim transaction build error:", error);
- const { message, details } = extractContractError(error, "vault");
- return new Response(
- JSON.stringify({
- error: message,
- details: details,
- }),
- { status: 500 },
- );
- }
-}
\ No newline at end of file
diff --git a/apps/investor-tokenization/src/app/my-investments/page.tsx b/apps/investor-tokenization/src/app/my-investments/page.tsx
index ae82820..2a1e06c 100644
--- a/apps/investor-tokenization/src/app/my-investments/page.tsx
+++ b/apps/investor-tokenization/src/app/my-investments/page.tsx
@@ -24,6 +24,8 @@ function toCampaign(inv: InvestmentFromApi): Campaign {
investedAmount: Number(inv.usdcAmount),
currency: "USDC",
vaultId: inv.campaign.vaultId ?? null,
+ escrowId: inv.campaign.escrowId,
+ poolSize: Number(inv.campaign.poolSize),
};
}
diff --git a/apps/investor-tokenization/src/features/investments/services/investment.service.ts b/apps/investor-tokenization/src/features/investments/services/investment.service.ts
index 99ca8db..462d112 100644
--- a/apps/investor-tokenization/src/features/investments/services/investment.service.ts
+++ b/apps/investor-tokenization/src/features/investments/services/investment.service.ts
@@ -46,6 +46,7 @@ export type InvestmentFromApi = {
description: string | null;
status: string;
escrowId: string;
+ poolSize: number;
tokenFactoryId: string | null;
tokenSaleId: string | null;
vaultId: string | null;
diff --git a/apps/investor-tokenization/src/features/roi/components/campaign-card.tsx b/apps/investor-tokenization/src/features/roi/components/campaign-card.tsx
index eda3b34..5cea159 100644
--- a/apps/investor-tokenization/src/features/roi/components/campaign-card.tsx
+++ b/apps/investor-tokenization/src/features/roi/components/campaign-card.tsx
@@ -1,16 +1,23 @@
"use client";
import Link from "next/link";
+import { useQuery } from "@tanstack/react-query";
import { Badge } from "@tokenization/ui/badge";
import { Button } from "@tokenization/ui/button";
import { CampaignCard as SharedCampaignCard } from "@tokenization/ui/campaign-card";
import { cn } from "@tokenization/shared/lib/utils";
-import { ExternalLink, FileText } from "lucide-react";
+import {
+ Banknote,
+ CheckCircle,
+ Circle,
+ ExternalLink,
+ FileText,
+} from "lucide-react";
+import { useGetEscrowFromIndexerByContractIds } from "@trustless-work/escrow";
+import type { MultiReleaseMilestone } from "@trustless-work/escrow/types";
import type { Campaign } from "../types/campaign.types";
import { CAMPAIGN_STATUS_CONFIG } from "../constants/campaign-status";
-
-const ESCROW_EXPLORER_URL =
- "https://stellar.expert/explorer/testnet/contract/CBBTYM6SM5KATWKLNXRUOGRVVGA762EZTB6LE7XEKZAX6VHVF7SYGIFO";
+import { fromStroops } from "@/utils/adjustedAmounts";
interface CampaignCardProps {
campaign: Campaign;
@@ -18,21 +25,80 @@ interface CampaignCardProps {
}
export function CampaignCard({ campaign, onClaimRoi }: CampaignCardProps) {
- const { title, description, status, id, loansCompleted, investedAmount, currency } = campaign;
+ const { title, description, status, id, escrowId, poolSize } = campaign;
const statusCfg = CAMPAIGN_STATUS_CONFIG[status];
- const formattedInvested = investedAmount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ const escrowExplorerUrl = `https://stellar.expert/explorer/testnet/contract/${escrowId}`;
+
+ const { getEscrowByContractIds } = useGetEscrowFromIndexerByContractIds();
+
+ type EscrowFromIndexer = {
+ milestones?: MultiReleaseMilestone[];
+ [key: string]: unknown;
+ };
+
+ const { data: escrowData } = useQuery({
+ queryKey: ["escrow", escrowId],
+ queryFn: async (): Promise => {
+ const data = (await getEscrowByContractIds({
+ contractIds: [escrowId],
+ validateOnChain: true,
+ })) as unknown;
+
+ if (!Array.isArray(data) || data.length === 0) {
+ return null;
+ }
+
+ return data[0] as EscrowFromIndexer;
+ },
+ enabled: !!escrowId,
+ staleTime: 1000 * 60 * 5,
+ });
+
+ const allMilestones = (escrowData?.milestones ?? []) as MultiReleaseMilestone[];
+ const visibleMilestones = allMilestones.slice(1);
+ const assigned = allMilestones.reduce(
+ (sum, m) => sum + fromStroops((m.amount as number) ?? 0),
+ 0,
+ );
+ const loansCompleted = visibleMilestones.filter(
+ (m) => m.status === "Approved",
+ ).length;
+ const totalLoans = visibleMilestones.length;
+ const progressValue =
+ totalLoans > 0 ? Math.min(100, (loansCompleted / totalLoans) * 100) : 0;
return (
- {statusCfg.label}
-
+ <>
+
+
+ {statusCfg.label}
+
+
+
+
+ >
}
actions={
status === "CLAIMABLE" ? (
@@ -47,18 +113,48 @@ export function CampaignCard({ campaign, onClaimRoi }: CampaignCardProps) {
) : null
}
footer={
-
+
+
+ Pool Size: USDC{" "}
+ {assigned.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ })}{" "}
+ / USDC{" "}
+ {poolSize.toLocaleString("en-US", { minimumFractionDigits: 2 })}
+
+
}
- progress={{ label: "Loans Completed", value: loansCompleted }}
- />
+ progress={{ label: "Loans Completed", value: progressValue }}
+ >
+ {visibleMilestones.length > 0 ? (
+ <>
+
+ Loans
+
+
+ {visibleMilestones.map((m, i) => (
+ -
+ {m.flags?.approved ? (
+
+ ) : m.flags?.released ? (
+
+ ) : (
+
+ )}
+
+ {m.description || `Loan ${i + 1}`}
+
+ {m.amount} USDC
+
+ ))}
+
+ >
+ ) : (
+ No loans available.
+ )}
+
);
}
diff --git a/apps/investor-tokenization/src/features/roi/data/mock-campaigns.ts b/apps/investor-tokenization/src/features/roi/data/mock-campaigns.ts
deleted file mode 100644
index 302c581..0000000
--- a/apps/investor-tokenization/src/features/roi/data/mock-campaigns.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Campaign } from "../types/campaign.types";
-
-export const mockCampaigns: Campaign[] = [
- {
- id: "1",
- title: "Coffee Producers Cooperative",
- description:
- "Expanding sustainable harvest infrastructure in Antioquia. This project aims to implement water-saving processing systems for 50 local families.",
- status: "ACTIVE",
- loansCompleted: 10,
- investedAmount: 25000,
- currency: "USD",
- vaultId: null,
- },
- {
- id: "2",
- title: "Artisan Ceramic Collective",
- description:
- "Supporting traditional pottery techniques and new kiln installations. The collective brings together 30 artisans from the region to scale production and reach new markets.",
- status: "ACTIVE",
- loansCompleted: 8,
- investedAmount: 10000,
- currency: "USD",
- vaultId: null,
- },
- {
- id: "3",
- title: "Urban Agriculture Network",
- description:
- "Rooftop and community garden expansion in Medellín. This initiative creates green jobs and improves food security through urban farming training and shared infrastructure.",
- status: "CLAIMABLE",
- loansCompleted: 12,
- investedAmount: 50000,
- currency: "USD",
- vaultId: null,
- },
-];
diff --git a/apps/investor-tokenization/src/features/roi/types/campaign.types.ts b/apps/investor-tokenization/src/features/roi/types/campaign.types.ts
index 7352b78..e62582c 100644
--- a/apps/investor-tokenization/src/features/roi/types/campaign.types.ts
+++ b/apps/investor-tokenization/src/features/roi/types/campaign.types.ts
@@ -35,4 +35,6 @@ export type Campaign = {
investedAmount: number;
currency: string;
vaultId: string | null;
+ escrowId: string;
+ poolSize: number;
};
diff --git a/apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx b/apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx
index 8989348..0de816d 100644
--- a/apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx
+++ b/apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx
@@ -32,7 +32,8 @@ import { MultiReleaseMilestone } from "@trustless-work/escrow";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { fromStroops } from "@/utils/adjustedAmounts";
-import axios from "axios";
+import { httpClient } from "@/lib/httpClient";
+import { Networks, rpc, TransactionBuilder } from "@stellar/stellar-sdk";
type InvestFormValues = {
amount: number;
@@ -67,6 +68,11 @@ export function InvestDialog({
const onSubmit = async (values: InvestFormValues) => {
setErrorMessage(null);
+
+ const server = new rpc.Server(
+ "https://soroban-testnet.stellar.org",
+ );
+
if (!walletAddress) {
setErrorMessage("Please connect your wallet to continue.");
return;
@@ -83,27 +89,6 @@ export function InvestDialog({
setSubmitting(true);
try {
- // Ensure USDC trustline exists before buying
- const trustlineRes = await axios.post("/api/trustline/add", {
- address: walletAddress,
- });
-
- if (trustlineRes.data?.success && trustlineRes.data?.xdr) {
- const signedTrustlineTx = await signTransaction({
- unsignedTransaction: trustlineRes.data.xdr,
- address: walletAddress,
- });
- const sender = new SendTransactionService();
- const trustlineResult = await sender.sendTransaction({
- signedXdr: signedTrustlineTx,
- });
- if (trustlineResult.status !== "SUCCESS") {
- throw new Error(
- trustlineResult.message ?? "Failed to add USDC trustline.",
- );
- }
- }
-
const tokenService = new TokenService();
const payload: BuyTokenPayload = {
@@ -127,25 +112,23 @@ export function InvestDialog({
address: walletAddress,
});
- const sender = new SendTransactionService();
- const submitResponse = await sender.sendTransaction({
- signedXdr: signedTxXdr,
- });
+ const tx = TransactionBuilder.fromXDR(signedTxXdr ?? "", Networks.TESTNET);
- if (submitResponse.status !== "SUCCESS") {
+ const send = await server.sendTransaction(tx);
+ if (send.status === "ERROR") {
throw new Error(
- submitResponse.message ?? "Transaction submission failed."
+ `Soroban error: ${JSON.stringify(send.errorResult)}`,
);
}
- if (selected.campaignId && submitResponse.hash) {
+ if (selected.campaignId && send.hash) {
try {
await createInvestment({
campaignId: selected.campaignId,
investorAddress: walletAddress,
usdcAmount: values.amount,
tokenAmount: values.amount,
- txHash: submitResponse.hash,
+ txHash: send.hash ?? "",
});
} catch (dbError) {
console.error("Failed to save investment to database:", dbError);
@@ -228,131 +211,131 @@ export function InvestDialog({
Invest