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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
FRONTEND_URL=
NEON_DB_CONNECTION_STRING=
DATABASE_URL=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
JWT_SECRET=
BLACKFROST_PROJECT_ID=
WALLET_PRIVATE_KEY=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
node_modules/
.env
/src/generated/prisma
/dist
199 changes: 191 additions & 8 deletions bun.lock

Large diffs are not rendered by default.

55 changes: 29 additions & 26 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
{
"name": "dagent-api",
"scripts": {
"dev": "bun run --hot src/index.ts",
"db:deploy": "bunx prisma migrate deploy && bunx prisma generate"
},
"dependencies": {
"@meshsdk/core": "^1.9.0-beta.87",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/node": "^24.10.1",
"name": "dagent-api",
"scripts": {
"dev": "bun run --hot src/index.ts",
"build": "bunx tsc --noEmit",
"db:deploy": "bunx prisma migrate deploy && bunx prisma generate"
},
"dependencies": {
"@blockfrost/blockfrost-js": "^6.0.0",
"@meshsdk/core": "^1.9.0-beta.87",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/node": "^24.10.1",
"@upstash/redis": "^1.35.7",
"axios": "^1.13.2",
"better-auth": "^1.3.16",
"axios": "^1.13.2",
"better-auth": "^1.3.16",
"chromadb": "^3.1.6",
"dotenv": "^17.2.3",
"ethers": "^6.15.0",
"hono": "^4.9.8",
"pg": "^8.16.3",
"prisma": "^7.0.1",
"sharp": "^0.34.4",
"zod": "^4.1.11"
},
"devDependencies": {
"@types/bun": "latest",
"@types/dotenv": "^8.2.3",
"@types/pg": "^8.15.6"
}
}
"dotenv": "^17.2.3",
"ethers": "^6.15.0",
"hono": "^4.9.8",
"lucid-cardano": "^0.10.11",
"pg": "^8.16.3",
"prisma": "^7.0.1",
"sharp": "^0.34.4",
"zod": "^4.1.11"
},
"devDependencies": {
"@types/bun": "latest",
"@types/dotenv": "^8.2.3",
"@types/pg": "^8.15.6"
}
}
376 changes: 376 additions & 0 deletions src/lib/contracts/cardano/abis/plutus.json

Large diffs are not rendered by default.

259 changes: 259 additions & 0 deletions src/lib/contracts/cardano/agent.contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { CardanoContractClient } from "./contract";
import AgentPlutusAbi from "./abis/plutus.json";
import { config } from "../../env";
import { Data, Script, UTxO } from "lucid-cardano";
import {
Agent,
AgentDatum,
AgentDatumSchema,
AgentRedeemer,
} from "./schema/agent-contract.types";

class AgentContractClient extends CardanoContractClient {
public static instance: AgentContractClient;
private walletAddress: string;
private agentUtxo!: UTxO[];
private agentValidator: Script;
private agentAddress!: string;

constructor() {
super({ projectId: config.BLACKFROST_PROJECT_ID });
this.walletAddress = config.WALLET_PRIVATE_KEY;
this.agentValidator = {
type: "PlutusV2",
script: AgentPlutusAbi.validators[0].compiledCode,
};
}

async initialize() {
await super.initialize();
this.lucid.selectWalletFromPrivateKey(this.walletAddress);
this.agentAddress = this.lucid.utils.validatorToAddress(
this.agentValidator
);
this.agentUtxo = await this.lucid.utxosAt(this.agentAddress);
}

public static async getInstance(): Promise<AgentContractClient> {
if (!AgentContractClient.instance) {
AgentContractClient.instance = new AgentContractClient();
await AgentContractClient.instance.initialize();
}
return AgentContractClient.instance;
}
Comment on lines +37 to +43
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same initialization issue exists here. The initialize() method is async but not awaited when creating a new instance in getInstance(). This could cause this.agentAddress and this.agentUtxo to be undefined when methods are called immediately after getting the instance.

Copilot uses AI. Check for mistakes.

private async fetchAgentUtxo(): Promise<UTxO[]> {
const utxos = await this.lucid.utxosAt(this.agentAddress);

if (!utxos || utxos.length === 0)
throw new Error(
"No contract UTxOs found. Contract not deployed / no state."
);

return utxos;
}

async registerAgent({
agentAddress,
provider,
agentIdHash,
owner,
metadataUri,
}: {
agentAddress: string;
provider: string;
agentIdHash: string;
owner: string;
metadataUri: string;
}): Promise<string> {
const agentUtxo = await this.fetchAgentUtxo();

if (!agentUtxo[0]?.datum) {
throw new Error("Agent UTxO datum is missing");
}

const currentDatum = Data.from(
agentUtxo[0].datum,
AgentDatumSchema
) as unknown as AgentDatum;

const timestamp = BigInt(Date.now());
const newAgent: Agent = {
agent_address: agentAddress,
provider: provider,
agent_id_hash: agentIdHash,
owner: owner,
is_active: true,
created_at: timestamp,
updated_at: timestamp,
metadata_uri: metadataUri,
};

const newDatum: AgentDatum = {
contract_owner: currentDatum.contract_owner,
agents: [...currentDatum.agents, newAgent],
};

const datumPlutus = Data.to(newDatum as any);

const redeemer: AgentRedeemer = {
RegisterAgent: {
agent_address: agentAddress,
agent_id_hash: agentIdHash,
owner: owner,
metadata_uri: metadataUri,
},
};

const redeemerPlutus = Data.to(redeemer as any);

const tx = await this.lucid
.newTx()
.collectFrom(this.agentUtxo, redeemerPlutus)
.attachSpendingValidator(this.agentValidator)
.payToContract(
this.agentAddress,
{ inline: datumPlutus },
{
lovelace: this.agentUtxo[0].assets.lovelace,
}
)
.complete();

if (!tx) {
throw new Error("Transaction creation failed");
}

const signed = await tx.sign().complete();
const txHash = await signed.submit();

return txHash;
}

async updateAgent({
agentAddress,
isActive,
metadataUri,
}: {
agentAddress: string;
isActive: boolean;
metadataUri: string;
}): Promise<string> {
const agentUtxo = await this.fetchAgentUtxo();
const utxo = agentUtxo[0];

if (!utxo.datum) {
throw new Error("Agent UTxO datum is missing");
}

const currentDatum = Data.from(
utxo.datum,
AgentDatumSchema
) as unknown as AgentDatum;

const agentIndex = currentDatum.agents.findIndex(
(a) => a.agent_address === agentAddress
);

if (agentIndex === -1) {
throw new Error(`Agent with address ${agentAddress} not found`);
}

const updatedAgents = [...currentDatum.agents];
updatedAgents[agentIndex] = {
...updatedAgents[agentIndex],
is_active: isActive,
metadata_uri: metadataUri,
updated_at: BigInt(Date.now()),
};

const newDatum: AgentDatum = {
contract_owner: currentDatum.contract_owner,
agents: updatedAgents,
};

const datumPlutus = Data.to(newDatum as any);

const redeemer: AgentRedeemer = {
UpdateAgent: {
agent_address: agentAddress,
is_active: isActive,
metadata_uri: metadataUri,
},
};

const redeemerPlutus = Data.to(redeemer as any);

const tx = await this.lucid
.newTx()
.collectFrom(this.agentUtxo, redeemerPlutus)
.attachSpendingValidator(this.agentValidator)
.payToContract(
this.agentAddress,
{ inline: datumPlutus },
{
lovelace: this.agentUtxo[0].assets.lovelace,
}
)
.complete();

if (!tx) {
throw new Error("Transaction creation failed");
}

const signed = await tx.sign().complete();
const txHash = await signed.submit();

return txHash;
}

async getAgent(agentAddress: string): Promise<Agent | null> {
const agentUtxo = await this.fetchAgentUtxo();
if (!agentUtxo[0]?.datum) {
throw new Error("Agent UTxO datum is missing");
}

const currentDatum = Data.from(
agentUtxo[0].datum,
AgentDatumSchema
) as unknown as AgentDatum;

const agent = currentDatum.agents.find(
(a) => a.agent_address === agentAddress
);

return agent || null;
}

async getAllAgents(): Promise<Agent[]> {
const agentUtxo = await this.fetchAgentUtxo();

if (!agentUtxo[0]?.datum) {
throw new Error("Agent UTxO datum is missing");
}

const currentDatum = Data.from(
agentUtxo[0].datum,
AgentDatumSchema
) as unknown as AgentDatum;

return currentDatum.agents;
}

async getAgentsByOwner(owner: string): Promise<Agent[]> {
const allAgents = await this.getAllAgents();
return allAgents.filter((agent) => agent.owner === owner);
}

async getActiveAgents(): Promise<Agent[]> {
const allAgents = await this.getAllAgents();
return allAgents.filter((agent) => agent.is_active);
}

async getAgentsByProvider(provider: string): Promise<Agent[]> {
const allAgents = await this.getAllAgents();
return allAgents.filter((agent) => agent.provider === provider);
}
}

export const agentContract = AgentContractClient.getInstance();
34 changes: 34 additions & 0 deletions src/lib/contracts/cardano/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Blockfrost, Lucid } from "lucid-cardano";

enum Network {
Mainnet = "Mainnet",
Preview = "Preview",
Preprod = "Preprod",
}

enum NetworkURI {
Mainnet = "https://cardano-mainnet.blockfrost.io/api/v0",
Preview = "https://cardano-preview.blockfrost.io/api/v0",
Preprod = "https://cardano-preprod.blockfrost.io/api/v0",
}

export class CardanoContractClient {
private network: Network.Preview;
private cardanoNodeUrl = NetworkURI[Network.Preview];
public lucid!: Lucid;
public blockfrost!: Blockfrost;

constructor(private config: { projectId: string }) {
this.network = Network.Preview;
Comment on lines +16 to +22
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The hardcoded network type Network.Preview limits flexibility. Consider making the network configurable through environment variables to support different environments (Mainnet, Preprod, Preview).

Suggested change
private network: Network.Preview;
private cardanoNodeUrl = NetworkURI[Network.Preview];
public lucid!: Lucid;
public blockfrost!: Blockfrost;
constructor(private config: { projectId: string }) {
this.network = Network.Preview;
private network: Network;
private cardanoNodeUrl: string;
public lucid!: Lucid;
public blockfrost!: Blockfrost;
constructor(private config: { projectId: string }) {
const envNetwork = process.env.CARDANO_NETWORK;
if (
envNetwork === Network.Mainnet ||
envNetwork === Network.Preprod ||
envNetwork === Network.Preview
) {
this.network = envNetwork as Network;
} else {
this.network = Network.Preview;
}
this.cardanoNodeUrl = NetworkURI[this.network];

Copilot uses AI. Check for mistakes.
}

async initialize() {
if (!this.lucid) {
this.blockfrost = new Blockfrost(
this.cardanoNodeUrl,
this.config.projectId
);
this.lucid = await Lucid.new(this.blockfrost, this.network);
}
}
}
Loading