diff --git a/packages/sdk-core/tests/resources/Powerup.test.ts b/packages/sdk-core/tests/resources/Powerup.test.ts new file mode 100644 index 0000000..57e4711 --- /dev/null +++ b/packages/sdk-core/tests/resources/Powerup.test.ts @@ -0,0 +1,181 @@ +import { UInt128, UInt64 } from "@wireio/sdk-core/chain/Integer" +import { BNPrecision, PowerUpState, Resources } from "@wireio/sdk-core" +import { PowerUpStateResourceCPU } from "@wireio/sdk-core/resources/powerup/Cpu" +import { PowerUpStateResourceNET } from "@wireio/sdk-core/resources/powerup/Net" +import { createSampleUsage } from "../support/resources" + +/** Matches `PowerUpStateResource` default block CPU limit (μs). */ +const DEFAULT_BLOCK_CPU_LIMIT = 200_000 +/** Matches `PowerUpStateResource` default block net limit (bytes). */ +const DEFAULT_BLOCK_NET_LIMIT = 1_048_576_000 +/** + * Day-window multiplier used by PowerUp `*_per_day` helpers: + * `limit * 2 * 60 * 60 * 24`. + */ +const POWERUP_DAY_WINDOW = 2 * 60 * 60 * 24 + +/** Shared PowerUp resource fields for CPU/NET fixtures. */ +function powerUpResourceFields(overrides: Record = {}) { + return { + version: 0, + weight: "100000000000000", + weight_ratio: "10000000000000", + assumed_stake_weight: "1", + initial_weight_ratio: "100000000000000", + target_weight_ratio: "10000000000000", + initial_timestamp: "2020-01-01T00:00:00", + target_timestamp: "2021-01-01T00:00:00", + exponent: "2", + decay_secs: 86_400, + min_price: "0.0000 SYS", + max_price: "1000.0000 SYS", + utilization: "0", + adjusted_utilization: "0", + utilization_timestamp: "2020-01-01T00:00:00", + ...overrides + } +} + +function createCpuState(overrides: Record = {}) { + return PowerUpStateResourceCPU.from(powerUpResourceFields(overrides)) +} + +function createNetState(overrides: Record = {}) { + return PowerUpStateResourceNET.from(powerUpResourceFields(overrides)) +} + +function createPowerUpState( + overrides: Record = {} +): InstanceType { + return PowerUpState.from({ + version: 0, + net: powerUpResourceFields(), + cpu: powerUpResourceFields(), + powerup_days: 1, + min_powerup_fee: "0.0001 SYS", + ...overrides + }) +} + +/** Mocked `Resources` parent that returns a fixed `powup.state` table row. */ +function createPowerUpResources(state = createPowerUpState()) { + const getTableRows = jest.fn(async () => ({ + rows: [state], + more: false + })) + + const resources = new Resources({ + api: { + v1: { + chain: { + get_table_rows: getTableRows + } + } + } as any + }) + + return { resources, getTableRows, state } +} + +describe("PowerUpStateResourceCPU", () => { + test("us_per_day uses the default block CPU limit", () => { + expect(createCpuState().us_per_day()).toBe( + DEFAULT_BLOCK_CPU_LIMIT * POWERUP_DAY_WINDOW + ) + }) + + test("us_per_day honors virtual_block_cpu_limit overrides", () => { + expect( + createCpuState().us_per_day({ + virtual_block_cpu_limit: UInt64.from(100_000) + }) + ).toBe(100_000 * POWERUP_DAY_WINDOW) + }) + + test("ms_per_day and per_day derive from us_per_day", () => { + const cpu = createCpuState() + expect(cpu.ms_per_day()).toBe(cpu.us_per_day() / 1000) + expect(cpu.per_day()).toBe(cpu.us_per_day()) + }) + + test("weight_to_us and us_to_weight round-trip with BNPrecision", () => { + const cpu = createCpuState() + const sample = UInt128.from(100_000_000) + const weight = BNPrecision.toNumber() + const us = cpu.weight_to_us(sample, weight) + expect(us).toBe(100_000_000) + expect(cpu.us_to_weight(sample, us)).toBe(weight) + }) + + test("frac_by_ms multiplies microseconds by 1000", () => { + const cpu = createCpuState() + const sample = createSampleUsage() + expect(cpu.frac_by_ms(sample, 1)).toBe(cpu.frac_by_us(sample, 1000)) + }) + + test("price_per_us returns a finite non-negative fee", () => { + const price = createCpuState().price_per_us(createSampleUsage(), 1000) + expect(Number.isFinite(price)).toBe(true) + expect(price).toBeGreaterThanOrEqual(0) + }) + + test("allocated reflects the weight_ratio shift toward PowerUp", () => { + // 1 - (1e13 / 1e13 / 100) = 0.99 + expect(createCpuState().allocated).toBeCloseTo(0.99) + }) + + test("symbol comes from min_price", () => { + expect(createCpuState().symbol.name).toBe("SYS") + }) +}) + +describe("PowerUpStateResourceNET", () => { + test("bytes_per_day uses the default block net limit", () => { + expect(createNetState().bytes_per_day()).toBe( + DEFAULT_BLOCK_NET_LIMIT * POWERUP_DAY_WINDOW + ) + }) + + test("kb_per_day and per_day derive from bytes_per_day", () => { + const net = createNetState() + expect(net.kb_per_day()).toBe(net.bytes_per_day() / 1000) + expect(net.per_day()).toBe(net.bytes_per_day()) + }) + + test("weight_to_bytes and bytes_to_weight round-trip with BNPrecision", () => { + const net = createNetState() + const sample = UInt128.from(100_000_000) + const weight = BNPrecision.toNumber() + const bytes = net.weight_to_bytes(sample, weight) + expect(bytes).toBe(100_000_000) + expect(net.bytes_to_weight(sample, bytes)).toBe(weight) + }) + + test("frac_by_kb multiplies bytes by 1000", () => { + const net = createNetState() + const sample = createSampleUsage() + expect(net.frac_by_kb(sample, 1)).toBe(net.frac_by_bytes(sample, 1000)) + }) + + test("price_per_byte returns a finite non-negative fee", () => { + const price = createNetState().price_per_byte(createSampleUsage(), 1000) + expect(Number.isFinite(price)).toBe(true) + expect(price).toBeGreaterThanOrEqual(0) + }) +}) + +describe("PowerUpAPI", () => { + test("get_state reads sysio//powup.state and returns the first row", async () => { + const { resources, getTableRows, state } = createPowerUpResources() + const result = await resources.v1.powerup.get_state() + + expect(result).toBe(state) + expect(result).toBeInstanceOf(PowerUpState) + expect(getTableRows).toHaveBeenCalledWith({ + code: "sysio", + scope: "", + table: "powup.state", + type: PowerUpState + }) + }) +}) diff --git a/packages/sdk-core/tests/resources/Ram.test.ts b/packages/sdk-core/tests/resources/Ram.test.ts new file mode 100644 index 0000000..191a772 --- /dev/null +++ b/packages/sdk-core/tests/resources/Ram.test.ts @@ -0,0 +1,94 @@ +import { Asset } from "@wireio/sdk-core/chain/Asset" +import { Int64 } from "@wireio/sdk-core/chain/Integer" +import { Resources, RAMState } from "@wireio/sdk-core" + +/** Canonical RAM market row used by `sysio.system` bancor pricing tests. */ +function createRamState( + overrides: Record = {} +): InstanceType { + return RAMState.from({ + supply: "10000000000.0000 RAMCORE", + base: { + balance: "10000000000.0000 RAM", + weight: "0.50000000000000000" + }, + quote: { + balance: "10000.0000 SYS", + weight: "0.50000000000000000" + }, + ...overrides + }) +} + +/** Mocked `Resources` parent that returns a fixed `rammarket` table row. */ +function createRamResources(state = createRamState()) { + const getTableRows = jest.fn(async () => ({ + rows: [state], + more: false + })) + + const resources = new Resources({ + api: { + v1: { + chain: { + get_table_rows: getTableRows + } + } + } as any + }) + + return { resources, getTableRows, state } +} + +describe("RAMState", () => { + test("get_input ceil-divides quote * value by (base - value)", () => { + const state = createRamState() + // ceil((100 * 10) / (1000 - 10)) = ceil(1.0101...) = 2 + expect( + Number(state.get_input(Int64.from(1000), Int64.from(100), Int64.from(10))) + ).toBe(2) + }) + + test("price_per returns a SYS asset for the requested byte count", () => { + const price = createRamState().price_per(1024) + expect(price).toBeInstanceOf(Asset) + expect(price.symbol.name).toBe("SYS") + expect(price.units.value.gtn(0)).toBe(true) + }) + + test("price_per_kb prices one kilobyte as 1000 bytes", () => { + const state = createRamState() + expect(state.price_per_kb(1).equals(state.price_per(1000))).toBe(true) + }) + + test("price_per scales up for larger byte requests on a shallow market", () => { + const state = createRamState({ + base: { + balance: "100000.0000 RAM", + weight: "0.50000000000000000" + }, + quote: { + balance: "10000.0000 SYS", + weight: "0.50000000000000000" + } + }) + expect( + state.price_per(20_000).units.value.gt(state.price_per(1_000).units.value) + ).toBe(true) + }) +}) + +describe("RAMAPI", () => { + test("get_state reads sysio/sysio/rammarket and returns the first row", async () => { + const { resources, getTableRows, state } = createRamResources() + const result = await resources.v1.ram.get_state() + + expect(result).toBe(state) + expect(getTableRows).toHaveBeenCalledWith({ + code: "sysio", + scope: "sysio", + table: "rammarket", + type: RAMState + }) + }) +}) diff --git a/packages/sdk-core/tests/resources/Resources.test.ts b/packages/sdk-core/tests/resources/Resources.test.ts new file mode 100644 index 0000000..79d6700 --- /dev/null +++ b/packages/sdk-core/tests/resources/Resources.test.ts @@ -0,0 +1,93 @@ +import { UInt128 } from "@wireio/sdk-core/chain/Integer" +import { BNPrecision, Resources } from "@wireio/sdk-core" +import BN from "bn.js" + +/** Account shape required by `Resources.getSampledUsage`. */ +function createAccountObject(overrides: Record = {}) { + return { + cpu_limit: { max: UInt128.from(200_000) }, + net_limit: { max: UInt128.from(1_048_576) }, + cpu_weight: UInt128.from(10_000), + net_weight: UInt128.from(20_000), + ...overrides + } +} + +describe("Resources", () => { + test("throws when neither url nor api is provided", () => { + expect(() => new Resources({} as any)).toThrow("Missing url or api client") + }) + + test("accepts an injected api client and default sample settings", () => { + const api = { v1: { chain: {} } } as any + const resources = new Resources({ api }) + + expect(resources.api).toBe(api) + expect(resources.sampleAccount).toBe("greymassfuel") + expect(resources.symbol).toBe("4,SYS") + expect(resources.v1.ram).toBeDefined() + expect(resources.v1.rex).toBeDefined() + expect(resources.v1.powerup).toBeDefined() + }) + + test("honors sampleAccount and symbol overrides", () => { + const resources = new Resources({ + api: { v1: { chain: {} } } as any, + sampleAccount: "fuel.sample", + symbol: "4,WIRE" + }) + + expect(resources.sampleAccount).toBe("fuel.sample") + expect(resources.symbol).toBe("4,WIRE") + }) + + test("BNPrecision is 1e8", () => { + expect(BNPrecision.eq(new BN(100_000_000))).toBe(true) + }) + + test("getSampledUsage samples the configured account and ceil-divides weights", async () => { + const account = createAccountObject() + const getAccount = jest.fn(async () => account) + const resources = new Resources({ + api: { + v1: { + chain: { + get_account: getAccount + } + } + } as any, + sampleAccount: "fuel.sample" + }) + + const sample = await resources.getSampledUsage() + + expect(getAccount).toHaveBeenCalledWith("fuel.sample") + expect(sample.account).toBe(account) + expect(sample.cpu).toBeInstanceOf(UInt128) + expect(sample.net).toBeInstanceOf(UInt128) + + // Mirror Resources.divCeil: (limit * BNPrecision) / weight, then + // subtract one when there is a remainder and the quotient is > 1. + const expectedCpu = divCeilSample( + account.cpu_limit.max.value, + account.cpu_weight.value + ) + const expectedNet = divCeilSample( + account.net_limit.max.value, + account.net_weight.value + ) + + expect(Number(sample.cpu)).toBe(Number(expectedCpu)) + expect(Number(sample.net)).toBe(Number(expectedNet)) + }) +}) + +/** Local copy of `Resources` sampling math for assertion expected values. */ +function divCeilSample(limit: BN, weight: BN): BN { + const num = limit.mul(BNPrecision) + let quotient = num.div(weight) + if (num.mod(weight).gtn(0) && quotient.gtn(1)) { + quotient = quotient.subn(1) + } + return quotient +} diff --git a/packages/sdk-core/tests/resources/Rex.test.ts b/packages/sdk-core/tests/resources/Rex.test.ts new file mode 100644 index 0000000..4d887fe --- /dev/null +++ b/packages/sdk-core/tests/resources/Rex.test.ts @@ -0,0 +1,92 @@ +import { Asset } from "@wireio/sdk-core/chain/Asset" +import { Resources, REXState } from "@wireio/sdk-core" +import { createSampleUsage } from "../support/resources" + +/** Canonical REX pool row for pricing and exchange helpers. */ +function createRexState( + overrides: Record = {} +): InstanceType { + return REXState.from({ + version: 0, + total_lent: "1000.0000 SYS", + total_unlent: "9000.0000 SYS", + total_rent: "100.0000 SYS", + total_lendable: "10000.0000 SYS", + total_rex: "10000.0000 REX", + namebid_proceeds: "0.0000 SYS", + loan_num: 1, + ...overrides + }) +} + +/** Mocked `Resources` parent that returns a fixed `rexpool` table row. */ +function createRexResources(state = createRexState()) { + const getTableRows = jest.fn(async () => ({ + rows: [state], + more: false + })) + + const resources = new Resources({ + api: { + v1: { + chain: { + get_table_rows: getTableRows + } + } + } as any + }) + + return { resources, getTableRows, state } +} + +describe("REXState", () => { + test("reserved is the lent / lendable ratio", () => { + expect(createRexState().reserved).toBe(0.1) + }) + + test("symbol and precision come from total_lent", () => { + const state = createRexState() + expect(state.symbol.name).toBe("SYS") + expect(state.precision).toBe(4) + }) + + test("value is (lent + unlent) / total_rex in units", () => { + // (10000000 + 90000000) / 100000000 = 1 + expect(createRexState().value).toBe(1) + }) + + test("exchange converts REX into system tokens at the pool rate", () => { + const exchanged = createRexState().exchange(Asset.from("1.0000 REX")) + expect(exchanged.toString()).toBe("1.0000 SYS") + }) + + test("price_per returns a positive SYS fee for sampled CPU", () => { + const price = createRexState().price_per(createSampleUsage(), 1000) + expect(typeof price).toBe("number") + expect(price).toBeGreaterThan(0) + }) + + test("price_per scales linearly with the requested unit count", () => { + const state = createRexState() + const sample = createSampleUsage() + expect(state.price_per(sample, 2000)).toBeCloseTo( + state.price_per(sample, 1000) * 2, + 8 + ) + }) +}) + +describe("REXAPI", () => { + test("get_state reads sysio/sysio/rexpool and returns the first row", async () => { + const { resources, getTableRows, state } = createRexResources() + const result = await resources.v1.rex.get_state() + + expect(result).toBe(state) + expect(getTableRows).toHaveBeenCalledWith({ + code: "sysio", + scope: "sysio", + table: "rexpool", + type: REXState + }) + }) +}) diff --git a/packages/sdk-core/tests/support/resources.ts b/packages/sdk-core/tests/support/resources.ts new file mode 100644 index 0000000..4bc596e --- /dev/null +++ b/packages/sdk-core/tests/support/resources.ts @@ -0,0 +1,14 @@ +import { UInt128 } from "@wireio/sdk-core/chain/Integer" +import { SampleUsage } from "@wireio/sdk-core" + +/** Minimal sampled usage used by REX / PowerUp pricing helpers. */ +export function createSampleUsage( + overrides: Partial = {} +): SampleUsage { + return { + account: {} as SampleUsage["account"], + cpu: UInt128.from(100_000_000), + net: UInt128.from(100_000_000), + ...overrides + } +}