diff --git a/packages/token-app/app/api/wallet/bond/disclosure/route.ts b/packages/token-app/app/api/wallet/bond/disclosure/route.ts new file mode 100644 index 0000000..94ff407 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/disclosure/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getWrappedSdkWithKeyPairForParty, + keyPairFromSeed, +} from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const bondInstrumentCid = searchParams.get("bondInstrumentCid"); + const adminPartyId = searchParams.get("adminPartyId"); + + if (!bondInstrumentCid || !adminPartyId) { + return NextResponse.json( + { error: "Missing bondInstrumentCid or adminPartyId" }, + { status: 400 } + ); + } + + // TODO: change to not hardcode the custodian seed + const keyPair = keyPairFromSeed("custodian"); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + adminPartyId, + keyPair + ); + + const disclosure = + await wrappedSdk.bonds.disclosure.getInstrumentDisclosure( + bondInstrumentCid + ); + + return NextResponse.json({ disclosure }); + } catch (error) { + console.error("Error getting bond disclosure:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/factory/instrument/route.ts b/packages/token-app/app/api/wallet/bond/factory/instrument/route.ts new file mode 100644 index 0000000..615f2aa --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/factory/instrument/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, +} from "@denotecapital/token-sdk"; + +export async function POST(request: NextRequest) { + try { + const { + instrumentId, + notional, + couponRate, + couponFrequency, + maturityDate, + partyId, + seed, + } = await request.json(); + + if ( + !instrumentId || + notional === undefined || + couponRate === undefined || + couponFrequency === undefined || + !maturityDate || + !partyId || + !seed + ) { + return NextResponse.json( + { + error: "Missing required fields", + }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + partyId, + keyPair + ); + + const bondFactoryCid = await wrappedSdk.bonds.factory.getOrCreate( + instrumentId + ); + + const bondInstrumentCid = + await wrappedSdk.bonds.factory.createInstrument( + bondFactoryCid, + instrumentId, + { + depository: partyId, + notional, + couponRate, + couponFrequency, + maturityDate, + } + ); + + return NextResponse.json({ + bondInstrumentCid, + bondFactoryCid, + }); + } catch (error) { + console.error("Error creating bond instrument:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/factory/instruments/route.ts b/packages/token-app/app/api/wallet/bond/factory/instruments/route.ts new file mode 100644 index 0000000..cfc61fb --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/factory/instruments/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getSdkForParty, + bondInstrumentTemplateId, + ActiveContractResponse, +} from "@denotecapital/token-sdk"; + +interface BondInstrumentParams { + issuer: string; + instrumentId: string; + maturityDate: string; + couponRate: number; + couponFrequency: number; +} + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const custodianPartyId = searchParams.get("custodianPartyId"); + + if (!custodianPartyId) { + return NextResponse.json( + { error: "Missing custodianPartyId" }, + { status: 400 } + ); + } + + const sdk = await getSdkForParty(custodianPartyId); + const ledger = sdk.userLedger!; + const end = await ledger.ledgerEnd(); + + const activeContracts = (await ledger.activeContracts({ + offset: end.offset, + filterByParty: true, + parties: [custodianPartyId], + templateIds: [bondInstrumentTemplateId], + })) as ActiveContractResponse[]; + + const instruments = activeContracts + .map((contract) => { + const jsActive = contract.contractEntry.JsActiveContract; + if (!jsActive) return null; + + const createArg = jsActive.createdEvent.createArgument; + const contractId = jsActive.createdEvent.contractId; + + if (createArg.issuer !== custodianPartyId) return null; + + const instrumentId = createArg.instrumentId; + const nameMatch = instrumentId.match(/^[^#]+#(.+)$/); + const name = nameMatch ? nameMatch[1] : instrumentId; + + return { + name, + instrumentId, + custodianPartyId, + bondInstrumentCid: contractId, + maturityDate: createArg.maturityDate, + couponRate: createArg.couponRate, + couponFrequency: createArg.couponFrequency, + }; + }) + .filter((inst): inst is NonNullable => inst !== null); + + return NextResponse.json({ instruments }); + } catch (error) { + console.error("Error getting bond instruments:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/claim-request/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/claim-request/route.ts new file mode 100644 index 0000000..b39d87e --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/claim-request/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, + getSdkForParty, + bondLifecycleClaimRequestTemplateId, + ActiveContractResponse, +} from "@denotecapital/token-sdk"; + +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 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: [bondLifecycleClaimRequestTemplateId], + })) as ActiveContractResponse[]; + + const requests = activeContracts + .map((contract) => { + const jsActive = contract.contractEntry.JsActiveContract; + if (!jsActive) return null; + + const createArg = jsActive.createdEvent.createArgument as { + effectCid: string; + bondHoldingCid: string; + holder: string; + issuer: string; + }; + const contractId = jsActive.createdEvent.contractId; + + if (createArg.issuer !== issuer) return null; + + return { + contractId, + effectCid: createArg.effectCid, + bondHoldingCid: createArg.bondHoldingCid, + holder: createArg.holder, + issuer: createArg.issuer, + }; + }) + .filter((req): req is NonNullable => req !== null); + + return NextResponse.json({ requests }); + } catch (error) { + console.error("Error getting lifecycle claim requests:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const { + effectCid, + bondHoldingCid, + bondRulesCid, + bondInstrumentCid, + currencyTransferFactoryCid, + issuerCurrencyHoldingCid, + holder, + issuer, + seed, + disclosure, + } = await request.json(); + + if ( + !effectCid || + !bondHoldingCid || + !bondRulesCid || + !bondInstrumentCid || + !currencyTransferFactoryCid || + !issuerCurrencyHoldingCid || + !holder || + !issuer || + !seed + ) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + holder, + keyPair + ); + + await wrappedSdk.bonds.lifecycleClaimRequest.create( + { + effectCid, + bondHoldingCid, + bondRulesCid, + bondInstrumentCid, + currencyTransferFactoryCid, + issuerCurrencyHoldingCid, + holder, + issuer, + }, + disclosure ? [disclosure] : undefined + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error creating lifecycle claim request:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/effect/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/effect/route.ts new file mode 100644 index 0000000..e51dbf7 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/effect/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getWrappedSdkForParty } from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const partyId = searchParams.get("partyId"); + + if (!partyId) { + return NextResponse.json( + { error: "Missing partyId" }, + { status: 400 } + ); + } + + const wrappedSdk = await getWrappedSdkForParty(partyId); + + try { + const effects = await wrappedSdk.bonds.lifecycleEffect.getAll( + partyId + ); + return NextResponse.json(effects); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("Bond lifecycle effect not found") + ) { + return NextResponse.json([], { status: 200 }); + } + throw error; + } + } catch (error) { + console.error("Error getting lifecycle effect:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/infrastructure/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/infrastructure/route.ts new file mode 100644 index 0000000..3507e31 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/infrastructure/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, +} from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const partyId = searchParams.get("partyId"); + const seed = searchParams.get("seed"); + const currencyInstrumentId = searchParams.get("currencyInstrumentId"); + + if (!partyId || !seed || !currencyInstrumentId) { + return NextResponse.json( + { error: "Missing partyId, seed, or currencyInstrumentId" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + partyId, + keyPair + ); + + const bondRulesCid = await wrappedSdk.bonds.bondRules.getLatest(); + if (!bondRulesCid) { + throw new Error( + "Bond rules not found. Please run setup script first." + ); + } + + const currencyRulesCid = await wrappedSdk.tokenRules.getLatest(); + if (!currencyRulesCid) { + throw new Error( + "Currency rules not found. Please run setup script first." + ); + } + + const currencyTransferFactoryCid = + await wrappedSdk.transferFactory.getLatest(currencyRulesCid); + if (!currencyTransferFactoryCid) { + throw new Error( + "Currency transfer factory not found. Please run setup script first." + ); + } + + const currencyBalance = await wrappedSdk.balances.getByInstrumentId({ + owner: partyId, + instrumentId: { admin: partyId, id: currencyInstrumentId }, + }); + + const currencyHoldings = currencyBalance.utxos.map((u) => u.contractId); + + return NextResponse.json({ + bondRulesCid, + currencyTransferFactoryCid, + currencyHoldings, + }); + } catch (error) { + console.error("Error getting bond lifecycle infrastructure:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/instruction/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/instruction/route.ts new file mode 100644 index 0000000..bebda5c --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/instruction/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, + getWrappedSdkForParty, +} from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const partyId = searchParams.get("partyId"); + + if (!partyId) { + return NextResponse.json( + { error: "Missing partyId" }, + { status: 400 } + ); + } + + const wrappedSdk = await getWrappedSdkForParty(partyId); + const instructions = await wrappedSdk.bonds.lifecycleInstruction.getAll( + partyId + ); + return NextResponse.json(instructions); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("Bond lifecycle instruction not found") + ) { + return NextResponse.json([], { status: 200 }); + } + console.error("Error getting lifecycle instruction:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const { contractId, partyId, seed, disclosure } = await request.json(); + + if (!contractId || !partyId || !seed) { + return NextResponse.json( + { error: "Missing contractId, partyId, or seed" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + partyId, + keyPair + ); + + await wrappedSdk.bonds.lifecycleInstruction.process( + contractId, + disclosure ? [disclosure] : undefined + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error processing lifecycle instruction:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/process/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/process/route.ts new file mode 100644 index 0000000..50be4c7 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/process/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, +} from "@denotecapital/token-sdk"; + +export async function POST(request: NextRequest) { + let eventType: string | undefined; + let targetInstrumentId: string | undefined; + let targetVersion: string | undefined; + let bondCid: string | undefined; + + try { + const { + lifecycleRuleCid, + eventType: eventTypeParam, + targetInstrumentId: targetInstrumentIdParam, + targetVersion: targetVersionParam, + bondCid: bondCidParam, + partyId, + seed, + } = await request.json(); + + eventType = eventTypeParam; + targetInstrumentId = targetInstrumentIdParam; + targetVersion = targetVersionParam; + bondCid = bondCidParam; + + if ( + !lifecycleRuleCid || + !eventTypeParam || + !targetInstrumentIdParam || + !targetVersionParam || + !bondCidParam || + !partyId || + !seed + ) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + if (eventTypeParam !== "coupon" && eventTypeParam !== "redemption") { + return NextResponse.json( + { error: "Event type must be 'coupon' or 'redemption'" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + partyId, + keyPair + ); + + if (eventTypeParam === "coupon") { + await wrappedSdk.bonds.lifecycleRule.processCouponPaymentEvent( + lifecycleRuleCid, + { + targetInstrumentId: targetInstrumentIdParam, + targetVersion: targetVersionParam, + bondCid: bondCidParam, + } + ); + } else { + await wrappedSdk.bonds.lifecycleRule.processRedemptionEvent( + lifecycleRuleCid, + { + targetInstrumentId: targetInstrumentIdParam, + targetVersion: targetVersionParam, + bondCid: bondCidParam, + } + ); + } + + const effect = await wrappedSdk.bonds.lifecycleEffect.getLatest( + partyId + ); + + return NextResponse.json({ + effectCid: effect.contractId, + producedVersion: effect.producedVersion, + }); + } catch (error) { + console.error("Error processing lifecycle event:", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error("Full error details:", { + errorMessage, + eventType, + targetInstrumentId, + targetVersion, + bondCid, + }); + return NextResponse.json({ error: errorMessage }, { status: 500 }); + } +} diff --git a/packages/token-app/app/api/wallet/bond/lifecycle/rule/route.ts b/packages/token-app/app/api/wallet/bond/lifecycle/rule/route.ts new file mode 100644 index 0000000..9a79e28 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/lifecycle/rule/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, +} from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const partyId = searchParams.get("partyId"); + const seed = searchParams.get("seed"); + + if (!partyId || !seed) { + return NextResponse.json( + { error: "Missing partyId or seed" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + partyId, + keyPair + ); + + // TODO: change to not hardcode the currency instrument id + const currencyInstrumentId = `${partyId}#Currency`; + + const lifecycleRuleCid = await wrappedSdk.bonds.lifecycleRule.getLatest( + { + depository: partyId, + currencyInstrumentId: { + admin: partyId, + id: currencyInstrumentId, + }, + } + ); + + return NextResponse.json({ + lifecycleRuleCid: lifecycleRuleCid || null, + }); + } catch (error) { + console.error("Error getting lifecycle rule:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/api/wallet/bond/mint-request/route.ts b/packages/token-app/app/api/wallet/bond/mint-request/route.ts new file mode 100644 index 0000000..b5a0b90 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/mint-request/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + keyPairFromSeed, + getWrappedSdkWithKeyPairForParty, + getSdkForParty, + bondIssuerMintRequestTemplateId, + ActiveContractResponse, +} from "@denotecapital/token-sdk"; + +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 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: [bondIssuerMintRequestTemplateId], + })) as ActiveContractResponse[]; + + const requests = activeContracts + .map((contract) => { + const jsActive = contract.contractEntry.JsActiveContract; + if (!jsActive) return null; + + const createArg = jsActive.createdEvent.createArgument as { + instrumentCid: string; + issuer: string; + receiver: string; + amount: number; + }; + const contractId = jsActive.createdEvent.contractId; + + // Filter by issuer if provided + if (createArg.issuer !== issuer) return null; + + return { + contractId, + instrumentCid: createArg.instrumentCid, + issuer: createArg.issuer, + receiver: createArg.receiver, + amount: createArg.amount, + }; + }) + .filter((req): req is NonNullable => req !== null); + + return NextResponse.json({ requests }); + } catch (error) { + console.error("Error getting bond mint requests:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const { instrumentCid, issuer, receiver, amount, seed } = + await request.json(); + + if ( + !instrumentCid || + !issuer || + !receiver || + amount === undefined || + !seed + ) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + const keyPair = keyPairFromSeed(seed); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + receiver, + keyPair + ); + + await wrappedSdk.bonds.issuerMintRequest.create({ + instrumentCid, + issuer, + receiver, + amount, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error creating bond 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/bond/version/route.ts b/packages/token-app/app/api/wallet/bond/version/route.ts new file mode 100644 index 0000000..be6b1e1 --- /dev/null +++ b/packages/token-app/app/api/wallet/bond/version/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getWrappedSdkForParty } from "@denotecapital/token-sdk"; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const partyId = searchParams.get("partyId"); + const contractId = searchParams.get("contractId"); + + if (!partyId || !contractId) { + return NextResponse.json( + { error: "Missing partyId or contractId" }, + { status: 400 } + ); + } + + const wrappedSdk = await getWrappedSdkForParty(partyId); + const bond = await wrappedSdk.bonds.bond.get(contractId); + + if (!bond) { + return NextResponse.json( + { error: "Bond contract not found" }, + { status: 404 } + ); + } + + return NextResponse.json({ version: bond.version }); + } catch (error) { + console.error("Error getting bond version:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/packages/token-app/app/bond/page.tsx b/packages/token-app/app/bond/page.tsx new file mode 100644 index 0000000..f468042 --- /dev/null +++ b/packages/token-app/app/bond/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import { ConnectionStatus } from "@/components/ConnectionStatus"; +import { BondPartyView } from "@/components/BondPartyView"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; + +const PARTIES = ["custodian", "alice"] as const; + +export default function BondPage() { + const [selectedParty, setSelectedParty] = useState("custodian"); + const [partyIds, setPartyIds] = useState>({ + custodian: null, + alice: null, + }); + + const handlePartyCreated = (partyId: string, partyName: string) => { + setPartyIds((prev) => ({ + ...prev, + [partyName]: partyId, + })); + }; + + return ( +
+
+
+
+

+ Bond Lifecycle Demo +

+ +
+ +
+
+ {PARTIES.map((party) => ( + + ))} +
+ + + +
+
+ + + + +
+
+ ); +} diff --git a/packages/token-app/app/page.tsx b/packages/token-app/app/page.tsx index 85b63c2..03d16e3 100644 --- a/packages/token-app/app/page.tsx +++ b/packages/token-app/app/page.tsx @@ -6,6 +6,7 @@ import { PartyView } from "@/components/PartyView"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import Link from "next/link"; const PARTIES = ["custodian", "alice", "bob"] as const; @@ -35,24 +36,29 @@ export default function Home() { -
- {PARTIES.map((party) => ( - - ))} +
+
+ {PARTIES.map((party) => ( + + ))} +
+ + +
diff --git a/packages/token-app/components/BondCustodianView.tsx b/packages/token-app/components/BondCustodianView.tsx new file mode 100644 index 0000000..3b675bd --- /dev/null +++ b/packages/token-app/components/BondCustodianView.tsx @@ -0,0 +1,421 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { useBondInstruments } from "@/lib/queries/bondInstruments"; +import { + useBondLifecycle, + useAllLifecycleEffects, +} from "@/lib/queries/bondLifecycle"; +import { + useQuery, + useQueries, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; + +interface BondCustodianViewProps { + partyId: string; + partyName: string; +} + +export function BondCustodianView({ + partyId, + partyName, +}: BondCustodianViewProps) { + const [bondName, setBondName] = useState("Bond"); + const [notional, setNotional] = useState("1000"); + const [couponRate, setCouponRate] = useState("0.05"); + const [couponFrequency, setCouponFrequency] = useState("2"); + const [maturityDays, setMaturityDays] = useState("10"); + + const queryClient = useQueryClient(); + const bondInstrumentsQuery = useBondInstruments(partyId); + const bondInstruments = bondInstrumentsQuery.data || []; + + const createInstrument = useMutation({ + mutationFn: async (params: { + instrumentId: string; + notional: number; + couponRate: number; + couponFrequency: number; + maturityDate: string; + partyId: string; + seed: string; + currencyInstrumentId?: string; + }) => { + const response = await fetch( + "/api/wallet/bond/factory/instrument", + { + 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 bond instrument" + ); + } + + return response.json() as Promise<{ + bondInstrumentCid: string; + bondFactoryCid: string; + }>; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["bondFactory"], + }); + queryClient.invalidateQueries({ + queryKey: ["bondInstruments"], + }); + queryClient.invalidateQueries({ + queryKey: ["lifecycleRule"], + }); + }, + }); + const bondLifecycle = useBondLifecycle(); + const allLifecycleEffects = useAllLifecycleEffects(partyId); + + const allBalanceQueries = useQueries({ + queries: bondInstruments.map((instrument) => ({ + queryKey: ["allBalances", partyId, instrument.instrumentId], + queryFn: async () => { + const params = new URLSearchParams({ + partyId, + admin: partyId, + id: instrument.instrumentId, + }); + const response = await fetch( + `/api/wallet/balances/all?${params}` + ); + return response.json() as Promise<{ + balances: Array<{ + party: string; + total: number; + utxos: Array<{ amount: number; contractId: string }>; + }>; + }>; + }, + enabled: !!partyId && !!instrument.instrumentId, + })), + }); + + const lifecycleRuleQuery = useQuery({ + queryKey: ["lifecycleRule", partyId], + queryFn: async () => { + const params = new URLSearchParams({ + partyId, + seed: partyName, + }); + const response = await fetch( + `/api/wallet/bond/lifecycle/rule?${params}` + ); + const data = await response.json(); + return data.lifecycleRuleCid || null; + }, + enabled: !!partyId, + }); + + const handleCreateBondInstrument = async () => { + try { + const maturityDate = new Date(); + maturityDate.setSeconds( + maturityDate.getSeconds() + parseInt(maturityDays) + ); + const instrumentId = `${partyId}#${bondName.trim()}`; + + await createInstrument.mutateAsync({ + instrumentId, + notional: parseFloat(notional), + couponRate: parseFloat(couponRate), + couponFrequency: parseInt(couponFrequency), + maturityDate: maturityDate.toISOString(), + partyId, + seed: partyName, + }); + queryClient.invalidateQueries({ + queryKey: ["allBalances"], + }); + toast.success("Bond instrument created!"); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to create bond instrument" + ); + } + }; + + const readyToProcess = useMemo(() => { + if (!lifecycleRuleQuery.data) return []; + + const items: Array<{ + instrumentId: string; + instrumentName: string; + bondCid: string; + totalOutstanding: number; + holders: number; + eventType: "coupon" | "redemption"; + maturityDate: Date; + }> = []; + + bondInstruments.forEach((instrument, index) => { + const balanceData = allBalanceQueries[index]?.data; + if (!balanceData?.balances?.length) return; + + const firstBond = balanceData.balances + .flatMap((b) => b.utxos) + .find((utxo) => utxo); + if (!firstBond) return; + + const isMatured = new Date(instrument.maturityDate) <= new Date(); + const eventType = isMatured ? "Redemption" : "CouponPayment"; + + const hasEffect = allLifecycleEffects.data?.some( + (e) => + e.targetInstrumentId === instrument.instrumentId && + e.eventType === eventType + ); + if (hasEffect) return; + + const totalOutstanding = balanceData.balances.reduce( + (sum, b) => sum + b.total, + 0 + ); + const holders = balanceData.balances.filter( + (b) => b.total > 0 + ).length; + + items.push({ + instrumentId: instrument.instrumentId, + instrumentName: instrument.name, + bondCid: firstBond.contractId, + totalOutstanding, + holders, + eventType: isMatured ? "redemption" : "coupon", + maturityDate: new Date(instrument.maturityDate), + }); + }); + + return items; + }, [ + bondInstruments, + allBalanceQueries, + lifecycleRuleQuery.data, + allLifecycleEffects.data, + ]); + + const handleProcessLifecycleEvent = async ( + instrumentId: string, + bondCid: string, + eventType: "coupon" | "redemption" + ) => { + try { + // Fetch bond version on-demand + const versionResponse = await fetch( + `/api/wallet/bond/version?partyId=${partyId}&contractId=${bondCid}` + ); + const { version } = versionResponse.ok + ? await versionResponse.json() + : { version: "0" }; + + await bondLifecycle.processEvent.mutateAsync({ + lifecycleRuleCid: lifecycleRuleQuery.data!, + eventType, + targetInstrumentId: instrumentId, + targetVersion: version, + bondCid, + partyId, + seed: partyName, + }); + + queryClient.invalidateQueries({ + queryKey: ["allBalances"], + }); + queryClient.invalidateQueries({ + queryKey: ["allLifecycleEffects", partyId], + }); + queryClient.invalidateQueries({ + queryKey: ["bondFactory"], + }); + + toast.success( + `${ + eventType === "coupon" ? "Coupon payment" : "Redemption" + } processed` + ); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to process lifecycle event" + ); + } + }; + + return ( +
+ + + Create Bond Instrument + + +
+ + setBondName(e.target.value)} + /> +
+
+
+ + setNotional(e.target.value)} + /> +
+
+ + setCouponRate(e.target.value)} + /> +
+
+ + + setCouponFrequency(e.target.value) + } + /> +
+
+ + + setMaturityDays(e.target.value) + } + /> +
+
+ +
+
+ + + + Issued Bonds + + +
+ {bondInstruments.map((instrument) => ( +
+

{instrument.name}

+

+ Rate:{" "} + {(instrument.couponRate * 100).toFixed(2)}% + | Freq: {instrument.couponFrequency}x | + Maturity:{" "} + {new Date( + instrument.maturityDate + ).toLocaleDateString()} +

+
+ ))} +
+
+
+ + + + Process Lifecycle Events + + + {readyToProcess.length > 0 ? ( +
+ {readyToProcess.map((item) => ( +
+
+
+

+ {item.instrumentName} +

+

+ {item.totalOutstanding} units + outstanding • {item.holders}{" "} + {item.holders === 1 + ? "holder" + : "holders"} +

+

+ Maturity:{" "} + {item.maturityDate.toLocaleDateString()} +

+
+ +
+
+ ))} +
+ ) : ( +

+ No bonds ready for processing. +

+ )} +
+
+
+ ); +} diff --git a/packages/token-app/components/BondPartyView.tsx b/packages/token-app/components/BondPartyView.tsx new file mode 100644 index 0000000..33938e4 --- /dev/null +++ b/packages/token-app/components/BondPartyView.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Loader2, Copy, User, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; +import { BondCustodianView } from "./BondCustodianView"; +import { BondUserView } from "./BondUserView"; + +interface BondPartyViewProps { + partyName: string; + partyId: string | null; + allPartyIds: Record; + onPartyCreated?: (partyId: string, partyName: string) => void; +} + +export function BondPartyView({ + partyName, + partyId, + allPartyIds, + onPartyCreated, +}: BondPartyViewProps) { + const createPartyMutation = useMutation({ + mutationFn: async (name: string) => { + const response = await fetch("/api/wallet/party", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to create party"); + } + + return response.json(); + }, + onSuccess: (data, name) => { + onPartyCreated?.(data.partyId, name); + toast.success(`Successfully created ${name} party`); + }, + onError: (error, name) => { + toast.error( + `Failed to create ${name} party: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + }, + }); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success("Copied to clipboard"); + }; + + if (!partyId) { + return ( + + + + + {partyName.charAt(0).toUpperCase() + partyName.slice(1)} + + + Create a party to start managing bonds + + + + + + + ); + } + + const custodianPartyId = allPartyIds.custodian; + const isCustodian = partyId === custodianPartyId; + + const PartyInfoCard = () => ( + + +
+
+ + + {partyName.charAt(0).toUpperCase() + + partyName.slice(1)} + + + Party information and status + +
+ + + Active + +
+
+ +
+ +
+ + {partyId} + + +
+
+
+
+ ); + + return ( +
+ + {isCustodian ? ( + + ) : ( + + )} +
+ ); +} diff --git a/packages/token-app/components/BondUserView.tsx b/packages/token-app/components/BondUserView.tsx new file mode 100644 index 0000000..3d1f210 --- /dev/null +++ b/packages/token-app/components/BondUserView.tsx @@ -0,0 +1,551 @@ +"use client"; + +import { useState } from "react"; +import { useBalance } from "@/lib/queries/balance"; +import { useBondMintRequest } from "@/lib/queries/bondMintRequest"; +import { + useAllLifecycleEffects, + useLifecycleClaimRequest, + useLifecycleInstruction, +} from "@/lib/queries/bondLifecycle"; +import type { BondLifecycleInstruction } from "@denotecapital/token-sdk"; +import { useBondInstruments } from "@/lib/queries/bondInstruments"; +import { useQueryClient } from "@tanstack/react-query"; +import { useTransferInstruction } from "@/lib/queries/transferInstruction"; +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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "sonner"; + +interface BondUserViewProps { + partyId: string; + partyName: string; + custodianPartyId: string | null; +} + +export function BondUserView({ + partyId, + partyName, + custodianPartyId, +}: BondUserViewProps) { + const [selectedBondInstrumentId, setSelectedBondInstrumentId] = + useState(""); + const [mintAmount, setMintAmount] = useState(1); + + const queryClient = useQueryClient(); + const bondInstrumentsQuery = useBondInstruments(custodianPartyId); + const bondInstruments = bondInstrumentsQuery.data || []; + const bondMintRequest = useBondMintRequest(); + const lifecycleClaimRequest = useLifecycleClaimRequest( + partyId, + custodianPartyId + ); + const lifecycleInstruction = useLifecycleInstruction(partyId); + const transferInstruction = useTransferInstruction(partyId); + + const selectedInstrument = bondInstruments.find( + (inst) => inst.instrumentId === selectedBondInstrumentId + ); + + const { data: selectedBalance } = useBalance( + partyId, + selectedInstrument && custodianPartyId + ? { admin: custodianPartyId, id: selectedInstrument.instrumentId } + : null + ); + + const allLifecycleEffects = useAllLifecycleEffects(custodianPartyId); + + const currencyInstrumentId = custodianPartyId + ? `${custodianPartyId}#Currency` + : null; + const { data: currencyBalance } = useBalance( + partyId, + custodianPartyId && currencyInstrumentId + ? { admin: custodianPartyId, id: currencyInstrumentId } + : null + ); + + const handleCreateBondMintRequest = async () => { + if (!selectedInstrument) { + toast.error("Please select a bond instrument"); + return; + } + + try { + await bondMintRequest.create.mutateAsync({ + instrumentCid: selectedInstrument.bondInstrumentCid, + issuer: custodianPartyId!, + receiver: partyId, + amount: mintAmount, + seed: partyName, + }); + queryClient.invalidateQueries({ + queryKey: ["balances", partyId], + }); + toast.success( + `Bond mint request created for ${mintAmount} bond(s)` + ); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to create bond mint request" + ); + } + }; + + const handleClaimLifecycleEvent = async (effectCid: string) => { + if (!selectedInstrument) { + toast.error("Please select a bond first"); + return; + } + + const effect = allLifecycleEffects.data?.find( + (e) => e.contractId === effectCid + ); + if (!effect) { + toast.error("Invalid effect"); + return; + } + + try { + const params = new URLSearchParams({ + owner: partyId, + admin: custodianPartyId!, + id: selectedInstrument.instrumentId, + }); + const balanceResponse = await fetch( + `/api/wallet/balances?${params}` + ); + if (!balanceResponse.ok) { + const error = await balanceResponse.json().catch(() => ({})); + toast.error(error.error || "Failed to fetch balance"); + return; + } + const balance = await balanceResponse.json(); + if (!balance?.utxos?.length) { + toast.error("No bonds available"); + return; + } + + let matchingBond = null; + for (const utxo of balance.utxos) { + const versionRes = await fetch( + `/api/wallet/bond/version?partyId=${partyId}&contractId=${utxo.contractId}` + ); + const { version } = versionRes.ok + ? await versionRes.json() + : { version: "0" }; + if (version === effect.targetVersion) { + matchingBond = utxo; + break; + } + } + + if (!matchingBond) { + toast.error("No bonds with matching version"); + return; + } + + const currencyInstrumentId = `${custodianPartyId}#Currency`; + const [disclosureRes, infraRes] = await Promise.all([ + fetch( + `/api/wallet/bond/disclosure?bondInstrumentCid=${selectedInstrument.bondInstrumentCid}&adminPartyId=${custodianPartyId}` + ), + fetch( + `/api/wallet/bond/lifecycle/infrastructure?partyId=${encodeURIComponent( + custodianPartyId! + )}¤cyInstrumentId=${encodeURIComponent( + currencyInstrumentId + )}&seed=custodian` + ), + ]); + + const { disclosure } = await disclosureRes.json(); + const infrastructure = await infraRes.json(); + + await lifecycleClaimRequest.create.mutateAsync({ + effectCid, + bondHoldingCid: matchingBond.contractId, + bondRulesCid: infrastructure.bondRulesCid, + bondInstrumentCid: selectedInstrument.bondInstrumentCid, + currencyTransferFactoryCid: + infrastructure.currencyTransferFactoryCid, + issuerCurrencyHoldingCid: infrastructure.currencyHoldings[0], + holder: partyId, + issuer: custodianPartyId!, + seed: partyName, + disclosure, + }); + + queryClient.invalidateQueries({ + queryKey: ["lifecycleInstruction"], + }); + queryClient.invalidateQueries({ + queryKey: ["lifecycleClaimRequests"], + }); + queryClient.invalidateQueries({ + queryKey: ["allLifecycleEffects"], + }); + toast.success("Claim request created!"); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to claim" + ); + } + }; + + const handleProcessInstruction = async ( + instruction: BondLifecycleInstruction + ) => { + try { + let disclosure = undefined; + if ( + instruction.eventType === "CouponPayment" && + instruction.bondInstrumentCid + ) { + const res = await fetch( + `/api/wallet/bond/disclosure?bondInstrumentCid=${instruction.bondInstrumentCid}&adminPartyId=${custodianPartyId}` + ); + disclosure = (await res.json()).disclosure; + } + + await lifecycleInstruction.process.mutateAsync({ + contractId: instruction.contractId, + partyId, + seed: partyName, + disclosure, + }); + + if (custodianPartyId) { + const transferRes = await fetch( + `/api/wallet/transfer-instruction?partyId=${partyId}` + ); + if (transferRes.ok) { + const { instructions } = await transferRes.json(); + const currencyTransfer = instructions?.find( + (inst: { + transfer: { + instrumentId: { admin: string; id: string }; + }; + }) => + inst.transfer.instrumentId.admin === + custodianPartyId && + inst.transfer.instrumentId.id.includes("Currency") + ); + + if (currencyTransfer) { + const disclosureRes = + await transferInstruction.getDisclosure.mutateAsync( + { + transferInstructionCid: + currencyTransfer.contractId, + adminPartyId: custodianPartyId, + } + ); + + await transferInstruction.accept.mutateAsync({ + contractId: currencyTransfer.contractId, + disclosure: disclosureRes.disclosure, + receiverPartyId: partyId, + seed: partyName, + }); + } + } + } + + queryClient.invalidateQueries({ + queryKey: ["allLifecycleInstructions"], + }); + queryClient.invalidateQueries({ queryKey: ["balances"] }); + queryClient.invalidateQueries({ + queryKey: ["transferInstructions"], + }); + toast.success("Instruction processed!"); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to process" + ); + } + }; + + if (!custodianPartyId) { + return ( + + + Setup Required + + Please create the custodian party first + + + + ); + } + + return ( +
+ + + Select Bond + + +
+ + {bondInstruments.length > 0 ? ( + + ) : ( + + setSelectedBondInstrumentId(e.target.value) + } + placeholder={`${custodianPartyId}#Bond`} + /> + )} +
+
+
+ + {!selectedInstrument && ( + + +

+ Please select a bond to continue +

+
+
+ )} + + {selectedInstrument && ( + <> + + + Balances + + + {selectedBalance && ( +
+

Bonds

+

+ {selectedBalance.total || 0} +

+
+ )} + {currencyBalance && ( +
+

+ Currency +

+

+ {currencyBalance.total || 0} +

+
+ )} +
+
+ + + + Mint Bonds + + +
+ + + setMintAmount( + parseFloat(e.target.value) || 0 + ) + } + /> +
+ +
+
+ + + + Claim Lifecycle Events + + + {!allLifecycleEffects.data?.length ? ( +

+ No lifecycle effects available. +

+ ) : ( +
+ {allLifecycleEffects.data + .filter((e) => { + // Must match selected bond + if ( + e.targetInstrumentId !== + selectedBondInstrumentId + ) + return false; + + // Check if instruction exists (indicates effect was already claimed/processed) + const hasInstruction = + lifecycleInstruction.getAll.data?.some( + (i) => + i.eventType === + e.eventType && + i.eventDate === + e.eventDate && + i.holder === partyId + ); + if (hasInstruction) return false; + + // Check if there's a pending claim request + const hasPending = + lifecycleClaimRequest.get.data?.requests?.some( + (r) => + r.effectCid === + e.contractId + ); + if (hasPending) return false; + + return true; + }) + .map((effect) => ( +
+
+

+ {effect.eventType === + "CouponPayment" + ? "Coupon" + : "Redemption"} +

+

+ {effect.amount} per unit +

+
+ +
+ ))} +
+ )} +
+
+ + + + + Process Lifecycle Instructions + + + + {!selectedInstrument ? ( +

+ Please select a bond first. +

+ ) : !lifecycleInstruction.getAll.data?.length ? ( +

+ No instructions available. +

+ ) : ( +
+ {lifecycleInstruction.getAll.data + ?.filter((i) => i.holder === partyId) + .map((instruction) => ( +
+
+

+ {instruction.eventType === + "CouponPayment" + ? "Coupon" + : "Redemption"} +

+

+ {instruction.amount} per + unit +

+
+ +
+ ))} +
+ )} +
+
+ + )} +
+ ); +} diff --git a/packages/token-app/lib/queries/bondInstruments.ts b/packages/token-app/lib/queries/bondInstruments.ts new file mode 100644 index 0000000..00a6bb7 --- /dev/null +++ b/packages/token-app/lib/queries/bondInstruments.ts @@ -0,0 +1,40 @@ +import { useQuery } from "@tanstack/react-query"; + +export interface BondInstrument { + name: string; + instrumentId: string; + custodianPartyId: string; + bondInstrumentCid: string; + maturityDate: string; + couponRate: number; + couponFrequency: number; +} + +export function useBondInstruments(custodianPartyId: string | null) { + return useQuery({ + queryKey: ["bondInstruments", custodianPartyId], + queryFn: async () => { + if (!custodianPartyId) + throw new Error("Custodian party ID required"); + + const params = new URLSearchParams({ + custodianPartyId: custodianPartyId, + }); + const response = await fetch( + `/api/wallet/bond/factory/instruments?${params}` + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || "Failed to get bond instruments" + ); + } + + const data = await response.json(); + return data.instruments as BondInstrument[]; + }, + enabled: !!custodianPartyId, + refetchInterval: 5000, + }); +} diff --git a/packages/token-app/lib/queries/bondLifecycle.ts b/packages/token-app/lib/queries/bondLifecycle.ts new file mode 100644 index 0000000..1eec447 --- /dev/null +++ b/packages/token-app/lib/queries/bondLifecycle.ts @@ -0,0 +1,262 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import type { + BondLifecycleEffect, + BondLifecycleInstruction, +} from "@denotecapital/token-sdk"; + +export interface LifecycleClaimRequest { + contractId: string; + effectCid: string; + bondHoldingCid: string; + holder: string; + issuer: string; +} + +export function useLifecycleEffect(partyId: string | null) { + return useQuery({ + queryKey: ["lifecycleEffect", partyId], + queryFn: async () => { + if (!partyId) throw new Error("Party ID required"); + + const params = new URLSearchParams({ partyId }); + const response = await fetch( + `/api/wallet/bond/lifecycle/effect?${params}` + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || "Failed to get lifecycle effect" + ); + } + + return response.json(); + }, + enabled: !!partyId, + refetchInterval: 5000, + }); +} + +export function useAllLifecycleEffects(partyId: string | null) { + return useQuery({ + queryKey: ["allLifecycleEffects", partyId], + queryFn: async () => { + if (!partyId) throw new Error("Party ID required"); + + const params = new URLSearchParams({ partyId }); + const response = await fetch( + `/api/wallet/bond/lifecycle/effect?${params}` + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || "Failed to get lifecycle effects" + ); + } + + const data = await response.json(); + return data; + }, + enabled: !!partyId, + refetchInterval: 5000, + }); +} + +export function useBondLifecycle() { + const queryClient = useQueryClient(); + + const processEvent = useMutation< + { effectCid: string; producedVersion: string | null }, + Error, + { + lifecycleRuleCid: string; + eventType: "coupon" | "redemption"; + targetInstrumentId: string; + targetVersion: string; + bondCid: string; + partyId: string; + seed: string; + } + >({ + mutationFn: async (params) => { + const response = await fetch("/api/wallet/bond/lifecycle/process", { + 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 process lifecycle event" + ); + } + + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["lifecycleEffect"], + }); + }, + }); + + return { processEvent }; +} + +export function useLifecycleClaimRequest( + holder: string | null, + issuer: string | null +) { + const queryClient = useQueryClient(); + + const get = useQuery<{ requests: LifecycleClaimRequest[] }>({ + queryKey: ["lifecycleClaimRequests", holder, issuer], + queryFn: async () => { + if (!holder || !issuer) + throw new Error("Holder and issuer required"); + + const params = new URLSearchParams({ + partyId: holder, + issuer, + }); + + const response = await fetch( + `/api/wallet/bond/lifecycle/claim-request?${params}` + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || "Failed to get lifecycle claim requests" + ); + } + + return response.json(); + }, + enabled: !!holder && !!issuer, + refetchInterval: 5000, + }); + + const create = useMutation({ + mutationFn: async (params: { + effectCid: string; + bondHoldingCid: string; + bondRulesCid: string; + bondInstrumentCid: string; + currencyTransferFactoryCid: string; + issuerCurrencyHoldingCid: string; + holder: string; + issuer: string; + seed: string; + disclosure?: unknown; + }) => { + const response = await fetch( + "/api/wallet/bond/lifecycle/claim-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 lifecycle claim request" + ); + } + + return response.json(); + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + "lifecycleClaimRequests", + variables.holder, + variables.issuer, + ], + }); + }, + }); + + return { + get, + create, + }; +} + +export function useLifecycleInstruction(partyId: string | null) { + const queryClient = useQueryClient(); + + const getAll = useQuery({ + queryKey: ["allLifecycleInstructions", partyId], + queryFn: async () => { + if (!partyId) throw new Error("Party ID required"); + + const params = new URLSearchParams({ partyId }); + const response = await fetch( + `/api/wallet/bond/lifecycle/instruction?${params}` + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || "Failed to get lifecycle instructions" + ); + } + + const data = await response.json(); + return data; + }, + enabled: !!partyId, + refetchInterval: 5000, + }); + + const process = useMutation({ + mutationFn: async (params: { + contractId: string; + partyId: string; + seed: string; + disclosure?: unknown; + }) => { + const response = await fetch( + "/api/wallet/bond/lifecycle/instruction", + { + 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 process lifecycle instruction" + ); + } + + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["lifecycleInstruction"], + }); + queryClient.invalidateQueries({ + queryKey: ["allLifecycleInstructions"], + }); + queryClient.invalidateQueries({ + queryKey: ["balances"], + }); + queryClient.invalidateQueries({ + queryKey: ["transferInstructions"], + }); + }, + }); + + return { + getAll, + process, + }; +} diff --git a/packages/token-app/lib/queries/bondMintRequest.ts b/packages/token-app/lib/queries/bondMintRequest.ts new file mode 100644 index 0000000..b2991d6 --- /dev/null +++ b/packages/token-app/lib/queries/bondMintRequest.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export interface BondMintRequest { + contractId: string; + instrumentCid: string; + issuer: string; + receiver: string; + amount: number; +} + +export function useBondMintRequest() { + const queryClient = useQueryClient(); + + const create = useMutation({ + mutationFn: async (params: { + instrumentCid: string; + issuer: string; + receiver: string; + amount: number; + seed: string; + }) => { + const response = await fetch("/api/wallet/bond/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 bond mint request" + ); + } + + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["balances"], + }); + }, + }); + + return { create }; +} diff --git a/packages/token-app/package.json b/packages/token-app/package.json index 5110b62..9a1c1e9 100644 --- a/packages/token-app/package.json +++ b/packages/token-app/package.json @@ -4,6 +4,8 @@ "private": true, "scripts": { "dev": "next dev", + "setup:bond-demo": "tsx scripts/setupBondDemo.ts", + "dev:setup": "npm run setup:bond-demo && npm run dev", "build": "next build", "start": "next start", "lint": "eslint" diff --git a/packages/token-app/scripts/setupBondDemo.ts b/packages/token-app/scripts/setupBondDemo.ts new file mode 100644 index 0000000..54011d4 --- /dev/null +++ b/packages/token-app/scripts/setupBondDemo.ts @@ -0,0 +1,225 @@ +/** + * Setup script for Bond Demo + * + * This script automatically sets up: + * 1. Custodian party (if it doesn't exist) + * 2. Bond infrastructure (bond rules, lifecycle rule, currency) + * 3. Default bond instrument + */ + +import { + keyPairFromSeed, + getDefaultSdkAndConnect, + getWrappedSdkWithKeyPairForParty, +} from "@denotecapital/token-sdk"; +import { signTransactionHash } from "@canton-network/wallet-sdk"; + +const CUSTODIAN_SEED = "custodian"; +const ALICE_SEED = "alice"; +const BOB_SEED = "bob"; +const DEFAULT_BOND_NAME = "TestBond"; +const DEFAULT_NOTIONAL = 1000; +const DEFAULT_COUPON_RATE = 0.05; +const DEFAULT_COUPON_FREQUENCY = 2; +const DEFAULT_MATURITY_SECONDS = 120; + +async function createParty(seed: string) { + console.log(`Creating ${seed} party...`); + + const keyPair = keyPairFromSeed(seed); + const sdk = await getDefaultSdkAndConnect(); + + if (!sdk.userLedger) { + throw new Error("SDK not connected"); + } + + const generatedParty = await sdk.userLedger.generateExternalParty( + keyPair.publicKey, + seed + ); + + if (!generatedParty) { + throw new Error(`Error creating ${seed} party`); + } + + const signedHash = signTransactionHash( + generatedParty.multiHash, + keyPair.privateKey + ); + + try { + const allocatedParty = await sdk.userLedger.allocateExternalParty( + signedHash, + generatedParty + ); + + if (!allocatedParty) { + throw new Error(`Error allocating ${seed} party`); + } + + console.log(`${seed} party created: ${allocatedParty.partyId}`); + return allocatedParty.partyId; + } catch (error) { + if ( + error instanceof Error && + (error.message.includes("already exists") || + error.message.includes("ALREADY_EXISTS")) + ) { + console.log( + `${seed} party already exists: ${generatedParty.partyId}` + ); + return generatedParty.partyId; + } + throw error; + } +} + +async function setupInfrastructure(custodianPartyId: string) { + console.log("Setting up bond infrastructure..."); + + const keyPair = keyPairFromSeed(CUSTODIAN_SEED); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + custodianPartyId, + keyPair + ); + + const currencyInstrumentId = `${custodianPartyId}#Currency`; + + console.log("Creating bond rules..."); + const bondRulesCid = await wrappedSdk.bonds.bondRules.getOrCreate(); + console.log(`Bond rules: ${bondRulesCid.slice(0, 20)}...`); + + console.log("Creating lifecycle rule..."); + const lifecycleRuleCid = await wrappedSdk.bonds.lifecycleRule.getOrCreate({ + depository: custodianPartyId, + currencyInstrumentId: { + admin: custodianPartyId, + id: currencyInstrumentId, + }, + }); + console.log(`Lifecycle rule: ${lifecycleRuleCid.slice(0, 20)}...`); + + console.log("Setting up currency infrastructure..."); + const currencyRulesCid = await wrappedSdk.tokenRules.getOrCreate(); + const currencyTransferFactoryCid = + await wrappedSdk.transferFactory.getOrCreate(currencyRulesCid); + const currencyTokenFactoryCid = await wrappedSdk.tokenFactory.getOrCreate( + currencyInstrumentId + ); + console.log(`Currency infrastructure ready`); + + const existingCurrencyBalance = await wrappedSdk.balances.getByInstrumentId( + { + owner: custodianPartyId, + instrumentId: { + admin: custodianPartyId, + id: currencyInstrumentId, + }, + } + ); + + if (!existingCurrencyBalance || existingCurrencyBalance.total === 0) { + console.log("Minting initial currency..."); + await wrappedSdk.tokenFactory.mintToken(currencyTokenFactoryCid, { + amount: 10000.0, + receiver: custodianPartyId, + }); + console.log("Minted 10,000 currency units"); + } else { + console.log( + `Currency already exists (${existingCurrencyBalance.total} units)` + ); + } + + console.log("Infrastructure setup complete"); +} + +async function createDefaultBondInstrument(custodianPartyId: string) { + console.log("📝 Creating default bond instrument..."); + + const keyPair = keyPairFromSeed(CUSTODIAN_SEED); + const wrappedSdk = await getWrappedSdkWithKeyPairForParty( + custodianPartyId, + keyPair + ); + + const instrumentId = `${custodianPartyId}#${DEFAULT_BOND_NAME}`; + + const existingInstrumentCid = + await wrappedSdk.bonds.factory.getLatestInstrument(instrumentId); + + if (existingInstrumentCid) { + console.log( + `Default bond instrument already exists: ${instrumentId} (${existingInstrumentCid.slice( + 0, + 20 + )}...)` + ); + return; + } + + const bondFactoryCid = await wrappedSdk.bonds.factory.getOrCreate( + instrumentId + ); + + const maturityDate = new Date(); + maturityDate.setSeconds( + maturityDate.getSeconds() + DEFAULT_MATURITY_SECONDS + ); + + await wrappedSdk.bonds.factory.createInstrument( + bondFactoryCid, + instrumentId, + { + depository: custodianPartyId, + notional: DEFAULT_NOTIONAL, + couponRate: DEFAULT_COUPON_RATE, + couponFrequency: DEFAULT_COUPON_FREQUENCY, + maturityDate: maturityDate.toISOString(), + } + ); + + console.log(`Default bond instrument created: ${instrumentId}`); + console.log( + ` Parameters: Notional=${DEFAULT_NOTIONAL}, Rate=${ + DEFAULT_COUPON_RATE * 100 + }%, Frequency=${DEFAULT_COUPON_FREQUENCY}x, Maturity=${DEFAULT_MATURITY_SECONDS}s` + ); +} + +async function main() { + try { + console.log("Starting bond demo setup...\n"); + + const custodianPartyId = await createParty(CUSTODIAN_SEED); + console.log(""); + const alicePartyId = await createParty(ALICE_SEED); + console.log(""); + const bobPartyId = await createParty(BOB_SEED); + console.log(""); + + await setupInfrastructure(custodianPartyId); + console.log(""); + + await createDefaultBondInstrument(custodianPartyId); + console.log(""); + + console.log("Bond demo setup complete!"); + console.log(`Custodian Party: ${custodianPartyId}`); + console.log(`Alice Party: ${alicePartyId}`); + console.log(`Bob Party: ${bobPartyId}`); + console.log( + `Default Instrument: ${custodianPartyId}#${DEFAULT_BOND_NAME}` + ); + + process.exit(0); + } catch (error) { + console.error("Setup failed:", error); + if (error instanceof Error) { + console.error(` Error: ${error.message}`); + } + process.exit(1); + } +} + +main(); diff --git a/packages/token-sdk/src/constants/templateIds.ts b/packages/token-sdk/src/constants/templateIds.ts index 1ba4c8b..dc646e9 100644 --- a/packages/token-sdk/src/constants/templateIds.ts +++ b/packages/token-sdk/src/constants/templateIds.ts @@ -30,6 +30,7 @@ export const bondLifecycleInstructionTemplateId = "#minimal-token:Bond.BondLifecycleInstruction:BondLifecycleInstruction"; export const bondLifecycleEffectTemplateId = "#minimal-token:Bond.BondLifecycleEffect:BondLifecycleEffect"; +export const bondTemplateId = "#minimal-token:Bond.Bond:Bond"; export const lockedBondTemplateId = "#minimal-token:Bond.Bond:LockedBond"; // ETF template IDs diff --git a/packages/token-sdk/src/wrappedSdk/bonds/bond.ts b/packages/token-sdk/src/wrappedSdk/bonds/bond.ts new file mode 100644 index 0000000..02f876c --- /dev/null +++ b/packages/token-sdk/src/wrappedSdk/bonds/bond.ts @@ -0,0 +1,38 @@ +import { LedgerController } from "@canton-network/wallet-sdk"; +import { ContractId } from "../../types/daml.js"; +import { bondTemplateId } from "../../constants/templateIds.js"; +import { getCreatedEventByCid } from "../../helpers/getCreatedEventByCid.js"; + +export interface BondParams { + issuer: string; + depository: string; + owner: string; + instrumentId: string; + version: string; + notional: number; + amount: number; + maturityDate: string; + couponRate: number; + couponFrequency: number; + issueDate: string; + lastEventTimestamp: string; +} + +export async function getBondContract( + ledger: LedgerController, + contractId: ContractId +): Promise { + try { + const event = await getCreatedEventByCid( + ledger, + contractId, + bondTemplateId + ); + return event?.createArgument; + } catch (error) { + if (error instanceof Error) { + return undefined; + } + return undefined; + } +} diff --git a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts index eebcb9e..e175c53 100644 --- a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts +++ b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts @@ -3,6 +3,25 @@ import { ActiveContractResponse } from "../../types/ActiveContractResponse.js"; import { ContractId, Party } from "../../types/daml.js"; import { bondLifecycleEffectTemplateId } from "../../constants/templateIds.js"; +export interface BondLifecycleEffectParams { + producedVersion: string | null; + eventType: "CouponPayment" | "Redemption"; + targetInstrumentId: string; + targetVersion: string; + eventDate: string; + amount: number; +} + +export interface BondLifecycleEffect { + contractId: ContractId; + producedVersion: string | null; + eventType: "CouponPayment" | "Redemption"; + targetInstrumentId: string; + targetVersion: string; + eventDate: string; + amount: number; +} + export async function getLatestBondLifecycleEffect( ledger: LedgerController, party: Party @@ -35,3 +54,38 @@ export async function getLatestBondLifecycleEffect( producedVersion: params.producedVersion, }; } + +export async function getAllBondLifecycleEffects( + ledger: LedgerController, + party: Party +): Promise { + const end = await ledger.ledgerEnd(); + const effects = (await ledger.activeContracts({ + offset: end.offset, + templateIds: [bondLifecycleEffectTemplateId], + filterByParty: true, + parties: [party], + })) as ActiveContractResponse[]; + + return effects + .map((contract) => { + const jsActive = contract.contractEntry.JsActiveContract; + if (!jsActive) return null; + + const createArg = jsActive.createdEvent.createArgument; + const contractId = jsActive.createdEvent.contractId; + + return { + contractId, + producedVersion: createArg.producedVersion, + eventType: createArg.eventType, + targetInstrumentId: createArg.targetInstrumentId, + targetVersion: createArg.targetVersion, + eventDate: createArg.eventDate, + amount: createArg.amount, + }; + }) + .filter( + (effect): effect is NonNullable => effect !== null + ); +} diff --git a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts index d3e7d63..d5865a5 100644 --- a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts +++ b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts @@ -131,6 +131,66 @@ export async function getBondLifecycleInstruction( return instruction.contractEntry.JsActiveContract.createdEvent; } +export interface BondLifecycleInstruction { + contractId: ContractId; + eventType: string; + lockedBond: ContractId; + bondInstrumentCid: ContractId | null; + producedVersion: string | null; + issuer: string; + holder: string; + eventDate: string; + amount: number; +} + +export async function getAllBondLifecycleInstructions( + ledger: LedgerController, + party: Party +): Promise { + const end = await ledger.ledgerEnd(); + const instructions = (await ledger.activeContracts({ + offset: end.offset, + templateIds: [bondLifecycleInstructionTemplateId], + filterByParty: true, + parties: [party], + })) as ActiveContractResponse<{ + eventType: unknown; + lockedBond: ContractId; + bondInstrumentCid: ContractId | null; + producedVersion: string | null; + issuer: string; + holder: string; + eventDate: string; + amount: number; + currencyInstrumentId: unknown; + }>[]; + + return instructions + .map((contract) => { + const jsActive = contract.contractEntry.JsActiveContract; + if (!jsActive) return null; + + const createArg = jsActive.createdEvent.createArgument; + const contractId = jsActive.createdEvent.contractId; + + return { + contractId, + eventType: createArg.eventType as string, + lockedBond: createArg.lockedBond, + bondInstrumentCid: createArg.bondInstrumentCid, + producedVersion: createArg.producedVersion, + issuer: createArg.issuer, + holder: createArg.holder, + eventDate: createArg.eventDate, + amount: createArg.amount, + }; + }) + .filter( + (instruction): instruction is NonNullable => + instruction !== null + ); +} + export async function getBondLifecycleInstructionDisclosure( issuerLedger: LedgerController, lifecycleInstructionCid: ContractId diff --git a/packages/token-sdk/src/wrappedSdk/wrappedSdk.ts b/packages/token-sdk/src/wrappedSdk/wrappedSdk.ts index e3d2f2c..6a082fb 100644 --- a/packages/token-sdk/src/wrappedSdk/wrappedSdk.ts +++ b/packages/token-sdk/src/wrappedSdk/wrappedSdk.ts @@ -1,85 +1,29 @@ +import { Types } from "@canton-network/core-ledger-client"; import { WalletSDK } from "@canton-network/wallet-sdk"; -import { UserKeyPair } from "../types/UserKeyPair.js"; import { getSdkForParty } from "../sdkHelpers.js"; +import { ContractId, Party } from "../types/daml.js"; +import { UserKeyPair } from "../types/UserKeyPair.js"; import { - createTokenFactory, - getLatestTokenFactory, - getOrCreateTokenFactory, - mintToken, - MintTokenParams, -} from "./tokenFactory.js"; -import { - createTokenRules, - getLatestTokenRules, - getOrCreateTokenRules, -} from "./tokenRules.js"; -import { - createTransferFactory, - getLatestTransferFactory, - getOrCreateTransferFactory, -} from "./transferFactory.js"; -import { + getAllBalancesByInstrumentId, + GetAllBalancesByInstrumentIdParams, getBalanceByInstrumentId, GetBalanceByInstrumentIdParams, getBalances, - getAllBalancesByInstrumentId, - GetAllBalancesByInstrumentIdParams, } from "./balances.js"; +import { getBondContract } from "./bonds/bond.js"; import { - createTransferPreapprovalProposal, - CreateTransferPreapprovalProposalParams, - getLatestTransferPreapprovalProposal, - getOrCreateTransferPreapprovalProposal, - transferPreapprovalProposalAccept, - TransferPreapprovalProposalAcceptParams, - TransferPreapprovalProposalParams, -} from "./transferPreapprovalProposal.js"; -import { - getLatestTransferPreapproval, - TransferPreapprovalParams, - transferPreapprovalSend, - TransferPreapprovalSendParams, -} from "./transferPreapproval.js"; -import { - createIssuerMintRequest, - getLatestIssuerMintRequest, - getAllIssuerMintRequests, - acceptIssuerMintRequest, - declineIssuerMintRequest, - withdrawIssuerMintRequest, - IssuerMintRequestParams, -} from "./issuerMintRequest.js"; -import { - createIssuerBurnRequest, - getLatestIssuerBurnRequest, - getAllIssuerBurnRequests, - acceptIssuerBurnRequest, - declineIssuerBurnRequest, - withdrawIssuerBurnRequest, - IssuerBurnRequestParams, -} from "./issuerBurnRequest.js"; -import { - createTransferRequest, - getLatestTransferRequest, - acceptTransferRequest, - declineTransferRequest, - withdrawTransferRequest, - getAllTransferRequests, - TransferRequestParams, -} from "./transferRequest.js"; -import { ContractId, Party } from "../types/daml.js"; -import { - acceptTransferInstruction, - rejectTransferInstruction, - TransferInstructionAcceptParams, - getLatestTokenTransferInstruction, -} from "./transferInstruction.js"; + createBondRules, + getLatestBondRules, + getOrCreateBondRules, +} from "./bonds/bondRules.js"; import { - getBondInstrumentDisclosure, - getLockedBondDisclosure, - getTransferInstructionDisclosure, -} from "./disclosure.js"; -import { Types } from "@canton-network/core-ledger-client"; + createBondFactory, + createBondInstrument, + CreateBondInstrumentParams, + getLatestBondFactory, + getLatestBondInstrument, + getOrCreateBondFactory, +} from "./bonds/factory.js"; import { acceptBondIssuerMintRequest, BondIssuerMintRequestParams, @@ -89,24 +33,6 @@ import { getLatestBondIssuerMintRequest, withdrawBondIssuerMintRequest, } from "./bonds/issuerMintRequest.js"; -import { - createBondLifecycleRule, - CreateBondLifecycleRuleParams, - getLatestBondLifecycleRule, - getOrCreateBondLifecycleRule, - processCouponPaymentEvent, - ProcessCouponPaymentEventParams, - processRedemptionEvent, - ProcessRedemptionEventParams, -} from "./bonds/lifecycleRule.js"; -import { - createBondFactory, - getLatestBondFactory, - getOrCreateBondFactory, - createBondInstrument, - getLatestBondInstrument, - CreateBondInstrumentParams, -} from "./bonds/factory.js"; import { acceptBondLifecycleClaimRequest, BondLifecycleClaimRequestParams, @@ -117,15 +43,40 @@ import { withdrawBondLifecycleClaimRequest, } from "./bonds/lifecycleClaimRequest.js"; import { - createBondRules, - getLatestBondRules, - getOrCreateBondRules, -} from "./bonds/bondRules.js"; + getAllBondLifecycleEffects, + getLatestBondLifecycleEffect, +} from "./bonds/lifecycleEffect.js"; +import { + abortBondLifecycleInstruction, + getAllBondLifecycleInstructions, + getBondLifecycleInstruction, + getBondLifecycleInstructionDisclosure, + getLatestBondLifecycleInstruction, + processBondLifecycleInstruction, +} from "./bonds/lifecycleInstruction.js"; +import { + createBondLifecycleRule, + CreateBondLifecycleRuleParams, + getLatestBondLifecycleRule, + getOrCreateBondLifecycleRule, + processCouponPaymentEvent, + ProcessCouponPaymentEventParams, + processRedemptionEvent, + ProcessRedemptionEventParams, +} from "./bonds/lifecycleRule.js"; import { createBondTransferFactory, getLatestBondTransferFactory, getOrCreateBondTransferFactory, } from "./bonds/transferFactory.js"; +import { + acceptBondTransferInstruction, + BondTransferInstructionAcceptParams, + getBondTransferInstructionDisclosure, + getLatestBondTransferInstruction, + rejectBondTransferInstruction, + withdrawBondTransferInstruction, +} from "./bonds/transferInstruction.js"; import { acceptBondTransferRequest, BondTransferRequestParams, @@ -136,60 +87,114 @@ import { withdrawBondTransferRequest, } from "./bonds/transferRequest.js"; import { - acceptBondTransferInstruction, - getBondTransferInstructionDisclosure, - getLatestBondTransferInstruction, - rejectBondTransferInstruction, - withdrawBondTransferInstruction, - BondTransferInstructionAcceptParams, -} from "./bonds/transferInstruction.js"; -import { - abortBondLifecycleInstruction, - getBondLifecycleInstruction, - getBondLifecycleInstructionDisclosure, - getLatestBondLifecycleInstruction, - processBondLifecycleInstruction, -} from "./bonds/lifecycleInstruction.js"; -import { getLatestBondLifecycleEffect } from "./bonds/lifecycleEffect.js"; + getBondInstrumentDisclosure, + getLockedBondDisclosure, + getTransferInstructionDisclosure, +} from "./disclosure.js"; import { - createPortfolioComposition, - getLatestPortfolioComposition, - getAllPortfolioCompositions, - getPortfolioComposition, - PortfolioCompositionParams, -} from "./etf/portfolioComposition.js"; + acceptEtfBurnRequest, + createEtfBurnRequest, + declineEtfBurnRequest, + EtfBurnRequestParams, + getAllEtfBurnRequests, + getLatestEtfBurnRequest, + withdrawEtfBurnRequest, +} from "./etf/burnRequest.js"; import { + addAuthorizedMinter, + AddAuthorizedMinterParams, + createAndUpdateComposition, + CreateAndUpdateCompositionParams, createMintRecipe, getLatestMintRecipe, getOrCreateMintRecipe, - addAuthorizedMinter, - removeAuthorizedMinter, - updateComposition, - createAndUpdateComposition, MintRecipeParams, - AddAuthorizedMinterParams, + removeAuthorizedMinter, RemoveAuthorizedMinterParams, + updateComposition, UpdateCompositionParams, - CreateAndUpdateCompositionParams, } from "./etf/mintRecipe.js"; import { - createEtfMintRequest, - getLatestEtfMintRequest, - getAllEtfMintRequests, acceptEtfMintRequest, + createEtfMintRequest, declineEtfMintRequest, - withdrawEtfMintRequest, EtfMintRequestParams, + getAllEtfMintRequests, + getLatestEtfMintRequest, + withdrawEtfMintRequest, } from "./etf/mintRequest.js"; import { - createEtfBurnRequest, - getLatestEtfBurnRequest, - getAllEtfBurnRequests, - acceptEtfBurnRequest, - declineEtfBurnRequest, - withdrawEtfBurnRequest, - EtfBurnRequestParams, -} from "./etf/burnRequest.js"; + createPortfolioComposition, + getAllPortfolioCompositions, + getLatestPortfolioComposition, + getPortfolioComposition, + PortfolioCompositionParams, +} from "./etf/portfolioComposition.js"; +import { + acceptIssuerBurnRequest, + createIssuerBurnRequest, + declineIssuerBurnRequest, + getAllIssuerBurnRequests, + getLatestIssuerBurnRequest, + IssuerBurnRequestParams, + withdrawIssuerBurnRequest, +} from "./issuerBurnRequest.js"; +import { + acceptIssuerMintRequest, + createIssuerMintRequest, + declineIssuerMintRequest, + getAllIssuerMintRequests, + getLatestIssuerMintRequest, + IssuerMintRequestParams, + withdrawIssuerMintRequest, +} from "./issuerMintRequest.js"; +import { + createTokenFactory, + getLatestTokenFactory, + getOrCreateTokenFactory, + mintToken, + MintTokenParams, +} from "./tokenFactory.js"; +import { + createTokenRules, + getLatestTokenRules, + getOrCreateTokenRules, +} from "./tokenRules.js"; +import { + createTransferFactory, + getLatestTransferFactory, + getOrCreateTransferFactory, +} from "./transferFactory.js"; +import { + acceptTransferInstruction, + getLatestTokenTransferInstruction, + rejectTransferInstruction, + TransferInstructionAcceptParams, +} from "./transferInstruction.js"; +import { + getLatestTransferPreapproval, + TransferPreapprovalParams, + transferPreapprovalSend, + TransferPreapprovalSendParams, +} from "./transferPreapproval.js"; +import { + createTransferPreapprovalProposal, + CreateTransferPreapprovalProposalParams, + getLatestTransferPreapprovalProposal, + getOrCreateTransferPreapprovalProposal, + transferPreapprovalProposalAccept, + TransferPreapprovalProposalAcceptParams, + TransferPreapprovalProposalParams, +} from "./transferPreapprovalProposal.js"; +import { + acceptTransferRequest, + createTransferRequest, + declineTransferRequest, + getAllTransferRequests, + getLatestTransferRequest, + TransferRequestParams, + withdrawTransferRequest, +} from "./transferRequest.js"; export const getWrappedSdk = (sdk: WalletSDK) => { if (!sdk.userLedger) { @@ -459,6 +464,8 @@ export const getWrappedSdk = (sdk: WalletSDK) => { getBondLifecycleInstruction(userLedger, contractId), getLatest: (party: Party) => getLatestBondLifecycleInstruction(userLedger, party), + getAll: (party: Party) => + getAllBondLifecycleInstructions(userLedger, party), getDisclosure: (contractId: ContractId) => getBondLifecycleInstructionDisclosure( userLedger, @@ -468,6 +475,12 @@ export const getWrappedSdk = (sdk: WalletSDK) => { lifecycleEffect: { getLatest: (party: Party) => getLatestBondLifecycleEffect(userLedger, party), + getAll: (party: Party) => + getAllBondLifecycleEffects(userLedger, party), + }, + bond: { + get: (contractId: ContractId) => + getBondContract(userLedger, contractId), }, }, tokenFactory: { @@ -994,6 +1007,8 @@ export const getWrappedSdkWithKeyPair = ( getBondLifecycleInstruction(userLedger, contractId), getLatest: (party: Party) => getLatestBondLifecycleInstruction(userLedger, party), + getAll: (party: Party) => + getAllBondLifecycleInstructions(userLedger, party), getDisclosure: (contractId: ContractId) => getBondLifecycleInstructionDisclosure( userLedger, @@ -1003,6 +1018,12 @@ export const getWrappedSdkWithKeyPair = ( lifecycleEffect: { getLatest: (party: Party) => getLatestBondLifecycleEffect(userLedger, party), + getAll: (party: Party) => + getAllBondLifecycleEffects(userLedger, party), + }, + bond: { + get: (contractId: ContractId) => + getBondContract(userLedger, contractId), }, }, tokenFactory: {