Files
agents-marketplace/tests/unit/tx-anchor-metadata.test.ts

228 lines
6.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import type { LiveOgmiosProvider } from "../../packages/shared/src/chain/LiveOgmiosProvider.js";
import type { WalletKey } from "../../packages/shared/src/tx/types.js";
const mocks = vi.hoisted(() => ({
createLucidContext: vi.fn(),
providerConstructor: vi.fn(),
}));
vi.mock(
"../../packages/shared/src/chain/OgmiosLucidProvider.js",
() => ({
OgmiosLucidProvider: class {
constructor(options: unknown) {
mocks.providerConstructor(options);
}
},
}),
);
vi.mock(
"../../packages/shared/src/tx/internal/lucidContext.js",
() => ({ createLucidContext: mocks.createLucidContext }),
);
import {
buildAnchorMetadataTx,
SURPLUS_SALE_PROOF_METADATA_LABEL,
SURPLUS_SALE_PROOF_PROTOCOL,
type SurplusSaleProofAnchorMetadata,
} from "../../packages/shared/src/tx/wallet/anchorMetadata.js";
const walletKey: WalletKey = {
address: "addr1wallet",
pubKeyHash: "1".repeat(56),
pubKeyHex: "2".repeat(64),
privateKeyHex: "3".repeat(64),
};
function validMetadata(): SurplusSaleProofAnchorMetadata {
return {
p: SURPLUS_SALE_PROOF_PROTOCOL,
root: "a".repeat(64),
count: 1_000,
first: "sale-000001",
last: "sale-001000",
};
}
interface LucidHarness {
chain: LiveOgmiosProvider;
submitTx: Mock;
getUtxos: Mock;
newTx: Mock;
attachMetadata: Mock;
addSignerKey: Mock;
setMinFee: Mock;
completeTx: Mock;
withWallet: Mock;
completeSignature: Mock;
payToAddress: Mock;
}
function lucidHarness(): LucidHarness {
const submitTx = vi.fn().mockResolvedValue("submitted-hash");
const chain = {
url: "http://ogmios.test",
fetchImpl: undefined,
submitTx,
} as unknown as LiveOgmiosProvider;
const realWalletUtxos = [
{ txHash: "f".repeat(64), outputIndex: 0, assets: { lovelace: 25_000_000n } },
];
const getUtxos = vi.fn().mockResolvedValue(realWalletUtxos);
const completeSignature = vi.fn().mockResolvedValue({
toCBOR: () => "signed-anchor-cbor",
toHash: () => "b".repeat(64),
});
const withWallet = vi.fn(() => ({ complete: completeSignature }));
const completeTx = vi.fn().mockResolvedValue({ sign: { withWallet } });
const payToAddress = vi.fn();
const txBuilder: Record<string, unknown> = {
pay: { ToAddress: payToAddress },
};
const attachMetadata = vi.fn(() => txBuilder);
const addSignerKey = vi.fn(() => txBuilder);
const setMinFee = vi.fn(() => txBuilder);
Object.assign(txBuilder, {
attachMetadata,
addSignerKey,
setMinFee,
complete: completeTx,
});
const newTx = vi.fn(() => txBuilder);
const lucid = {
wallet: vi.fn(() => ({ getUtxos })),
newTx,
};
mocks.createLucidContext.mockResolvedValue({ lucid });
return {
chain,
submitTx,
getUtxos,
newTx,
attachMetadata,
addSignerKey,
setMinFee,
completeTx,
withWallet,
completeSignature,
payToAddress,
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("buildAnchorMetadataTx", () => {
it("signs label-674 metadata with real wallet inputs and submits once", async () => {
const harness = lucidHarness();
const metadata = validMetadata();
const result = await buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata,
});
expect(mocks.providerConstructor).toHaveBeenCalledWith({
ogmiosUrl: "http://ogmios.test",
fetch: undefined,
});
expect(mocks.createLucidContext).toHaveBeenCalledWith(
expect.anything(),
walletKey,
{ networkId: 1, systemStartUnix: 0, slotLengthMs: 1000 },
{ usePresetProtocolParameters: true },
);
expect(harness.getUtxos).toHaveBeenCalledOnce();
expect(harness.newTx).toHaveBeenCalledOnce();
expect(harness.attachMetadata).toHaveBeenCalledWith(
SURPLUS_SALE_PROOF_METADATA_LABEL,
metadata,
);
expect(harness.addSignerKey).toHaveBeenCalledWith(walletKey.pubKeyHash);
expect(harness.setMinFee).toHaveBeenCalledWith(500_000n);
expect(harness.completeTx).toHaveBeenCalledWith({
presetWalletInputs: expect.arrayContaining([
expect.objectContaining({ assets: { lovelace: 25_000_000n } }),
]),
localUPLCEval: false,
});
expect(harness.withWallet).toHaveBeenCalledOnce();
expect(harness.completeSignature).toHaveBeenCalledOnce();
expect(harness.payToAddress).not.toHaveBeenCalled();
expect(harness.submitTx).toHaveBeenCalledOnce();
expect(harness.submitTx).toHaveBeenCalledWith("signed-anchor-cbor");
expect(result).toEqual({
txCborHex: "signed-anchor-cbor",
expectedTxHash: "b".repeat(64),
});
});
it.each([
["wrong protocol", { ...validMetadata(), p: "surplus-sale-proof-v2" }],
["short root", { ...validMetadata(), root: "a".repeat(63) }],
["uppercase root", { ...validMetadata(), root: "A".repeat(64) }],
["non-hex root", { ...validMetadata(), root: "z".repeat(64) }],
["zero count", { ...validMetadata(), count: 0 }],
["fractional count", { ...validMetadata(), count: 1.5 }],
["unsafe count", { ...validMetadata(), count: Number.MAX_SAFE_INTEGER + 1 }],
["empty first ID", { ...validMetadata(), first: "" }],
["control character in last ID", { ...validMetadata(), last: "sale\n2" }],
["non-ASCII first ID", { ...validMetadata(), first: "salé-1" }],
["extra field", { ...validMetadata(), extra: true }],
])("rejects %s before initializing Lucid", async (_case, metadata) => {
const harness = lucidHarness();
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: metadata as SurplusSaleProofAnchorMetadata,
}),
).rejects.toMatchObject({ reason: "anchor_metadata_invalid" });
expect(mocks.providerConstructor).not.toHaveBeenCalled();
expect(mocks.createLucidContext).not.toHaveBeenCalled();
expect(harness.submitTx).not.toHaveBeenCalled();
});
it("rejects metadata text over 64 UTF-8 bytes before initializing Lucid", async () => {
const harness = lucidHarness();
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: { ...validMetadata(), last: "x".repeat(65) },
}),
).rejects.toMatchObject({ reason: "anchor_metadata_oversized" });
expect(mocks.providerConstructor).not.toHaveBeenCalled();
expect(mocks.createLucidContext).not.toHaveBeenCalled();
expect(harness.submitTx).not.toHaveBeenCalled();
});
it("does not submit when transaction construction fails", async () => {
const harness = lucidHarness();
harness.completeTx.mockRejectedValueOnce(new Error("coin selection failed"));
await expect(
buildAnchorMetadataTx({
chain: harness.chain,
walletKey,
metadata: validMetadata(),
}),
).rejects.toMatchObject({
reason: "anchor_metadata_build_failed",
message: "coin selection failed",
});
expect(harness.submitTx).not.toHaveBeenCalled();
});
});