Skip to content
164 changes: 164 additions & 0 deletions test/unit/operation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { describe, it, expect } from "vitest";
import { Operation } from "../../src/operation.js";
import xdr from "../../src/xdr.js";

describe("Operation._checkUnsignedIntValue()", () => {
it("returns correct values for valid inputs", () => {
const cases: Array<{
value: number | string | undefined;
expected: number | undefined;
}> = [
{ value: 0, expected: 0 },
{ value: 10, expected: 10 },
{ value: "0", expected: 0 },
{ value: "10", expected: 10 },
{ value: undefined, expected: undefined },
];

for (const { value, expected } of cases) {
expect(Operation._checkUnsignedIntValue("field", value)).toBe(expected);
}
});

it("throws for invalid values", () => {
const invalids: unknown[] = [
{},
[],
"",
"test",
"0.5",
"-10",
"-10.5",
"Infinity",
Infinity,
"Nan",
NaN,
];

for (const value of invalids) {
expect(() =>
Operation._checkUnsignedIntValue("field", value as number),
).toThrow();
}
});

it("applies isValidFunction when provided", () => {
const lessThan10 = (v: number) => v < 10;

expect(
Operation._checkUnsignedIntValue("field", undefined, lessThan10),
).toBe(undefined);

expect(Operation._checkUnsignedIntValue("field", 8, lessThan10)).toBe(8);
expect(Operation._checkUnsignedIntValue("field", "8", lessThan10)).toBe(8);

expect(() =>
Operation._checkUnsignedIntValue("field", 12, lessThan10),
).toThrow();
expect(() =>
Operation._checkUnsignedIntValue("field", "12", lessThan10),
).toThrow();
});
});

describe("Operation.isValidAmount()", () => {
it("returns true for valid amounts", () => {
const valid = ["10", "0.10", "0.1234567", "922337203685.4775807"];
for (const amount of valid) {
expect(Operation.isValidAmount(amount)).toBe(true);
}
});

it("returns false for invalid amounts", () => {
const invalid: unknown[] = [
100,
100.5,
"",
"test",
"0",
"-10",
"-10.5",
"0.12345678",
"922337203685.4775808",
"Infinity",
Infinity,
"Nan",
NaN,
];
for (const amount of invalid) {
expect(Operation.isValidAmount(amount as string)).toBe(false);
}
});

it("allows 0 only when allowZero is true", () => {
expect(Operation.isValidAmount("0")).toBe(false);
expect(Operation.isValidAmount("0", true)).toBe(true);
});
});

describe("Operation._fromXDRAmount()", () => {
it("correctly parses XDR amounts", () => {
expect(Operation._fromXDRAmount(xdr.Int64.fromString("1"))).toBe(
"0.0000001",
);
expect(Operation._fromXDRAmount(xdr.Int64.fromString("10000000"))).toBe(
"1.0000000",
);
expect(Operation._fromXDRAmount(xdr.Int64.fromString("10000000000"))).toBe(
"1000.0000000",
);
expect(
Operation._fromXDRAmount(xdr.Int64.fromString("1000000000000000000")),
).toBe("100000000000.0000000");
});
});

describe("Operation._toXDRAmount()", () => {
it("correctly converts string amounts to XDR Int64", () => {
expect(Operation._toXDRAmount("0.0000001").toString()).toBe("1");
expect(Operation._toXDRAmount("1.0000000").toString()).toBe("10000000");
expect(Operation._toXDRAmount("1000.0000000").toString()).toBe(
"10000000000",
);
expect(Operation._toXDRAmount("100000000000.0000000").toString()).toBe(
"1000000000000000000",
);
});
});

describe("Operation._fromXDRPrice()", () => {
it("converts an XDR Price to a decimal string", () => {
expect(Operation._fromXDRPrice(new xdr.Price({ n: 1, d: 2 }))).toBe("0.5");
expect(Operation._fromXDRPrice(new xdr.Price({ n: 11, d: 10 }))).toBe(
"1.1",
);
expect(Operation._fromXDRPrice(new xdr.Price({ n: 1, d: 1 }))).toBe("1");
});
});

describe("Operation._toXDRPrice()", () => {
it("converts a string price to XDR", () => {
const price = Operation._toXDRPrice("0.5");
expect(price.n() / price.d()).toBeCloseTo(0.5);
});

it("converts a number price to XDR", () => {
const price = Operation._toXDRPrice(1.5);
expect(price.n() / price.d()).toBeCloseTo(1.5);
});

it("converts a {n, d} fraction to XDR", () => {
const price = Operation._toXDRPrice({ n: 11, d: 10 });
expect(price.n()).toBe(11);
expect(price.d()).toBe(10);
});

it("throws for a negative price", () => {
expect(() => Operation._toXDRPrice({ n: -1, d: 10 })).toThrow(
/price must be positive/,
);
expect(() => Operation._toXDRPrice({ n: 1, d: -10 })).toThrow(
/price must be positive/,
);
});
});
42 changes: 42 additions & 0 deletions test/unit/operations/bump_sequence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect } from "vitest";
import { Operation } from "../../../src/operation.js";
import xdr from "../../../src/xdr.js";

describe("Operation.bumpSequence()", () => {
it("creates a bumpSequence operation", () => {
const opts = { bumpTo: "77833036561510299" };
const op = Operation.bumpSequence(opts);
const xdrHex = op.toXDR("hex");
const operation = xdr.Operation.fromXDR(xdrHex, "hex");
const obj = Operation.fromXDRObject(operation);

expect(obj.type).toBe("bumpSequence");
if (obj.type !== "bumpSequence") throw new Error("unexpected type");

expect(obj.bumpTo).toBe(opts.bumpTo);
});

it("fails when bumpTo is not a string", () => {
expect(() =>
// @ts-expect-error: intentionally passing non-string to test runtime validation
Operation.bumpSequence({ bumpTo: 1000 }),
).toThrow(/bumpTo must be a string/);
});

it("fails when bumpTo is not a stringified number", () => {
expect(() => Operation.bumpSequence({ bumpTo: "not-a-number" })).toThrow(
/bumpTo must be a stringified number/,
);
});

it("preserves an optional source account", () => {
const source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
const op = Operation.bumpSequence({ bumpTo: "100", source });
const obj = Operation.fromXDRObject(
xdr.Operation.fromXDR(op.toXDR("hex"), "hex"),
);

if (obj.type !== "bumpSequence") throw new Error("unexpected type");
expect(obj.source).toBe(source);
});
});
119 changes: 119 additions & 0 deletions test/unit/operations/change_trust.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, it, expect } from "vitest";
import { Operation } from "../../../src/operation.js";
import { Asset } from "../../../src/asset.js";
import { LiquidityPoolAsset } from "../../../src/liquidity_pool_asset.js";
import { LiquidityPoolFeeV18 } from "../../../src/get_liquidity_pool_id.js";
import xdr from "../../../src/xdr.js";

const usd = new Asset(
"USD",
"GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7",
);

const lpAsset = new LiquidityPoolAsset(
new Asset("ARST", "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB"),
new Asset("USD", "GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7"),
LiquidityPoolFeeV18,
);

describe("Operation.changeTrust()", () => {
it("creates a changeTrustOp with Asset using default limit (MAX_INT64)", () => {
const op = Operation.changeTrust({ asset: usd });
const xdrHex = op.toXDR("hex");
const operation = xdr.Operation.fromXDR(xdrHex, "hex");
const obj = Operation.fromXDRObject(operation);

expect(obj.type).toBe("changeTrust");
if (obj.type !== "changeTrust") throw new Error("unexpected type");

expect(obj.line).toEqual(usd);
expect(
(operation.body().value() as xdr.ChangeTrustOp).limit().toString(),
).toBe("9223372036854775807");
expect(obj.limit).toBe("922337203685.4775807");
});

it("creates a changeTrustOp with Asset and explicit limit", () => {
const op = Operation.changeTrust({ asset: usd, limit: "50.0000000" });
const xdrHex = op.toXDR("hex");
const operation = xdr.Operation.fromXDR(xdrHex, "hex");
const obj = Operation.fromXDRObject(operation);

expect(obj.type).toBe("changeTrust");
if (obj.type !== "changeTrust") throw new Error("unexpected type");

expect(obj.line).toEqual(usd);
expect(
(operation.body().value() as xdr.ChangeTrustOp).limit().toString(),
).toBe("500000000");
expect(obj.limit).toBe("50.0000000");
});

it("creates a changeTrustOp with LiquidityPoolAsset using default limit (MAX_INT64)", () => {
const op = Operation.changeTrust({ asset: lpAsset });
expect(op).toBeInstanceOf(xdr.Operation);

const xdrOp = xdr.Operation.fromXDR(op.toXDR("hex"), "hex");
const obj = Operation.fromXDRObject(xdrOp);

expect(obj.type).toBe("changeTrust");
if (obj.type !== "changeTrust") throw new Error("unexpected type");

expect(obj.line).toEqual(lpAsset);
expect(
(xdrOp.body().value() as xdr.ChangeTrustOp).limit().toString(),
).toBe("9223372036854775807");
expect(obj.limit).toBe("922337203685.4775807");
});

it("deletes an Asset trustline by setting limit to 0", () => {
const op = Operation.changeTrust({ asset: usd, limit: "0.0000000" });
const obj = Operation.fromXDRObject(
xdr.Operation.fromXDR(op.toXDR("hex"), "hex"),
);

expect(obj.type).toBe("changeTrust");
if (obj.type !== "changeTrust") throw new Error("unexpected type");

expect(obj.line).toEqual(usd);
expect(obj.limit).toBe("0.0000000");
});

it("deletes a LiquidityPoolAsset trustline by setting limit to 0", () => {
const op = Operation.changeTrust({ asset: lpAsset, limit: "0.0000000" });
const obj = Operation.fromXDRObject(
xdr.Operation.fromXDR(op.toXDR("hex"), "hex"),
);

expect(obj.type).toBe("changeTrust");
if (obj.type !== "changeTrust") throw new Error("unexpected type");

expect(obj.line).toEqual(lpAsset);
expect(obj.limit).toBe("0.0000000");
});

it("throws TypeError for a non-string limit", () => {
expect(() =>
// @ts-expect-error: intentionally passing non-string limit to test runtime validation
Operation.changeTrust({ asset: usd, limit: 0 }),
).toThrow(TypeError);
});

it("throws TypeError for an invalid asset type", () => {
expect(() =>
// @ts-expect-error: intentionally passing invalid asset to test runtime validation
Operation.changeTrust({ asset: "not-an-asset" }),
).toThrow(TypeError);
});

it("preserves an optional source account", () => {
const source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
const op = Operation.changeTrust({ asset: usd, source });
const obj = Operation.fromXDRObject(
xdr.Operation.fromXDR(op.toXDR("hex"), "hex"),
);

if (obj.type !== "changeTrust") throw new Error("unexpected type");
expect(obj.source).toBe(source);
});
});
46 changes: 46 additions & 0 deletions test/unit/operations/claim_claimable_balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect } from "vitest";
import { Operation } from "../../../src/operation.js";
import xdr from "../../../src/xdr.js";

const balanceId =
"00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be";

describe("Operation.claimClaimableBalance()", () => {
it("creates a claimClaimableBalanceOp", () => {
const op = Operation.claimClaimableBalance({ balanceId });
const xdrHex = op.toXDR("hex");
const operation = xdr.Operation.fromXDR(xdrHex, "hex");
const obj = Operation.fromXDRObject(operation);

expect(obj.type).toBe("claimClaimableBalance");
if (obj.type !== "claimClaimableBalance")
throw new Error("unexpected type");

expect(obj.balanceId).toBe(balanceId);
});

it("throws when balanceId is not present", () => {
expect(() =>
// @ts-expect-error: intentionally omitting required field to test runtime validation
Operation.claimClaimableBalance({}),
).toThrow(/must provide a valid claimable balance id/);
});

it("throws for an invalid balanceId", () => {
expect(() =>
Operation.claimClaimableBalance({ balanceId: "badc0ffee" }),
).toThrow(/must provide a valid claimable balance id/);
});

it("preserves an optional source account", () => {
const source = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ";
const op = Operation.claimClaimableBalance({ balanceId, source });
const obj = Operation.fromXDRObject(
xdr.Operation.fromXDR(op.toXDR("hex"), "hex"),
);

if (obj.type !== "claimClaimableBalance")
throw new Error("unexpected type");
expect(obj.source).toBe(source);
});
});
Loading