Skip to content
Closed
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
113 changes: 113 additions & 0 deletions packages/core/src/skill-market/ingest/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { marketplaceEnv } from "./env.js";
import { publishSkill as corePublishSkill } from "../../nft/skill.js";
import { publishWorkflow as corePublishWorkflow } from "../../nft/workflow.js";

vi.mock("../../nft/skill.js", () => ({
publishSkill: vi.fn().mockResolvedValue("mockSkillMint"),
buySkill: vi.fn(),
}));

vi.mock("../../nft/workflow.js", () => ({
publishWorkflow: vi.fn().mockResolvedValue("mockWorkflowMint"),
}));

vi.mock("../../core/chain.js", () => ({
init: vi.fn(),
signerAddress: vi.fn().mockResolvedValue("mockAddress"),
}));

vi.mock("../../core/rpc.js", () => ({
resolveRpcUrl: vi.fn().mockResolvedValue("http://localhost:8899"),
}));

describe("skill-market/ingest/env publish", () => {
const mockWallet = { address: "mockWalletAddress" } as any;

beforeEach(() => {
vi.clearAllMocks();
});

it("routes workflow frontmatter to publishWorkflow", async () => {
const text = `---
type: workflow
requiredSkills: [skillMint1, skillMint2]
---
Some workflow body`;

const env = await marketplaceEnv(mockWallet);
const result = await env.publishSkill({
name: "My Workflow",
description: "A workflow",
text,
category: "testing",
hashtags: ["test", "workflow"],
priceSol: "0.25",
});

expect(result).toEqual({ ok: true, mint: "mockWorkflowMint" });
expect(corePublishWorkflow).toHaveBeenCalledWith(expect.any(Object), mockWallet, {
name: "My Workflow",
description: "A workflow",
text,
requiredSkills: ["skillMint1", "skillMint2"],
category: "testing",
hashtags: ["test", "workflow"],
price: 250000000n,
});
expect(corePublishSkill).not.toHaveBeenCalled();
});

it("routes skill frontmatter to publishSkill", async () => {
const text = `---
type: skill
---
Some skill body`;

const env = await marketplaceEnv(mockWallet);
const result = await env.publishSkill({
name: "My Skill",
description: "A skill",
text,
category: "testing",
hashtags: ["test"],
priceSol: "0.1",
});

expect(result).toEqual({ ok: true, mint: "mockSkillMint" });
expect(corePublishSkill).toHaveBeenCalledWith(expect.any(Object), mockWallet, {
name: "My Skill",
description: "A skill",
text,
category: "testing",
hashtags: ["test"],
price: 100000000n,
image: undefined,
});
expect(corePublishWorkflow).not.toHaveBeenCalled();
});

it("routes missing frontmatter to publishSkill", async () => {
const text = "Pure markdown without frontmatter";

const env = await marketplaceEnv(mockWallet);
const result = await env.publishSkill({
name: "My Skill",
description: "A skill",
text,
priceSol: "1",
});

expect(result).toEqual({ ok: true, mint: "mockSkillMint" });
expect(corePublishSkill).toHaveBeenCalledWith(expect.any(Object), mockWallet, {
name: "My Skill",
description: "A skill",
text,
category: undefined,
hashtags: undefined,
price: 1000000000n,
image: undefined,
});
expect(corePublishWorkflow).not.toHaveBeenCalled();
});
});
41 changes: 41 additions & 0 deletions packages/core/src/skill-market/ingest/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { Wallet } from "../../runtime/contract.js";
import { searchSkills } from "../../search/index.js";
import { dasSource, indexerSource, ownedSkillMints } from "../../core/skillSource.js";
import { buySkill, publishSkill as corePublishSkill } from "../../nft/skill.js";
import { publishWorkflow as corePublishWorkflow } from "../../nft/workflow.js";
import { getSolBalance } from "../../notes/index.js";
import { readSkillText, readSkillMintMetadata } from "../../nft/token2022.js";
import { heldSkillCreators } from "../../notes/holdings.js";
Expand Down Expand Up @@ -87,6 +88,33 @@ export function solToLamports(sol: string): bigint | null {
return BigInt(whole) * LAMPORTS_PER_SOL + BigInt(frac.padEnd(9, "0") || "0");
}

function publishFrontmatter(text: string): { type?: string; requiredSkills?: string[] } {
const lines = text.split("\n");
if (lines[0]?.trim() !== "---") return {};
let closeIdx = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === "---") { closeIdx = i; break; }
}
if (closeIdx === -1) return {};

const out: { type?: string; requiredSkills?: string[] } = {};
for (const line of lines.slice(1, closeIdx)) {
const colon = line.indexOf(":");
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
const raw = line.slice(colon + 1).trim();
if (key === "type") out.type = raw.replace(/^['"]|['"]$/g, "");
if (key === "requiredSkills" && raw.startsWith("[") && raw.endsWith("]")) {
out.requiredSkills = raw
.slice(1, -1)
.split(",")
.map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
.filter(Boolean);
}
}
return out;
}

export async function marketplaceEnv(wallet: Wallet) {
const conn = new Connection(await resolveRpcUrl(), "confirmed");
// Wire the chain layer's module-level connection. Writes (publishSkill -> codeIn,
Expand Down Expand Up @@ -222,6 +250,19 @@ export async function marketplaceEnv(wallet: Wallet) {
try {
const lamports = solToLamports(input.priceSol);
if (lamports === null) return { ok: false, error: "Enter a valid price in SOL (e.g. 0.1)" };
const frontmatter = publishFrontmatter(input.text);
if (frontmatter.type === "workflow") {
const mint = await corePublishWorkflow(conn, wallet, {
name: input.name,
description: input.description,
text: input.text,
requiredSkills: frontmatter.requiredSkills ?? [],
category: input.category,
hashtags: input.hashtags,
price: lamports,
});
return { ok: true, mint };
}
const mint = await corePublishSkill(conn, wallet, {
name: input.name,
description: input.description,
Expand Down