diff --git a/packages/token-app/app/api/wallet/etf/mint-recipe/route.ts b/packages/token-app/app/api/wallet/etf/mint-recipe/route.ts
new file mode 100644
index 0000000..761dfa1
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/mint-recipe/route.ts
@@ -0,0 +1,118 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkForParty,
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+ getSdkForParty,
+ mintRecipeTemplateId,
+ ActiveContractResponse,
+} from "@denotecapital/token-sdk";
+
+interface MintRecipeParams {
+ issuer: string;
+ instrumentId: string;
+ authorizedMinters: string[];
+ composition: string;
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const issuer = searchParams.get("issuer");
+
+ if (!issuer) {
+ return NextResponse.json(
+ { error: "Missing issuer" },
+ { status: 400 }
+ );
+ }
+
+ const sdk = await getSdkForParty(issuer);
+ const ledger = sdk.userLedger!;
+ const end = await ledger.ledgerEnd();
+
+ const activeContracts = (await ledger.activeContracts({
+ offset: end.offset,
+ filterByParty: true,
+ parties: [issuer],
+ templateIds: [mintRecipeTemplateId],
+ })) as ActiveContractResponse[];
+
+ const recipes = activeContracts.map((contract) => {
+ const jsActive = contract.contractEntry.JsActiveContract;
+ if (!jsActive) return null;
+
+ const createArg = jsActive.createdEvent.createArgument;
+ const contractId = jsActive.createdEvent.contractId;
+
+ return {
+ contractId,
+ issuer: createArg.issuer,
+ instrumentId: createArg.instrumentId,
+ authorizedMinters: createArg.authorizedMinters,
+ composition: createArg.composition,
+ };
+ });
+
+ const validRecipes = recipes.filter((recipe) => recipe !== null);
+
+ return NextResponse.json({ recipes: validRecipes });
+ } catch (error) {
+ console.error("Error getting mint recipes:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const { issuer, instrumentId, authorizedMinters, composition, seed } =
+ await request.json();
+
+ if (
+ !issuer ||
+ !instrumentId ||
+ !authorizedMinters ||
+ !composition ||
+ !seed
+ ) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ if (
+ !Array.isArray(authorizedMinters) ||
+ authorizedMinters.length === 0
+ ) {
+ return NextResponse.json(
+ { error: "authorizedMinters must be a non-empty array" },
+ { status: 400 }
+ );
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ issuer,
+ keyPair
+ );
+
+ await wrappedSdk.etf.mintRecipe.create({
+ issuer,
+ instrumentId,
+ authorizedMinters,
+ composition,
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error creating mint recipe:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/mint-request/accept/route.ts b/packages/token-app/app/api/wallet/etf/mint-request/accept/route.ts
new file mode 100644
index 0000000..85b990a
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/mint-request/accept/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+} from "@denotecapital/token-sdk";
+
+export async function POST(request: NextRequest) {
+ try {
+ const { contractId, issuer, seed } = await request.json();
+
+ if (!contractId || !issuer || !seed) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ issuer,
+ keyPair
+ );
+
+ await wrappedSdk.etf.mintRequest.accept(contractId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error accepting ETF mint request:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/mint-request/decline/route.ts b/packages/token-app/app/api/wallet/etf/mint-request/decline/route.ts
new file mode 100644
index 0000000..f6c34a9
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/mint-request/decline/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+} from "@denotecapital/token-sdk";
+
+export async function POST(request: NextRequest) {
+ try {
+ const { contractId, issuer, seed } = await request.json();
+
+ if (!contractId || !issuer || !seed) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ issuer,
+ keyPair
+ );
+
+ await wrappedSdk.etf.mintRequest.decline(contractId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error declining ETF mint request:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/mint-request/route.ts b/packages/token-app/app/api/wallet/etf/mint-request/route.ts
new file mode 100644
index 0000000..e8f2b45
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/mint-request/route.ts
@@ -0,0 +1,145 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkForParty,
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+ getSdkForParty,
+ etfMintRequestTemplateId,
+ ActiveContractResponse,
+} from "@denotecapital/token-sdk";
+
+interface EtfMintRequestParams {
+ mintRecipeCid: string;
+ requester: string;
+ amount: number;
+ transferInstructionCids: string[];
+ issuer: string;
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const partyId = searchParams.get("partyId");
+ const issuer = searchParams.get("issuer");
+
+ if (!partyId || !issuer) {
+ return NextResponse.json(
+ { error: "Missing partyId or issuer" },
+ { status: 400 }
+ );
+ }
+
+ const wrappedSdk = await getWrappedSdkForParty(partyId);
+ const contractIds = await wrappedSdk.etf.mintRequest.getAll(issuer);
+
+ const sdk = await getSdkForParty(partyId);
+ const ledger = sdk.userLedger!;
+ const end = await ledger.ledgerEnd();
+
+ const activeContracts = (await ledger.activeContracts({
+ offset: end.offset,
+ filterByParty: true,
+ parties: [partyId],
+ templateIds: [etfMintRequestTemplateId],
+ })) as ActiveContractResponse[];
+
+ const requests = contractIds.map((contractId: string) => {
+ const contract = activeContracts.find(
+ (c) =>
+ c.contractEntry.JsActiveContract?.createdEvent
+ .contractId === contractId
+ );
+ if (!contract?.contractEntry.JsActiveContract) {
+ return null;
+ }
+
+ const jsActive = contract.contractEntry.JsActiveContract;
+ const createArg = jsActive.createdEvent.createArgument;
+
+ return {
+ contractId,
+ mintRecipeCid: createArg.mintRecipeCid,
+ requester: createArg.requester,
+ amount: createArg.amount,
+ transferInstructionCids: createArg.transferInstructionCids,
+ issuer: createArg.issuer,
+ };
+ });
+
+ const validRequests = requests.filter((req) => req !== null);
+
+ return NextResponse.json({ requests: validRequests });
+ } catch (error) {
+ console.error("Error getting ETF mint requests:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const {
+ mintRecipeCid,
+ requester,
+ amount,
+ transferInstructionCids,
+ issuer,
+ seed,
+ } = await request.json();
+
+ if (
+ !mintRecipeCid ||
+ !requester ||
+ !amount ||
+ !transferInstructionCids ||
+ !issuer ||
+ !seed
+ ) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ if (
+ !Array.isArray(transferInstructionCids) ||
+ transferInstructionCids.length === 0
+ ) {
+ return NextResponse.json(
+ { error: "transferInstructionCids must be a non-empty array" },
+ { status: 400 }
+ );
+ }
+
+ if (typeof amount !== "number" || amount <= 0) {
+ return NextResponse.json(
+ { error: "amount must be a positive number" },
+ { status: 400 }
+ );
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ requester,
+ keyPair
+ );
+
+ await wrappedSdk.etf.mintRequest.create({
+ mintRecipeCid,
+ requester,
+ amount,
+ transferInstructionCids,
+ issuer,
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error creating ETF mint request:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/mint-request/withdraw/route.ts b/packages/token-app/app/api/wallet/etf/mint-request/withdraw/route.ts
new file mode 100644
index 0000000..290f650
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/mint-request/withdraw/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+} from "@denotecapital/token-sdk";
+
+export async function POST(request: NextRequest) {
+ try {
+ const { contractId, requester, seed } = await request.json();
+
+ if (!contractId || !requester || !seed) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ requester,
+ keyPair
+ );
+
+ await wrappedSdk.etf.mintRequest.withdraw(contractId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error withdrawing ETF mint request:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/portfolio-composition/route.ts b/packages/token-app/app/api/wallet/etf/portfolio-composition/route.ts
new file mode 100644
index 0000000..d2ecca0
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/portfolio-composition/route.ts
@@ -0,0 +1,99 @@
+import { NextRequest, NextResponse } from "next/server";
+import {
+ getWrappedSdkForParty,
+ getWrappedSdkWithKeyPairForParty,
+ keyPairFromSeed,
+} from "@denotecapital/token-sdk";
+
+export async function GET(request: NextRequest) {
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const owner = searchParams.get("owner");
+
+ if (!owner) {
+ return NextResponse.json(
+ { error: "Missing owner" },
+ { status: 400 }
+ );
+ }
+
+ const wrappedSdk = await getWrappedSdkForParty(owner);
+ const compositionCids =
+ await wrappedSdk.etf.portfolioComposition.getAll();
+
+ const compositions = await Promise.all(
+ compositionCids.map(async (cid) => {
+ const details = await wrappedSdk.etf.portfolioComposition.get(
+ cid
+ );
+ return {
+ contractId: cid,
+ ...details,
+ };
+ })
+ );
+
+ return NextResponse.json({ compositions });
+ } catch (error) {
+ console.error("Error getting portfolio compositions:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const { owner, name, items, seed } = await request.json();
+
+ if (!owner || !name || !items || !seed) {
+ return NextResponse.json(
+ { error: "Missing required parameters" },
+ { status: 400 }
+ );
+ }
+
+ if (!Array.isArray(items) || items.length === 0) {
+ return NextResponse.json(
+ { error: "Items must be a non-empty array" },
+ { status: 400 }
+ );
+ }
+
+ for (const item of items) {
+ if (
+ !item.instrumentId ||
+ !item.instrumentId.admin ||
+ !item.instrumentId.id ||
+ typeof item.weight !== "number" ||
+ item.weight <= 0
+ ) {
+ return NextResponse.json(
+ { error: "Invalid portfolio item structure" },
+ { status: 400 }
+ );
+ }
+ }
+
+ const keyPair = keyPairFromSeed(seed);
+ const wrappedSdk = await getWrappedSdkWithKeyPairForParty(
+ owner,
+ keyPair
+ );
+
+ await wrappedSdk.etf.portfolioComposition.create({
+ owner,
+ name,
+ items,
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Error creating portfolio composition:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/etf/setup/route.ts b/packages/token-app/app/api/wallet/etf/setup/route.ts
new file mode 100644
index 0000000..b87a3c9
--- /dev/null
+++ b/packages/token-app/app/api/wallet/etf/setup/route.ts
@@ -0,0 +1,16 @@
+import { NextRequest, NextResponse } from "next/server";
+import { etfSetup } from "@denotecapital/token-sdk";
+
+export async function POST(_request: NextRequest) {
+ try {
+ const setupResult = await etfSetup();
+
+ return NextResponse.json(setupResult);
+ } catch (error) {
+ console.error("Error running ETF setup:", error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : "Unknown error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/packages/token-app/app/api/wallet/transfer-instruction/route.ts b/packages/token-app/app/api/wallet/transfer-instruction/route.ts
index 0d23e6b..c06d68d 100644
--- a/packages/token-app/app/api/wallet/transfer-instruction/route.ts
+++ b/packages/token-app/app/api/wallet/transfer-instruction/route.ts
@@ -44,7 +44,10 @@ export async function GET(request: NextRequest) {
};
const contractId = jsActive.createdEvent.contractId;
- if (createArg.transfer?.receiver !== partyId) {
+ if (
+ createArg.transfer?.sender !== partyId &&
+ createArg.transfer?.receiver !== partyId
+ ) {
return null;
}
diff --git a/packages/token-app/app/etf/page.tsx b/packages/token-app/app/etf/page.tsx
new file mode 100644
index 0000000..617eec5
--- /dev/null
+++ b/packages/token-app/app/etf/page.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useState } from "react";
+import { ConnectionStatus } from "@/components/ConnectionStatus";
+import { EtfSetupSection } from "@/components/EtfSetupSection";
+import { EtfCustodianView } from "@/components/EtfCustodianView";
+import { EtfUserView } from "@/components/EtfUserView";
+import { Separator } from "@/components/ui/separator";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+const PARTIES = ["custodian", "alice"] as const;
+
+export default function EtfPage() {
+ const [selectedParty, setSelectedParty] = useState("custodian");
+ const [partyIds, setPartyIds] = useState>({
+ custodian: null,
+ alice: null,
+ });
+ const [setupComplete, setSetupComplete] = useState(false);
+
+ return (
+
+
+
+
+
+ ETF Management
+
+
+
+
+
+ {PARTIES.map((party) => (
+ setSelectedParty(party)}
+ className={cn(
+ "capitalize",
+ selectedParty === party && "shadow-sm"
+ )}
+ >
+ {party}
+
+ ))}
+
+
+
+
+
+ {!setupComplete && (
+ {
+ setSetupComplete(true);
+ setPartyIds({
+ custodian: result.parties.custodian,
+ alice: result.parties.alice,
+ });
+ }}
+ />
+ )}
+
+ {setupComplete &&
+ (selectedParty === "custodian" ? (
+
+ ) : (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/token-app/components/BondUserView.tsx b/packages/token-app/components/BondUserView.tsx
index 3d1f210..1b2ef76 100644
--- a/packages/token-app/components/BondUserView.tsx
+++ b/packages/token-app/components/BondUserView.tsx
@@ -8,7 +8,10 @@ import {
useLifecycleClaimRequest,
useLifecycleInstruction,
} from "@/lib/queries/bondLifecycle";
-import type { BondLifecycleInstruction } from "@denotecapital/token-sdk";
+import type {
+ BondLifecycleInstruction,
+ TokenBalance,
+} from "@denotecapital/token-sdk";
import { useBondInstruments } from "@/lib/queries/bondInstruments";
import { useQueryClient } from "@tanstack/react-query";
import { useTransferInstruction } from "@/lib/queries/transferInstruction";
@@ -359,7 +362,8 @@ export function BondUserView({
Bonds
- {selectedBalance.total || 0}
+ {(selectedBalance as TokenBalance)
+ .total || 0}
)}
@@ -369,7 +373,8 @@ export function BondUserView({
Currency
- {currencyBalance.total || 0}
+ {(currencyBalance as TokenBalance)
+ .total || 0}
)}
diff --git a/packages/token-app/components/EtfCustodianView.tsx b/packages/token-app/components/EtfCustodianView.tsx
new file mode 100644
index 0000000..253d0b6
--- /dev/null
+++ b/packages/token-app/components/EtfCustodianView.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { useState } from "react";
+import { useTokenFactory } from "@/lib/queries/tokenFactory";
+import { useAllBalancesForAllInstruments } from "@/lib/queries/allBalances";
+import { EtfPortfolioComposition } from "./EtfPortfolioComposition";
+import { EtfMintRecipe } from "./EtfMintRecipe";
+import { EtfMintRequestManagement } from "./EtfMintRequestManagement";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Loader2 } from "lucide-react";
+
+interface EtfCustodianViewProps {
+ partyId: string;
+ partyName: string;
+ allPartyIds: Record;
+}
+
+export function EtfCustodianView({
+ partyId,
+ partyName,
+ allPartyIds,
+}: EtfCustodianViewProps) {
+ const [selectedInstrumentForBalances, setSelectedInstrumentForBalances] =
+ useState(null);
+
+ const tokenFactory = useTokenFactory(partyId);
+ const instruments = tokenFactory.getInstruments.data?.instruments || [];
+
+ const {
+ allBalances,
+ isLoadingBalances,
+ hasBalanceError,
+ balanceQueries,
+ setupInstruments,
+ } = useAllBalancesForAllInstruments(partyId, instruments);
+
+ const allBalancesList = selectedInstrumentForBalances
+ ? balanceQueries[
+ setupInstruments.findIndex(
+ (inst) => inst.instrumentId === selectedInstrumentForBalances
+ )
+ ]?.data?.balances ?? []
+ : allBalances;
+
+ return (
+
+
+
+ Minted Tokens Summary
+
+ View token balances by instrument
+
+
+
+ {setupInstruments.length > 0 && (
+
+
+ Select Instrument
+
+
+ setSelectedInstrumentForBalances(
+ e.target.value || null
+ )
+ }
+ className="w-full px-3 py-2 text-sm border rounded-md bg-background"
+ >
+ All Instruments
+ {setupInstruments.map((instrument) => (
+
+ {instrument.instrumentId.split("#")[1]}
+
+ ))}
+
+
+ )}
+ {isLoadingBalances ? (
+
+
+
+ ) : hasBalanceError ? (
+
+ Unable to load balances. Token factory may not be
+ set up yet.
+
+ ) : allBalancesList.length === 0 ? (
+
+ {selectedInstrumentForBalances
+ ? "No tokens minted yet for this instrument"
+ : "No tokens minted yet"}
+
+ ) : (
+
+ {allBalancesList.map((balance) => {
+ const instrumentName =
+ selectedInstrumentForBalances
+ ? selectedInstrumentForBalances.split(
+ "#"
+ )[1]
+ : null;
+
+ return (
+
+
+
+ {balance.party}
+
+ {instrumentName && (
+
+ {instrumentName}
+
+ )}
+
+
+
+ {balance.total} tokens
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+ {/* TODO: Add ETF burn section */}
+
+ );
+}
diff --git a/packages/token-app/components/EtfMintRecipe.tsx b/packages/token-app/components/EtfMintRecipe.tsx
new file mode 100644
index 0000000..a6f4ead
--- /dev/null
+++ b/packages/token-app/components/EtfMintRecipe.tsx
@@ -0,0 +1,372 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { useEtfMintRecipe, type MintRecipe } from "@/lib/queries/etfMintRecipe";
+import { useEtfPortfolioComposition } from "@/lib/queries/etfPortfolioComposition";
+import { useTokenFactory } from "@/lib/queries/tokenFactory";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface EtfMintRecipeProps {
+ partyId: string;
+ partyName: string;
+ allPartyIds: Record;
+}
+
+export function EtfMintRecipe({
+ partyId,
+ partyName,
+ allPartyIds,
+}: EtfMintRecipeProps) {
+ const [selectedInstrumentId, setSelectedInstrumentId] = useState("");
+ const [selectedComposition, setSelectedComposition] = useState("");
+ const [selectedMinters, setSelectedMinters] = useState([partyId]);
+
+ const { get: getRecipes, create: createRecipe } = useEtfMintRecipe(partyId);
+ const { get: getCompositions } = useEtfPortfolioComposition(partyId);
+ const { getInstruments } = useTokenFactory(partyId);
+
+ const recipes = getRecipes.data?.recipes ?? [];
+ const compositions = getCompositions.data?.compositions ?? [];
+ const instruments = getInstruments.data?.instruments ?? [];
+
+ const getInstrumentName = (instrumentId: string) => {
+ return instrumentId.split("#")[1];
+ };
+
+ const etfInstruments = useMemo(() => {
+ return instruments
+ .filter((inst) =>
+ getInstrumentName(inst.instrumentId).includes("ETF")
+ )
+ .map((inst) => ({
+ instrumentId: inst.instrumentId,
+ name: getInstrumentName(inst.instrumentId),
+ }));
+ }, [instruments]);
+
+ const availableParties = useMemo(() => {
+ const parties = Object.entries(allPartyIds)
+ .filter(([_, id]) => id !== null && id !== partyId)
+ .map(([name, id]) => ({ name, id: id! }));
+ return parties;
+ }, [allPartyIds, partyId]);
+
+ const handleToggleMinter = (partyIdToToggle: string) => {
+ if (partyIdToToggle === partyId) return;
+
+ setSelectedMinters((prev) =>
+ prev.includes(partyIdToToggle)
+ ? prev.filter((id) => id !== partyIdToToggle)
+ : [...prev, partyIdToToggle]
+ );
+ };
+
+ const getPartyName = (partyId: string) => {
+ const entry = Object.entries(allPartyIds).find(
+ ([_, id]) => id === partyId
+ );
+ return entry?.[0] ?? `${partyId.slice(0, 20)}...`;
+ };
+
+ const handleCreateRecipe = async () => {
+ if (!selectedInstrumentId) {
+ toast.error("Please select an ETF instrument");
+ return;
+ }
+ if (!selectedComposition) {
+ toast.error("Portfolio composition is required");
+ return;
+ }
+ if (selectedMinters.length === 0) {
+ toast.error("At least one authorized minter is required");
+ return;
+ }
+
+ try {
+ await createRecipe.mutateAsync({
+ issuer: partyId,
+ instrumentId: selectedInstrumentId,
+ authorizedMinters: selectedMinters,
+ composition: selectedComposition,
+ seed: partyName,
+ });
+ toast.success("Mint recipe created successfully!");
+ setSelectedComposition("");
+ setSelectedMinters([partyId]);
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create mint recipe"
+ );
+ }
+ };
+
+ return (
+
+
+
+ Create Mint Recipe
+
+ Define how ETF tokens can be minted based on a portfolio
+ composition
+
+
+
+
+
+ ETF Instrument
+
+ {getInstruments.isLoading ? (
+
+
+
+ Loading instruments...
+
+
+ ) : etfInstruments.length > 0 ? (
+
+
+
+
+
+ {etfInstruments.map((inst) => (
+
+ {inst.name}
+
+ ))}
+
+
+ ) : (
+
+
+ No ETF instrument found. Please create an
+ ETF instrument first.
+
+
+ )}
+
+
+
+
+ Portfolio Composition
+
+
+
+
+
+
+ {compositions.map((comp) => (
+
+ {comp.name}
+
+ ))}
+
+
+ {compositions.length === 0 && (
+
+ No compositions available. Create one first.
+
+ )}
+
+
+
+
+ Authorized Minters
+
+ {selectedMinters.length} selected
+
+
+
+
+
+
+ {partyName} (Custodian)
+
+
+ {partyId.slice(0, 30)}...
+
+
+
+ {availableParties.length > 0 ? (
+
+ {availableParties.map((party) => (
+
+
+ handleToggleMinter(party.id)
+ }
+ className="h-4 w-4 rounded"
+ />
+
+ {party.name}
+
+
+ ))}
+
+ ) : (
+
+ No additional parties available
+
+ )}
+
+
+
+ {createRecipe.isPending ? (
+ <>
+
+ Creating...
+ >
+ ) : (
+ "Create Mint Recipe"
+ )}
+
+
+
+
+
+
+ Existing Mint Recipes
+
+ {recipes.length === 0
+ ? "No mint recipes created yet"
+ : `${recipes.length} recipe${
+ recipes.length === 1 ? "" : "s"
+ } found`}
+
+
+
+ {getRecipes.isLoading ? (
+
+
+
+ ) : recipes.length === 0 ? (
+
+
+ No mint recipes have been created yet.
+
+
+ Create one above to get started.
+
+
+ ) : (
+
+ {recipes.map((recipe: MintRecipe) => {
+ const instrumentName = getInstrumentName(
+ recipe.instrumentId
+ );
+ const compositionName =
+ compositions.find(
+ (c) =>
+ c.contractId === recipe.composition
+ )?.name ?? "Unknown Composition";
+
+ return (
+
+
+
+ {instrumentName}
+
+
+ {recipe.instrumentId}
+
+
+
+
+
+ Portfolio Composition
+
+
+ {compositionName}
+
+
+ {recipe.composition}
+
+
+
+
+
+ Authorized Minters (
+ {
+ recipe.authorizedMinters
+ .length
+ }
+ )
+
+
+ {recipe.authorizedMinters.map(
+ (minter) => (
+
+ {getPartyName(
+ minter
+ )}
+
+ )
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/token-app/components/EtfMintRequestManagement.tsx b/packages/token-app/components/EtfMintRequestManagement.tsx
new file mode 100644
index 0000000..9f57a16
--- /dev/null
+++ b/packages/token-app/components/EtfMintRequestManagement.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import { useEtfMintRequest } from "@/lib/queries/etfMintRequest";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+
+export function EtfMintRequestManagement({
+ partyId,
+ partyName,
+}: {
+ partyId: string;
+ partyName: string;
+}) {
+ const {
+ get: getEtfMintRequest,
+ accept: acceptEtfMintRequest,
+ decline: declineEtfMintRequest,
+ } = useEtfMintRequest(partyId, partyId);
+ const mintRequests = getEtfMintRequest.data?.requests ?? [];
+
+ const handleAccept = async (contractId: string) => {
+ try {
+ await acceptEtfMintRequest.mutateAsync({
+ contractId,
+ issuer: partyId,
+ seed: partyName,
+ });
+ toast.success("ETF mint request accepted successfully!");
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to accept ETF mint request"
+ );
+ }
+ };
+
+ const handleDecline = async (contractId: string) => {
+ try {
+ await declineEtfMintRequest.mutateAsync({
+ contractId,
+ issuer: partyId,
+ seed: partyName,
+ });
+ toast.success("ETF mint request declined");
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to decline ETF mint request"
+ );
+ }
+ };
+
+ return (
+
+
+ ETF Mint Requests
+
+ Review and approve/decline ETF mint requests from users
+
+
+
+ {getEtfMintRequest.isLoading ? (
+
+
+
+ ) : (
+
+ {mintRequests.map((mintRequest) => (
+
+
+
+
+ Amount: {mintRequest.amount} ETF
+ tokens
+
+
+ Requester: {mintRequest.requester}
+
+
+ {mintRequest.contractId.slice(
+ 0,
+ 20
+ )}
+ ...
+
+
+
+
+
+ Transfer Instructions:{" "}
+ {
+ mintRequest.transferInstructionCids
+ .length
+ }
+
+
+ Mint Recipe:{" "}
+ {mintRequest.mintRecipeCid.slice(0, 20)}
+ ...
+
+
+
+
+ handleAccept(mintRequest.contractId)
+ }
+ disabled={
+ acceptEtfMintRequest.isPending
+ }
+ size="sm"
+ >
+ {acceptEtfMintRequest.isPending ? (
+
+ ) : (
+ "Accept"
+ )}
+
+
+ handleDecline(
+ mintRequest.contractId
+ )
+ }
+ disabled={
+ declineEtfMintRequest.isPending
+ }
+ variant="destructive"
+ size="sm"
+ >
+ {declineEtfMintRequest.isPending ? (
+
+ ) : (
+ "Decline"
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/packages/token-app/components/EtfPortfolioComposition.tsx b/packages/token-app/components/EtfPortfolioComposition.tsx
new file mode 100644
index 0000000..13703c2
--- /dev/null
+++ b/packages/token-app/components/EtfPortfolioComposition.tsx
@@ -0,0 +1,300 @@
+"use client";
+
+import { useState } from "react";
+import {
+ useEtfPortfolioComposition,
+ PortfolioItem,
+} from "@/lib/queries/etfPortfolioComposition";
+import { useTokenFactory } from "@/lib/queries/tokenFactory";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Loader2, Plus, Trash2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface EtfPortfolioCompositionProps {
+ partyId: string;
+ partyName: string;
+}
+
+export function EtfPortfolioComposition({
+ partyId,
+ partyName,
+}: EtfPortfolioCompositionProps) {
+ const [compositionName, setCompositionName] = useState("");
+ const [items, setItems] = useState([]);
+
+ const { get: getCompositions, create: createComposition } =
+ useEtfPortfolioComposition(partyId);
+ const { getInstruments } = useTokenFactory(partyId);
+ const instruments = getInstruments.data?.instruments ?? [];
+
+ const compositions = getCompositions.data?.compositions ?? [];
+
+ const parseInstrumentId = (instrumentId: string) => {
+ const [admin, id] = instrumentId.split("#");
+ return { admin, id };
+ };
+
+ const handleAddItem = () => {
+ if (instruments.length === 0) {
+ toast.error(
+ "No instruments available. Please create tokens first."
+ );
+ return;
+ }
+
+ setItems([
+ ...items,
+ {
+ instrumentId: parseInstrumentId(instruments[0].instrumentId),
+ weight: 1.0,
+ },
+ ]);
+ };
+
+ const handleRemoveItem = (index: number) => {
+ setItems(items.filter((_, i) => i !== index));
+ };
+
+ const handleInstrumentChange = (index: number, instrumentId: string) => {
+ setItems(
+ items.map((item, i) =>
+ i === index
+ ? { ...item, instrumentId: parseInstrumentId(instrumentId) }
+ : item
+ )
+ );
+ };
+
+ const handleWeightChange = (index: number, weight: number) => {
+ setItems(
+ items.map((item, i) => (i === index ? { ...item, weight } : item))
+ );
+ };
+
+ const handleCreateComposition = async () => {
+ if (!compositionName.trim()) {
+ toast.error("Composition name is required");
+ return;
+ }
+
+ if (items.length === 0) {
+ toast.error("At least one portfolio item is required");
+ return;
+ }
+
+ try {
+ await createComposition.mutateAsync({
+ owner: partyId,
+ name: compositionName.trim(),
+ items,
+ seed: partyName,
+ });
+ toast.success(
+ `Portfolio composition "${compositionName.trim()}" created successfully!`
+ );
+ setCompositionName("");
+ setItems([]);
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create portfolio composition"
+ );
+ }
+ };
+
+ return (
+
+
+
+ Portfolio Compositions
+
+ Create and manage portfolio compositions for ETF tokens
+
+
+
+
+
+ Composition Name
+
+ setCompositionName(e.target.value)}
+ placeholder="e.g., Three Token ETF"
+ />
+
+
+
+
+
Portfolio Items
+
+
+ Add Item
+
+
+
+ {items.map((item, index) => (
+
+
+ Instrument
+
+ handleInstrumentChange(index, value)
+ }
+ >
+
+
+
+
+ {instruments.map((inst) => (
+
+ {
+ parseInstrumentId(
+ inst.instrumentId
+ ).id
+ }
+
+ ))}
+
+
+
+
+ Weight
+
+ handleWeightChange(
+ index,
+ e.target.valueAsNumber || 0
+ )
+ }
+ />
+
+
handleRemoveItem(index)}
+ variant="ghost"
+ size="icon"
+ >
+
+
+
+ ))}
+
+
+
+ {createComposition.isPending ? (
+ <>
+
+ Creating...
+ >
+ ) : (
+ "Create Portfolio Composition"
+ )}
+
+
+
+
+
+
+ Existing Compositions
+
+ {compositions.length === 0
+ ? "No portfolio compositions created yet"
+ : `${compositions.length} composition(s) found`}
+
+
+
+ {getCompositions.isLoading ? (
+
+
+
+ ) : (
+
+ {compositions.map((composition) => (
+
+
+
+ {composition.name}
+
+
+ {composition.contractId.slice(
+ 0,
+ 20
+ )}
+ ...
+
+
+
+
+ Items ({composition.items.length}):
+
+
+ {composition.items.map(
+ (item, idx) => (
+
+ •{" "}
+
+ {
+ item
+ .instrumentId
+ .id
+ }
+ {" "}
+ (weight: {item.weight})
+
+ )
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/token-app/components/EtfSetupSection.tsx b/packages/token-app/components/EtfSetupSection.tsx
new file mode 100644
index 0000000..f5f9b58
--- /dev/null
+++ b/packages/token-app/components/EtfSetupSection.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { useMutation } from "@tanstack/react-query";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+
+interface EtfSetupSectionProps {
+ onSetupComplete?: (result: any) => void;
+}
+
+export function EtfSetupSection({ onSetupComplete }: EtfSetupSectionProps) {
+ const setupMutation = useMutation({
+ mutationFn: async () => {
+ const response = await fetch("/api/wallet/etf/setup", {
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to run ETF setup");
+ }
+
+ return response.json();
+ },
+ onSuccess: (data) => {
+ onSetupComplete?.(data);
+ toast.success("ETF setup completed successfully!");
+ },
+ onError: (error) => {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to run ETF setup"
+ );
+ },
+ });
+
+ return (
+
+
+ ETF Setup
+
+
+ setupMutation.mutate()}
+ disabled={setupMutation.isPending}
+ className="w-full"
+ >
+ {setupMutation.isPending ? (
+ <>
+
+ Setting up...
+ >
+ ) : (
+ "Setup ETF Environment"
+ )}
+
+
+
+ );
+}
diff --git a/packages/token-app/components/EtfUserView.tsx b/packages/token-app/components/EtfUserView.tsx
new file mode 100644
index 0000000..2b08792
--- /dev/null
+++ b/packages/token-app/components/EtfUserView.tsx
@@ -0,0 +1,656 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { useEtfMintRequest } from "@/lib/queries/etfMintRequest";
+import { useEtfMintRecipe } from "@/lib/queries/etfMintRecipe";
+import { useTransferInstruction } from "@/lib/queries/transferInstruction";
+import { useTransferRequest } from "@/lib/queries/transferRequest";
+import { useTokenFactory } from "@/lib/queries/tokenFactory";
+import { useBalance, TokenBalance } from "@/lib/queries/balance";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Loader2, CheckCircle2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Badge } from "@/components/ui/badge";
+
+interface EtfUserViewProps {
+ partyId: string;
+ partyName: string;
+ custodianPartyId: string;
+}
+
+export function EtfUserView({
+ partyId,
+ partyName,
+ custodianPartyId,
+}: EtfUserViewProps) {
+ const [selectedMintRecipe, setSelectedMintRecipe] = useState("");
+ const [selectedTransferInstructions, setSelectedTransferInstructions] =
+ useState([]);
+ const [amount, setAmount] = useState(0);
+ const [transferAmounts, setTransferAmounts] = useState<
+ Record
+ >({
+ Token1: 0,
+ Token2: 0,
+ Token3: 0,
+ });
+
+ const {
+ get: getEtfMintRequest,
+ create: createEtfMintRequest,
+ withdraw: withdrawEtfMintRequest,
+ } = useEtfMintRequest(partyId, custodianPartyId);
+ const { get: getMintRecipe } = useEtfMintRecipe(custodianPartyId);
+ const { get: getTransferInstruction } = useTransferInstruction(partyId);
+ const { get: getTransferRequest, create: createTransferRequest } =
+ useTransferRequest(partyId, custodianPartyId);
+ const { getInstruments, getTransferFactory } =
+ useTokenFactory(custodianPartyId);
+
+ const requests = getEtfMintRequest.data?.requests || [];
+ const recipes = getMintRecipe.data?.recipes ?? [];
+ const instructions = getTransferInstruction.data?.instructions ?? [];
+ const transferRequests = getTransferRequest.data?.requests ?? [];
+ const instruments = getInstruments.data?.instruments ?? [];
+ const transferFactory = getTransferFactory.data;
+
+ const { data: allBalances, isLoading: isLoadingBalances } =
+ useBalance(partyId);
+ const balancesRecord =
+ (allBalances as Record) || undefined;
+
+ const availableInstructions = instructions.filter(
+ (inst) => inst.transfer.receiver === custodianPartyId
+ );
+
+ const pendingTransferRequests = transferRequests.filter(
+ (req) => req.transfer.receiver === custodianPartyId
+ );
+
+ const handleCreateRequest = async () => {
+ if (
+ !selectedMintRecipe ||
+ selectedTransferInstructions.length === 0 ||
+ amount <= 0
+ ) {
+ toast.error("Please fill in all fields");
+ return;
+ }
+
+ try {
+ await createEtfMintRequest.mutateAsync({
+ mintRecipeCid: selectedMintRecipe,
+ requester: partyId,
+ amount,
+ transferInstructionCids: selectedTransferInstructions,
+ issuer: custodianPartyId,
+ seed: partyName,
+ });
+ toast.success("ETF mint request created successfully!");
+ setSelectedMintRecipe("");
+ setSelectedTransferInstructions([]);
+ setAmount(0);
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create ETF mint request"
+ );
+ }
+ };
+
+ const handleWithdrawRequest = async (contractId: string) => {
+ try {
+ await withdrawEtfMintRequest.mutateAsync({
+ contractId,
+ requester: partyId,
+ seed: partyName,
+ });
+ toast.success("ETF mint request withdrawn successfully!");
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to withdraw ETF mint request"
+ );
+ }
+ };
+
+ const toggleTransferInstruction = (instructionId: string) => {
+ setSelectedTransferInstructions((prev) =>
+ prev.includes(instructionId)
+ ? prev.filter((id) => id !== instructionId)
+ : [...prev, instructionId]
+ );
+ };
+
+ const handleCreateTransferRequest = async (instrumentId: string) => {
+ const token = instruments.find(
+ (inst) => inst.instrumentId === instrumentId
+ );
+ if (!token) return;
+
+ const tokenName = instrumentId.split("#")[1];
+ const tokenAmount = transferAmounts[tokenName];
+ const balance = balancesRecord?.[instrumentId];
+
+ if (!balance || !transferFactory?.transferFactoryCid) {
+ toast.error(
+ `Missing information for ${tokenName}. Ensure infrastructure is set up.`
+ );
+ return;
+ }
+
+ if (tokenAmount <= 0 || tokenAmount > (balance.total || 0)) {
+ toast.error(`Invalid amount for ${tokenName}`);
+ return;
+ }
+
+ if (!balance.utxos || balance.utxos.length === 0) {
+ toast.error(`No tokens available for ${tokenName}`);
+ return;
+ }
+
+ try {
+ const [, tokenName] = instrumentId.split("#");
+
+ await createTransferRequest.mutateAsync({
+ transferFactoryCid: transferFactory.transferFactoryCid,
+ expectedAdmin: custodianPartyId,
+ sender: partyId,
+ receiver: custodianPartyId,
+ amount: tokenAmount,
+ instrumentId: {
+ admin: custodianPartyId,
+ id: tokenName,
+ },
+ inputHoldingCids: [balance.utxos[0].contractId],
+ seed: partyName,
+ });
+ toast.success(`Transfer request created for ${tokenName}`);
+ setTransferAmounts((prev) => ({ ...prev, [tokenName]: 0 }));
+ } catch (error) {
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : `Failed to create transfer request for ${tokenName}`
+ );
+ }
+ };
+
+ return (
+
+
+
+ Token Balances
+
+ Your current balances for underlying tokens and ETF
+
+
+
+ {isLoadingBalances ? (
+
+
+
+ ) : (
+
+ {instruments.map((token) => {
+ const tokenName = token.instrumentId.includes(
+ "#"
+ )
+ ? token.instrumentId.split("#")[1]
+ : token.instrumentId;
+ const balance =
+ balancesRecord?.[token.instrumentId];
+ const isEtf = tokenName.includes("ETF");
+
+ return (
+
+
+
+ {tokenName}
+
+
+ {isEtf
+ ? "ETF Token"
+ : token.instrumentId}
+
+
+
+ {balance?.total || 0}
+
+
+ );
+ })}
+ {instruments.length === 0 && (
+
+ No token balances found
+
+ )}
+
+ )}
+
+
+
+
+
+ Create Transfer Requests
+
+ Create transfer requests for underlying tokens to the
+ custodian. Once accepted, they become transfer
+ instructions you can use for ETF minting.
+
+
+
+ {instruments.filter((inst) => {
+ const tokenName = inst.instrumentId.includes("#")
+ ? inst.instrumentId.split("#")[1]
+ : inst.instrumentId;
+ return !tokenName.includes("ETF");
+ }).length === 0 ? (
+
+ No underlying tokens found. Ensure setup is
+ complete.
+
+ ) : (
+ instruments
+ .filter((inst) => {
+ const tokenName = inst.instrumentId.includes(
+ "#"
+ )
+ ? inst.instrumentId.split("#")[1]
+ : inst.instrumentId;
+ return !tokenName.includes("ETF");
+ })
+ .map((token) => {
+ const tokenName = token.instrumentId.includes(
+ "#"
+ )
+ ? token.instrumentId.split("#")[1]
+ : token.instrumentId;
+ const balance =
+ balancesRecord?.[token.instrumentId];
+ const tokenAmount =
+ transferAmounts[tokenName] || 0;
+ const hasBalance = balance && balance.total > 0;
+ const isPending =
+ createTransferRequest.isPending &&
+ createTransferRequest.variables
+ ?.instrumentId.id === tokenName;
+ const pendingRequests =
+ pendingTransferRequests.filter((req) => {
+ const reqTokenName =
+ req.transfer.instrumentId.id;
+ const tokenTokenName =
+ token.instrumentId.includes("#")
+ ? token.instrumentId.split(
+ "#"
+ )[1]
+ : token.instrumentId;
+ return reqTokenName === tokenTokenName;
+ });
+
+ return (
+
+
+
+ {tokenName}
+
+
+ Balance: {balance?.total || 0}
+
+
+
+ {hasBalance && (
+
+
+
+ setTransferAmounts(
+ (prev) => ({
+ ...prev,
+ [tokenName]:
+ e.target
+ .valueAsNumber ||
+ 0,
+ })
+ )
+ }
+ placeholder="Amount"
+ className="flex-1"
+ />
+
+ handleCreateTransferRequest(
+ token.instrumentId
+ )
+ }
+ disabled={
+ !transferFactory?.transferFactoryCid ||
+ tokenAmount <= 0 ||
+ tokenAmount >
+ (balance?.total ||
+ 0) ||
+ isPending
+ }
+ >
+ {isPending ? (
+
+ ) : (
+ "Create Request"
+ )}
+
+
+ {pendingRequests.length > 0 && (
+
+ {pendingRequests.length}{" "}
+ pending request(s) -
+ waiting for custodian
+ acceptance
+
+ )}
+
+ )}
+
+ );
+ })
+ )}
+
+
+
+
+
+ ETF Mint Requests
+
+ Create requests to mint ETF tokens using underlying
+ assets
+
+
+
+
+
Mint Recipe
+
+
+
+
+
+ {recipes.length === 0 ? (
+
+ No mint recipes available
+
+ ) : (
+ recipes.map((recipe) => {
+ const displayName =
+ recipe.instrumentId.includes("#")
+ ? recipe.instrumentId.split(
+ "#"
+ )[1]
+ : recipe.instrumentId;
+ return (
+
+ {displayName} (
+ {recipe.contractId.slice(0, 20)}
+ ...)
+
+ );
+ })
+ )}
+
+
+ {recipes.length === 0 && (
+
+ No mint recipes available. Ask custodian to
+ create one.
+
+ )}
+
+
+
+
Transfer Instructions
+
+ Select transfer instructions for underlying assets
+ to be used in minting
+
+ {availableInstructions.length === 0 ? (
+
+
No transfer instructions available.
+
+ Create transfer requests in the "Create
+ Transfer Requests" section above. Once the
+ custodian accepts them, they will appear
+ here as transfer instructions.
+
+ {pendingTransferRequests.length > 0 && (
+
+ {pendingTransferRequests.length}{" "}
+ transfer request(s) pending acceptance
+
+ )}
+
+ ) : (
+
+ {availableInstructions.map((instruction) => {
+ const instrumentId = `${instruction.transfer.instrumentId.admin}#${instruction.transfer.instrumentId.id}`;
+ const displayName = instrumentId.includes(
+ "#"
+ )
+ ? instrumentId.split("#")[1]
+ : instrumentId;
+ const isSelected =
+ selectedTransferInstructions.includes(
+ instruction.contractId
+ );
+ return (
+
+ toggleTransferInstruction(
+ instruction.contractId
+ )
+ }
+ className={`w-full flex items-center space-x-2 p-2 rounded border text-left ${
+ isSelected
+ ? "bg-muted border-primary"
+ : "hover:bg-muted/50"
+ }`}
+ >
+
+ {isSelected && (
+
+ )}
+
+
+
+ {displayName}
+
+
+ Amount:{" "}
+ {
+ instruction.transfer
+ .amount
+ }{" "}
+ | To:{" "}
+ {
+ instruction.transfer
+ .receiver
+ }
+
+
+
+ );
+ })}
+
+ )}
+ {selectedTransferInstructions.length > 0 && (
+
+ {selectedTransferInstructions.length}{" "}
+ instruction(s) selected
+
+ )}
+
+
+
+ Amount
+
+ setAmount(e.target.valueAsNumber || 0)
+ }
+ placeholder="e.g., 1.0"
+ />
+
+
+
+ {createEtfMintRequest.isPending ? (
+ <>
+
+ Creating...
+ >
+ ) : (
+ "Create Mint Request"
+ )}
+
+
+
+
+
+
+ Pending Mint Requests
+
+ {requests.length === 0
+ ? "No mint requests created yet"
+ : `${requests.length} request(s) found`}
+
+
+
+ {getEtfMintRequest.isLoading ? (
+
+
+
+ ) : (
+
+ {requests.map((request) => (
+
+
+
+
+ Amount: {request.amount} ETF
+ tokens
+
+
+ {request.contractId.slice(
+ 0,
+ 20
+ )}
+ ...
+
+
+
+
+ Pending
+
+
+ handleWithdrawRequest(
+ request.contractId
+ )
+ }
+ disabled={
+ withdrawEtfMintRequest.isPending
+ }
+ >
+ {withdrawEtfMintRequest.isPending ? (
+
+ ) : (
+ "Withdraw"
+ )}
+
+
+
+
+
+ Transfer Instructions:{" "}
+ {
+ request.transferInstructionCids
+ .length
+ }
+
+
+ Mint Recipe:{" "}
+ {request.mintRecipeCid.slice(0, 20)}
+ ...
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/token-app/components/UserView.tsx b/packages/token-app/components/UserView.tsx
index 7443f39..23338f9 100644
--- a/packages/token-app/components/UserView.tsx
+++ b/packages/token-app/components/UserView.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import { useBalance } from "@/lib/queries/balance";
+import { useBalance, TokenBalance } from "@/lib/queries/balance";
import { useIssuerMintRequest } from "@/lib/queries/issuerMintRequest";
import { useIssuerBurnRequest } from "@/lib/queries/issuerBurnRequest";
import { useTokenFactory } from "@/lib/queries/tokenFactory";
@@ -51,13 +51,15 @@ export function UserView({
(i) => i.instrumentId === selectedInstrumentId
);
- const { data: balance } = useBalance(
+ const { data: balanceData } = useBalance(
partyId,
custodianPartyId && selectedInstrumentId
? { admin: custodianPartyId, id: selectedInstrumentId }
: null
);
+ const balance = balanceData as TokenBalance | undefined;
+
const transferFactory = tokenFactoryQuery.getTransferFactory.data;
const issuerMintRequest = useIssuerMintRequest(partyId, custodianPartyId);
diff --git a/packages/token-app/lib/queries/allBalances.ts b/packages/token-app/lib/queries/allBalances.ts
index 67f33d1..b773938 100644
--- a/packages/token-app/lib/queries/allBalances.ts
+++ b/packages/token-app/lib/queries/allBalances.ts
@@ -95,5 +95,6 @@ export function useAllBalancesForAllInstruments(
isLoadingBalances,
hasBalanceError,
setupInstruments,
+ balanceQueries,
};
}
diff --git a/packages/token-app/lib/queries/balance.ts b/packages/token-app/lib/queries/balance.ts
index 437aa63..cab5623 100644
--- a/packages/token-app/lib/queries/balance.ts
+++ b/packages/token-app/lib/queries/balance.ts
@@ -9,7 +9,7 @@ export function useBalance(
owner: string | null,
instrumentId?: { admin: string; id: string } | null
) {
- return useQuery({
+ return useQuery | TokenBalance>({
queryKey: ["balances", owner, instrumentId],
queryFn: async () => {
if (!owner) throw new Error("Owner required");
@@ -28,7 +28,7 @@ export function useBalance(
throw new Error(error.error || "Failed to get balance");
}
- return response.json() as Promise;
+ return response.json();
},
enabled: !!owner,
refetchInterval: 5000,
diff --git a/packages/token-app/lib/queries/etfMintRecipe.ts b/packages/token-app/lib/queries/etfMintRecipe.ts
new file mode 100644
index 0000000..53f9a9e
--- /dev/null
+++ b/packages/token-app/lib/queries/etfMintRecipe.ts
@@ -0,0 +1,74 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+
+export interface MintRecipe {
+ contractId: string;
+ issuer: string;
+ instrumentId: string;
+ authorizedMinters: string[];
+ composition: string;
+}
+
+export function useEtfMintRecipe(issuer: string | null) {
+ const queryClient = useQueryClient();
+
+ const get = useQuery({
+ queryKey: ["etfMintRecipes", issuer],
+ queryFn: async () => {
+ if (!issuer) {
+ throw new Error("Issuer required");
+ }
+
+ const params = new URLSearchParams({
+ issuer,
+ });
+
+ const response = await fetch(
+ `/api/wallet/etf/mint-recipe?${params}`
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to get mint recipes");
+ }
+
+ return response.json() as Promise<{
+ recipes: MintRecipe[];
+ }>;
+ },
+ enabled: !!issuer,
+ refetchInterval: 5000,
+ });
+
+ const create = useMutation({
+ mutationFn: async (params: {
+ issuer: string;
+ instrumentId: string;
+ authorizedMinters: string[];
+ composition: string;
+ seed: string;
+ }) => {
+ const response = await fetch("/api/wallet/etf/mint-recipe", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to create mint recipe");
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["etfMintRecipes", variables.issuer],
+ });
+ },
+ });
+
+ return {
+ get,
+ create,
+ };
+}
diff --git a/packages/token-app/lib/queries/etfMintRequest.ts b/packages/token-app/lib/queries/etfMintRequest.ts
new file mode 100644
index 0000000..b8778cc
--- /dev/null
+++ b/packages/token-app/lib/queries/etfMintRequest.ts
@@ -0,0 +1,184 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+
+export interface EtfMintRequest {
+ contractId: string;
+ mintRecipeCid: string;
+ requester: string;
+ amount: number;
+ transferInstructionCids: string[];
+ issuer: string;
+}
+
+export function useEtfMintRequest(
+ partyId: string | null,
+ issuer: string | null
+) {
+ const queryClient = useQueryClient();
+
+ const get = useQuery({
+ queryKey: ["etfMintRequests", partyId, issuer],
+ queryFn: async () => {
+ if (!partyId || !issuer) {
+ throw new Error("Party ID and issuer required");
+ }
+
+ const params = new URLSearchParams({
+ partyId,
+ issuer,
+ });
+
+ const response = await fetch(
+ `/api/wallet/etf/mint-request?${params}`
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to get ETF mint requests"
+ );
+ }
+
+ return response.json() as Promise<{
+ requests: EtfMintRequest[];
+ }>;
+ },
+ enabled: !!partyId && !!issuer,
+ refetchInterval: 5000,
+ });
+
+ const create = useMutation({
+ mutationFn: async (params: {
+ mintRecipeCid: string;
+ requester: string;
+ amount: number;
+ transferInstructionCids: string[];
+ issuer: string;
+ seed: string;
+ }) => {
+ const response = await fetch("/api/wallet/etf/mint-request", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to create ETF mint request"
+ );
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: [
+ "etfMintRequests",
+ variables.requester,
+ variables.issuer,
+ ],
+ });
+ },
+ });
+
+ const accept = useMutation({
+ mutationFn: async (params: {
+ contractId: string;
+ issuer: string;
+ seed: string;
+ }) => {
+ const response = await fetch(
+ "/api/wallet/etf/mint-request/accept",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to accept ETF mint request"
+ );
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["etfMintRequests"],
+ });
+ },
+ });
+
+ const decline = useMutation({
+ mutationFn: async (params: {
+ contractId: string;
+ issuer: string;
+ seed: string;
+ }) => {
+ const response = await fetch(
+ "/api/wallet/etf/mint-request/decline",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to decline ETF mint request"
+ );
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["etfMintRequests"],
+ });
+ },
+ });
+
+ const withdraw = useMutation({
+ mutationFn: async (params: {
+ contractId: string;
+ requester: string;
+ seed: string;
+ }) => {
+ const response = await fetch(
+ "/api/wallet/etf/mint-request/withdraw",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to withdraw ETF mint request"
+ );
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["etfMintRequests"],
+ });
+ },
+ });
+
+ return {
+ get,
+ create,
+ accept,
+ decline,
+ withdraw,
+ };
+}
diff --git a/packages/token-app/lib/queries/etfPortfolioComposition.ts b/packages/token-app/lib/queries/etfPortfolioComposition.ts
new file mode 100644
index 0000000..ac45d3a
--- /dev/null
+++ b/packages/token-app/lib/queries/etfPortfolioComposition.ts
@@ -0,0 +1,87 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+
+export interface PortfolioItem {
+ instrumentId: {
+ admin: string;
+ id: string;
+ };
+ weight: number;
+}
+
+export interface PortfolioComposition {
+ contractId: string;
+ owner: string;
+ name: string;
+ items: PortfolioItem[];
+}
+
+export function useEtfPortfolioComposition(owner: string | null) {
+ const queryClient = useQueryClient();
+
+ const get = useQuery({
+ queryKey: ["etfPortfolioCompositions", owner],
+ queryFn: async () => {
+ if (!owner) {
+ throw new Error("Owner required");
+ }
+
+ const params = new URLSearchParams({
+ owner,
+ });
+
+ const response = await fetch(
+ `/api/wallet/etf/portfolio-composition?${params}`
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to get portfolio compositions"
+ );
+ }
+
+ return response.json() as Promise<{
+ compositions: PortfolioComposition[];
+ }>;
+ },
+ enabled: !!owner,
+ refetchInterval: 5000,
+ });
+
+ const create = useMutation({
+ mutationFn: async (params: {
+ owner: string;
+ name: string;
+ items: PortfolioItem[];
+ seed: string;
+ }) => {
+ const response = await fetch(
+ "/api/wallet/etf/portfolio-composition",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(
+ error.error || "Failed to create portfolio composition"
+ );
+ }
+
+ return response.json();
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["etfPortfolioCompositions", variables.owner],
+ });
+ },
+ });
+
+ return {
+ get,
+ create,
+ };
+}
diff --git a/packages/token-app/next-env.d.ts b/packages/token-app/next-env.d.ts
index c4b7818..0c7fad7 100644
--- a/packages/token-app/next-env.d.ts
+++ b/packages/token-app/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
diff --git a/packages/token-sdk/src/index.ts b/packages/token-sdk/src/index.ts
index 7315b3e..2462539 100644
--- a/packages/token-sdk/src/index.ts
+++ b/packages/token-sdk/src/index.ts
@@ -3,3 +3,4 @@ export * from "./helpers/index.js";
export * from "./types/index.js";
export * from "./wrappedSdk/index.js";
export * from "./sdkHelpers.js";
+export * from "./testScripts/etfSetup.js";
diff --git a/packages/token-sdk/src/testScripts/etfSetup.ts b/packages/token-sdk/src/testScripts/etfSetup.ts
new file mode 100644
index 0000000..60f8eaf
--- /dev/null
+++ b/packages/token-sdk/src/testScripts/etfSetup.ts
@@ -0,0 +1,205 @@
+import { signTransactionHash } from "@canton-network/wallet-sdk";
+import { getDefaultSdkAndConnect } from "../sdkHelpers.js";
+import { keyPairFromSeed } from "../helpers/keyPairFromSeed.js";
+import { getWrappedSdkWithKeyPair } from "../wrappedSdk/wrappedSdk.js";
+
+/**
+ * ETF Setup Script
+ *
+ * Automates initial setup for ETF functionality:
+ * 1. Allocates parties (custodian, alice)
+ * 2. Creates infrastructure for 3 underlying tokens
+ * 3. Mints underlying tokens to Alice (100 each)
+ *
+ * Note: Does NOT create portfolio composition or mint recipe (done in UI)
+ */
+export async function etfSetup() {
+ console.info("=== ETF Setup Script ===\n");
+
+ const custodianSdk = await getDefaultSdkAndConnect();
+ const aliceSdk = await getDefaultSdkAndConnect();
+
+ const custodianKeyPair = keyPairFromSeed("custodian");
+ const aliceKeyPair = keyPairFromSeed("alice");
+
+ const custodianLedger = custodianSdk.userLedger!;
+ const aliceLedger = aliceSdk.userLedger!;
+
+ const custodianWrappedSdk = getWrappedSdkWithKeyPair(
+ custodianSdk,
+ custodianKeyPair
+ );
+ const aliceWrappedSdk = getWrappedSdkWithKeyPair(aliceSdk, aliceKeyPair);
+
+ // === PHASE 1: PARTY ALLOCATION ===
+ console.info("1. Allocating parties...");
+
+ // Allocate Custodian (issuer/admin)
+ const custodianParty = await custodianLedger.generateExternalParty(
+ custodianKeyPair.publicKey,
+ "custodian"
+ );
+ if (!custodianParty) throw new Error("Error creating Custodian party");
+
+ const custodianSignedHash = signTransactionHash(
+ custodianParty.multiHash,
+ custodianKeyPair.privateKey
+ );
+ const custodianAllocatedParty = await custodianLedger.allocateExternalParty(
+ custodianSignedHash,
+ custodianParty
+ );
+
+ const aliceParty = await aliceLedger.generateExternalParty(
+ aliceKeyPair.publicKey,
+ "alice"
+ );
+ if (!aliceParty) throw new Error("Error creating Alice party");
+
+ const aliceSignedHash = signTransactionHash(
+ aliceParty.multiHash,
+ aliceKeyPair.privateKey
+ );
+ const aliceAllocatedParty = await aliceLedger.allocateExternalParty(
+ aliceSignedHash,
+ aliceParty
+ );
+
+ await custodianSdk.setPartyId(custodianAllocatedParty.partyId);
+ await aliceSdk.setPartyId(aliceAllocatedParty.partyId);
+
+ console.info("✓ Parties allocated:");
+ console.info(` Custodian (issuer): ${custodianAllocatedParty.partyId}`);
+ console.info(` Alice (minter): ${aliceAllocatedParty.partyId}\n`);
+
+ // === PHASE 2: INFRASTRUCTURE SETUP ===
+ console.info("2. Setting up infrastructure (underlying tokens)...");
+
+ // Instrument IDs for 3 underlying tokens
+ const instrumentId1 = custodianAllocatedParty.partyId + "#Token1";
+ const instrumentId2 = custodianAllocatedParty.partyId + "#Token2";
+ const instrumentId3 = custodianAllocatedParty.partyId + "#Token3";
+ const etfInstrumentId = custodianAllocatedParty.partyId + "#ThreeTokenETF";
+
+ // Create token rules (shared for all transfers)
+ const rulesCid = await custodianWrappedSdk.tokenRules.getOrCreate();
+ console.info(`✓ MyTokenRules created: ${rulesCid}`);
+
+ // Create token factories for underlying assets
+ const tokenFactory1Cid = await custodianWrappedSdk.tokenFactory.getOrCreate(
+ instrumentId1
+ );
+ console.info(`✓ Token1 factory created: ${tokenFactory1Cid}`);
+
+ const tokenFactory2Cid = await custodianWrappedSdk.tokenFactory.getOrCreate(
+ instrumentId2
+ );
+ console.info(`✓ Token2 factory created: ${tokenFactory2Cid}`);
+
+ const tokenFactory3Cid = await custodianWrappedSdk.tokenFactory.getOrCreate(
+ instrumentId3
+ );
+ console.info(`✓ Token3 factory created: ${tokenFactory3Cid}`);
+
+ // Create transfer factories for underlying assets
+ const transferFactory1Cid =
+ await custodianWrappedSdk.transferFactory.getOrCreate(rulesCid);
+ console.info(`✓ Transfer factory 1 created: ${transferFactory1Cid}`);
+
+ const transferFactory2Cid =
+ await custodianWrappedSdk.transferFactory.getOrCreate(rulesCid);
+ console.info(`✓ Transfer factory 2 created: ${transferFactory2Cid}`);
+
+ const transferFactory3Cid =
+ await custodianWrappedSdk.transferFactory.getOrCreate(rulesCid);
+ console.info(`✓ Transfer factory 3 created: ${transferFactory3Cid}\n`);
+
+ // Note: ETF tokens are created directly via MyMintRecipe (no factory needed)
+
+ // === PHASE 3: MINT UNDERLYING TOKENS TO ALICE ===
+ console.info("3. Minting underlying tokens to Alice (100 each)...");
+
+ // Token 1
+ await aliceWrappedSdk.issuerMintRequest.create({
+ tokenFactoryCid: tokenFactory1Cid,
+ issuer: custodianAllocatedParty.partyId,
+ receiver: aliceAllocatedParty.partyId,
+ amount: 100.0,
+ });
+ const mintRequest1Cid = await aliceWrappedSdk.issuerMintRequest.getLatest(
+ custodianAllocatedParty.partyId
+ );
+ if (!mintRequest1Cid) {
+ throw new Error("Mint request 1 not found");
+ }
+ await custodianWrappedSdk.issuerMintRequest.accept(mintRequest1Cid);
+ console.info(" ✓ Token1 minted to Alice (100.0)");
+
+ // Token 2
+ await aliceWrappedSdk.issuerMintRequest.create({
+ tokenFactoryCid: tokenFactory2Cid,
+ issuer: custodianAllocatedParty.partyId,
+ receiver: aliceAllocatedParty.partyId,
+ amount: 100.0,
+ });
+ const mintRequest2Cid = await aliceWrappedSdk.issuerMintRequest.getLatest(
+ custodianAllocatedParty.partyId
+ );
+ if (!mintRequest2Cid) {
+ throw new Error("Mint request 2 not found");
+ }
+ await custodianWrappedSdk.issuerMintRequest.accept(mintRequest2Cid);
+ console.info(" ✓ Token2 minted to Alice (100.0)");
+
+ // Token 3
+ await aliceWrappedSdk.issuerMintRequest.create({
+ tokenFactoryCid: tokenFactory3Cid,
+ issuer: custodianAllocatedParty.partyId,
+ receiver: aliceAllocatedParty.partyId,
+ amount: 100.0,
+ });
+ const mintRequest3Cid = await aliceWrappedSdk.issuerMintRequest.getLatest(
+ custodianAllocatedParty.partyId
+ );
+ if (!mintRequest3Cid) {
+ throw new Error("Mint request 3 not found");
+ }
+ await custodianWrappedSdk.issuerMintRequest.accept(mintRequest3Cid);
+ console.info(" ✓ Token3 minted to Alice (100.0)");
+
+ console.info("✓ All 3 underlying tokens minted to Alice\n");
+
+ // === RETURN SETUP RESULT ===
+ const result = {
+ parties: {
+ custodian: custodianAllocatedParty.partyId,
+ alice: aliceAllocatedParty.partyId,
+ },
+ tokens: {
+ token1: {
+ instrumentId: instrumentId1,
+ tokenFactoryCid: tokenFactory1Cid,
+ transferFactoryCid: transferFactory1Cid,
+ },
+ token2: {
+ instrumentId: instrumentId2,
+ tokenFactoryCid: tokenFactory2Cid,
+ transferFactoryCid: transferFactory2Cid,
+ },
+ token3: {
+ instrumentId: instrumentId3,
+ tokenFactoryCid: tokenFactory3Cid,
+ transferFactoryCid: transferFactory3Cid,
+ },
+ },
+ etf: {
+ instrumentId: etfInstrumentId,
+ },
+ rulesCid: rulesCid,
+ };
+
+ console.info("=== Setup Complete ===");
+ console.info(JSON.stringify(result, null, 2));
+
+ return result;
+}
diff --git a/packages/token-sdk/src/wrappedSdk/balances.ts b/packages/token-sdk/src/wrappedSdk/balances.ts
index f407276..b5b7378 100644
--- a/packages/token-sdk/src/wrappedSdk/balances.ts
+++ b/packages/token-sdk/src/wrappedSdk/balances.ts
@@ -42,15 +42,16 @@ export async function getBalances(
> = {};
utxos.forEach((utxo) => {
- const instrumentId = instrumentIdToString(utxo.instrumentId);
- if (!balances[instrumentId]) {
- balances[instrumentId] = { total: 0, utxos: [] };
+ const { id } = utxo.instrumentId;
+
+ if (!balances[id]) {
+ balances[id] = { total: 0, utxos: [] };
}
const amount = Number(utxo.amount);
- balances[instrumentId].total += amount;
- balances[instrumentId].utxos.push({
+ balances[id].total += amount;
+ balances[id].utxos.push({
amount,
contractId: utxo.contractId,
});
diff --git a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts
index d5865a5..d2c87ad 100644
--- a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts
+++ b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts
@@ -15,18 +15,18 @@ import { CreatedEvent } from "../../types/CreatedEvent.js";
export type LifecycleEventType = "CouponPayment" | "Redemption";
-export interface BondLifecycleEffect {
- issuer: Party;
- depository: Party;
- eventType: LifecycleEventType;
- targetInstrumentId: string;
- targetVersion: string;
- producedVersion?: string;
- eventDate: number;
- settlementTime?: number;
- amount: number;
- currencyInstrumentId: InstrumentId;
-}
+// export interface BondLifecycleEffect {
+// issuer: Party;
+// depository: Party;
+// eventType: LifecycleEventType;
+// targetInstrumentId: string;
+// targetVersion: string;
+// producedVersion?: string;
+// eventDate: number;
+// settlementTime?: number;
+// amount: number;
+// currencyInstrumentId: InstrumentId;
+// }
export interface BondLifecycleInstructionParams {
eventType: LifecycleEventType;