-
Notifications
You must be signed in to change notification settings - Fork 0
feat: cardano intrgration #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,3 +2,4 @@ | |
| node_modules/ | ||
| .env | ||
| /src/generated/prisma | ||
| /dist | ||
Large diffs are not rendered by default.
| 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" | ||
| } | ||
| } |
Large diffs are not rendered by default.
| 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; | ||
| } | ||
|
|
||
| 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(); | ||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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]; |
There was a problem hiding this comment.
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 ingetInstance(). This could causethis.agentAddressandthis.agentUtxoto be undefined when methods are called immediately after getting the instance.